Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 16 additions & 12 deletions apps/frontend/DESIGN_SYSTEM.md
Original file line number Diff line number Diff line change
Expand Up @@ -350,22 +350,21 @@ Each palette supplies a deeper shade and a brighter shade. `action` uses the
deeper shade in light mode and the brighter shade in dark mode for links,
focus borders, selection indicators, and compact status UI. Pair these filled
indicators with `on-action`. `button-action` uses the deeper shade in both
themes and pairs with `on-button-action` for white labels. Keep the gloss in
themes and pairs with `on-button-action` for white labels. Keep the lighting in
contrast checks. Warning, danger, presence, and server identity colours do not
change with the accent. Do not use the selected accent as the only way to
communicate status.

Filled buttons use a subtle raised finish: a faint top highlight, a soft lower
edge, and a small shadow over the semantic fill. Pressed buttons look inset.
Ghost buttons stay flat. Disabled buttons have no gloss or raised edge.
Filled buttons use the same quiet finish as the composer: a faint top highlight
and soft rim over the semantic fill, without a cast shadow. Pressed buttons
change fill. Ghost buttons stay flat. Disabled buttons have no lighting or raised edge.
`ToggleChip` uses the same `btn` foundation through `toggle-chip`, including
compact admin actions. It uses quiet `shell-lighting` without an outer shadow
so dense action rows stay subtle. Neutral chips use `input-border` to keep
a visible boundary on grey surfaces. Its labelled form uses the standard button radius;
its square form uses the compact icon radius.
Fills retain a matched tonal border. Secondary buttons use a quiet
`surface-emphasized` fill and an input-coloured border, so they stay visible
inside a `surface` card. Filled buttons and selected pane-header icon buttons
Coloured fills retain a matched tonal border. Secondary buttons use a quiet
`surface-emphasized` fill and transparent border space. Filled buttons, Select fields, and selected pane-header icon buttons
share the `control-raised` finish. Inactive header icons stay flat. Do not add
local gloss, blur, transparency, or extra shadows.

Expand Down Expand Up @@ -400,23 +399,28 @@ instead of adding local gradients or arbitrary inset shadows.
| --- | --- |
| `surface-raised` | Base raised finish. Semantic utilities set `--lighting-*` strength. |
| `surface-lowered` | Base recessed finish. Semantic utilities set `--lowered-shadow` and `--lowered-edge`. |
| `control-raised` | Filled buttons and selected header icons; includes pressed and disabled states. |
| `control-raised` | Buttons, Select fields, and selected header icons; shared shell lighting with disabled-state handling and no cast shadow. Semantic utilities supply fill changes for press feedback. |
| `option-depth` | Quiet checkbox and radio rows; removes depth when disabled. |
| `control-well` | Empty checkbox and radio indicators. |
| `selection-indicator` | Soft lighting on selected checkbox and radio indicators, without a drop shadow. |
| `shell-surface` | User card and call participant cards; soft rim with no button elevation or pressed finish. |
| `shell-action` | Standalone native buttons in the bottom row, such as Start call. Uses the same surface and 48 px minimum height as the composer and user card, with hover, focus, pressed, and disabled states. |
| `chat-input-surface` | Composer and sidebar search fields; the same quiet raised `shell-surface` finish as the user card. |
| `shell-lighting` | The same quiet finish over server gutter artwork; does not change the image or intercept clicks. |
| `shell-lighting` | Shared quiet finish for shell surfaces and raised controls. Also lights server gutter artwork without changing the image or intercepting clicks. |
| `floating-frame` | Lit panel, dialog, and menu frames. |
| `floating-inset` | Recessed content inside those frames. |
| `app-frame-shell` / `app-frame-inset` | Desktop app frame with a flat fill and no outer border or highlight in any depth mode. Light mode raises the content area with a light upper/left edge, dark lower/right edge, and soft outer shadow. Dark mode keeps the recessed content edge and inset shadow. No full-window gradient. The inset overlay passes pointer input through to the panes. Mobile stays edge-to-edge. |
| `accent-swatch` | Palette samples with their own colour gradient and shared lit edges. |

The shared `--shell-*` theme tokens soften shell bevels in light mode with a
cleaner top highlight, less lower shading, and a small edge blur. Dark mode
keeps its sharper, low-light finish. Composer, user card, and pill segments
all use the same recipe.
keeps its sharper, low-light finish. Composer, user card, pill segments, buttons,
Select fields, and selected toolbar controls all use the same recipe. Buttons
have no cast shadow or inset pressed effect. Secondary buttons keep transparent
border space; Select fields retain their field boundary. Ghost buttons stay flat.
Disabled and loading controls, including button-like links, have no decorative
lighting. See `Form/Button` → `Shared quiet depth` for a comparison in all three
depth modes.

Keep the `--lighting-*` and `--lowered-*` parameters inside semantic utilities.
Each depth primitive resets its parameters so a nested control does not inherit the
Expand Down Expand Up @@ -685,7 +689,7 @@ closes. `PaneHeader` opts in with `hideOnKeyboard`; thread and settings headers
stay visible. The `keyboard-hide-mobile` utility removes the complete header
from the layout without an animation. Input focus alone does not hide it.

Controls use solid semantic fills, with a subtle gloss on filled buttons;
Controls use solid semantic fills, with quiet shell lighting on filled buttons;
borders define structure, and shadows are reserved for genuinely floating or
raised surfaces. Do not use decorative one-sided accent borders or inset edge
stripes on cards, rows, panels, or selected states. When a boundary is needed,
Expand Down
43 changes: 18 additions & 25 deletions apps/frontend/src/app.css
Original file line number Diff line number Diff line change
Expand Up @@ -478,13 +478,10 @@
linear-gradient(to bottom right, color-mix(in srgb, var(--accent-strong), var(--accent-bright) calc(min(1, var(--depth-strength)) * 100%)), var(--accent-strong));
}

/* Shared lighting for filled buttons and selected header icon controls. */
/* Interactive controls share the shell finish. Disabled controls lose lighting;
* semantic utilities own fill changes for hover and press feedback. */
@utility control-raised {
@apply surface-raised shadow-[0_1px_2px_rgb(0_0_0/calc(0.12*var(--depth-strength)))];
/* Keep white labels readable even at the strongest depth setting. */
--lighting-top: rgb(255 255 255 / min(0.14, calc(0.1 * var(--depth-strength))));
@apply enabled:active:shadow-none enabled:active:inset-shadow-[0_1px_2px_rgb(0_0_0/calc(0.12*var(--depth-strength)))];
@apply disabled:shadow-none disabled:inset-shadow-none;
@apply shell-lighting shadow-none disabled:inset-shadow-none;

&:disabled,
&[aria-disabled='true'] {
Expand All @@ -506,7 +503,7 @@

/* Compact actions and independent toggles share the standard button foundation. */
@utility toggle-chip {
@apply btn shell-lighting shadow-none feedback-quick gap-1.5 px-2.5 text-xs;
@apply btn feedback-quick gap-1.5 px-2.5 text-xs;
}

@utility button-content {
Expand Down Expand Up @@ -612,46 +609,51 @@

@utility btn-neutral {
@apply btn border-neutral-action bg-neutral-action text-on-neutral-action hover:border-neutral-action-hover hover:bg-neutral-action-hover;
@apply active:bg-neutral-action-hover;
}

@utility btn-action {
@apply btn border-(--color-button-action) bg-(--color-button-action) font-semibold text-(--color-on-button-action) hover:border-(--color-button-action-hover) hover:bg-(--color-button-action-hover);
@apply active:bg-(--color-button-action-hover);
}

@utility btn-success {
@apply btn border-(--color-button-success) bg-(--color-button-success) font-semibold text-white hover:border-(--color-button-success-hover) hover:bg-(--color-button-success-hover);
@apply active:bg-(--color-button-success-hover);
}

@utility btn-secondary {
@apply btn border-input-border/70 bg-surface-emphasized/60 text-text;
@apply hover:border-input-border hover:bg-surface-emphasized;
@apply btn bg-surface-emphasized/60 text-text;
@apply hover:bg-surface-emphasized active:bg-surface-strong;
}

@utility btn-ghost {
@apply btn bg-transparent text-text shadow-none inset-shadow-none hover:bg-action/10 hover:text-action enabled:active:inset-shadow-none;
@apply btn bg-transparent text-text inset-shadow-none hover:bg-action/10 hover:text-action active:bg-action/15;
background-image: none;
}

@utility btn-warning {
@apply btn border-(--color-button-warning) bg-(--color-button-warning) font-semibold text-white hover:border-(--color-button-warning-hover) hover:bg-(--color-button-warning-hover);
@apply active:bg-(--color-button-warning-hover);
}

@utility btn-danger {
@apply btn border-(--color-button-danger) bg-(--color-button-danger) font-semibold text-white hover:border-(--color-button-danger-hover) hover:bg-(--color-button-danger-hover);
@apply active:bg-(--color-button-danger-hover);
}

/* Quiet destructive action: neutral at rest, destructive on hover/focus. */
@utility btn-danger-secondary {
@apply btn-secondary;
@apply hover:border-danger/30 hover:bg-danger/10 hover:text-danger;
@apply focus-visible:border-danger/30 focus-visible:bg-danger/10 focus-visible:text-danger focus-visible:ring-danger/25;
@apply hover:bg-danger/10 hover:text-danger active:bg-danger/15;
@apply focus-visible:bg-danger/10 focus-visible:text-danger focus-visible:ring-danger/25;
}

/* Quiet room-membership action that reveals its destructive meaning on hover. */
@utility btn-danger-ghost {
@apply btn bg-transparent text-muted shadow-none inset-shadow-none enabled:active:inset-shadow-none;
@apply btn bg-transparent text-muted inset-shadow-none;
background-image: none;
@apply hover:bg-danger/10 hover:text-danger;
@apply hover:bg-danger/10 hover:text-danger active:bg-danger/15;
}

@utility btn-sm {
Expand Down Expand Up @@ -695,11 +697,6 @@
@utility select-control {
@apply input max-w-full min-w-0 cursor-pointer text-base;
@apply control-raised bg-surface;
/* Select fields use softer lighting than action buttons. */
--lighting-top: rgb(255 255 255 / min(0.112, calc(0.08 * var(--depth-strength))));
--lighting-bottom: rgb(0 0 0 / calc(0.03 * var(--depth-strength)));
--lighting-edge-top: rgb(255 255 255 / calc(0.12 * var(--depth-strength)));
--lighting-edge-bottom: rgb(0 0 0 / calc(0.06 * var(--depth-strength)));
text-overflow: ellipsis;

@supports (appearance: base-select) {
Expand Down Expand Up @@ -998,11 +995,6 @@
@utility pane-header-icon-button-active {
@apply control-raised transition-[background-color,color,box-shadow] feedback-quick;
@apply bg-surface-emphasized text-text-top not-disabled:hover:bg-surface-emphasized not-disabled:active:bg-surface-selected focus-visible:bg-surface-emphasized;
/* Selected toolbar actions need less gloss than primary action buttons. */
--lighting-top: rgb(255 255 255 / calc(0.025 * var(--depth-strength)));
--lighting-bottom: rgb(0 0 0 / calc(0.01 * var(--depth-strength)));
--lighting-edge-top: rgb(255 255 255 / calc(0.04 * var(--depth-strength)));
--lighting-edge-bottom: rgb(0 0 0 / calc(0.02 * var(--depth-strength)));
}

/* Sidebar navigation utilities */
Expand Down Expand Up @@ -1208,7 +1200,8 @@
}
}

/* Resting shell surfaces have a soft rim, without button elevation or press feedback. */
/* Shared quiet finish for shell surfaces and raised controls. No outer shadow
* or interaction states: consumers own those separately. */
@utility shell-lighting {
@apply surface-raised;
--lighting-top: rgb(255 255 255 / calc(var(--shell-highlight) * var(--depth-strength)));
Expand Down
36 changes: 34 additions & 2 deletions apps/frontend/src/lib/ui/form/Button.stories.svelte
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
<script module lang="ts">
import { defineMeta } from '@storybook/addon-svelte-csf';
import Button from './Button.svelte';
import Select from './Select.svelte';
import HeaderIconButton from '../HeaderIconButton.svelte';

const componentDescription = `
Use Button for committed actions, form submits, destructive commands, and link-styled calls to
action. Keep modal footer actions visible and horizontal, using secondary for cancel and the
strongest applicable tone for the action. Labelled buttons share the rounded-xl radius
of chat input surfaces; icon-only buttons keep rounded-md corners. Filled buttons have a
subtle gloss and raised edge. Ghost buttons stay flat; disabled buttons lose the raised finish.
quiet shell lighting without a cast shadow. Ghost buttons stay flat; disabled buttons lose the raised finish.
`.trim();

const { Story } = defineMeta({
Expand Down Expand Up @@ -61,7 +63,7 @@
docs: {
description: {
story:
'Filled buttons use a faint top highlight, a soft lower edge, and a small shadow. Pressed buttons look inset. Secondary buttons keep a quiet surface fill; ghost buttons stay flat and use an action tint on hover.'
'Filled buttons share the soft rim and lighting of the composer. Fill changes give press feedback. Secondary buttons keep a quiet surface fill; ghost buttons stay flat and use an action tint on hover.'
}
}
}}
Expand All @@ -73,6 +75,36 @@
</div>
</Story>

<Story name="Shared quiet depth" asChild>
<div class="flex flex-col gap-6">
{#each [
{ label: 'Flat', strength: 0, width: 1 },
{ label: 'Kinda 3D', strength: 0.75, width: 1 },
{ label: 'Very 3D', strength: 1.75, width: 1.5 }
] as mode (mode.label)}
<section class="flex flex-col gap-3" style:--depth-strength={mode.strength} style:--depth-width={mode.width}>
<h2 class="font-semibold">{mode.label}</h2>
<div class="flex flex-wrap items-center gap-3">
<div class="chat-input-surface flex items-center px-4 text-muted">Composer surface</div>
<button type="button" class="shell-action">Start call</button>
<Button variant="secondary">Cancel</Button>
<Button>Current Server</Button>
<Button variant="danger">All Servers</Button>
<HeaderIconButton icon="icon-[uil--users-alt]" label="Members" tone="active" />
</div>
<div class="flex flex-wrap items-end gap-3">
<Select id={`quiet-depth-${mode.strength}`} label="Visibility" value="public" options={[{ value: 'public', label: 'Public' }, { value: 'private', label: 'Private' }]} />
<Button variant="ghost">Save draft</Button>
<Button href="#">Button link</Button>
<Button disabled>Disabled</Button>
<Button loading loadingText="Saving…">Save</Button>
<Button href="#" disabled>Disabled link</Button>
</div>
</section>
{/each}
</div>
</Story>

<Story
name="Sizes"
asChild
Expand Down
4 changes: 2 additions & 2 deletions apps/frontend/src/lib/ui/form/Select.stories.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -67,13 +67,13 @@
</Story>

<Story name="Flat" asChild>
<div class="max-w-md" style="--depth-strength: 0; --depth-width: 0">
<div class="max-w-md" style="--depth-strength: 0; --depth-width: 1">
<Select id="flat-role" label="Role" options={role} value="member" />
</div>
</Story>

<Story name="Kinda 3D" asChild>
<div class="max-w-md" style="--depth-strength: 1; --depth-width: 1">
<div class="max-w-md" style="--depth-strength: 0.75; --depth-width: 1">
<Select id="raised-role" label="Role" options={role} value="member" />
</div>
</Story>
Expand Down
Loading