@weichenlin/swell-ui 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -38,32 +38,127 @@ export function App() {
38
38
  }
39
39
  ```
40
40
 
41
- ### Override tokens
41
+ ### Change the brand
42
42
 
43
- Redefine any token after the import. Light values go in `@theme`; dark values in the same two dark
44
- blocks `theme.css` uses:
43
+ The default look is neutral gray with a near-black primary. A rebrand is two tokens, set once,
44
+ after the import:
45
45
 
46
46
  ```css
47
47
  @import 'tailwindcss';
48
48
  @import '@weichenlin/swell-ui/styles.css';
49
49
 
50
50
  @theme {
51
- --color-primary: #2563eb;
52
- --radius-md: 6px;
51
+ --color-primary: #f7a501; /* buttons, input focus, checked switches and checkboxes, halos */
52
+ --color-primary-fg: #141413; /* text and marks drawn on the primary fill */
53
53
  }
54
+ ```
55
+
56
+ That covers light, dark-from-the-system and `data-theme="dark"` alike. Input focus
57
+ (`--color-input-focus`), checked controls (`--color-control-checked`, `-fg`) and the focus halo
58
+ (`--color-input-halo`) are `var()` references to these two, resolved at runtime, so they follow.
59
+
60
+ To use a different value per scheme, write both on one line with `light-dark()`:
54
61
 
55
- :root[data-theme='dark'] {
56
- --color-primary: #60a5fa;
62
+ ```css
63
+ @theme {
64
+ --color-primary: light-dark(#2563eb, #60a5fa);
65
+ --color-primary-fg: light-dark(#ffffff, #0b1220);
57
66
  }
58
- :root:not([data-theme='light']) {
59
- @media (prefers-color-scheme: dark) {
60
- --color-primary: #60a5fa;
61
- }
67
+ ```
68
+
69
+ A light brand color (yellow, lime, cyan) can fall under 3:1 against the light page. The buttons
70
+ stay readable through `--color-primary-fg`, but if the focus border or a checked box needs more
71
+ contrast, override just that one:
72
+
73
+ ```css
74
+ @theme {
75
+ --color-input-focus: light-dark(#b37700, #f7a501); /* darker amber on light only */
76
+ }
77
+ ```
78
+
79
+ Set tokens in `@theme` (or a plain `:root` rule) rather than on an inner element: the derived
80
+ tokens are resolved on the root.
81
+
82
+ ### Tokens
83
+
84
+ Every color token is a `light-dark(<light>, <dark>)` pair; override it the same way, once. All of
85
+ them are also Tailwind utilities (`bg-surface`, `text-fg-muted`, `border-input-border`, ...).
86
+
87
+ | Token | Used for |
88
+ | ----------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------- |
89
+ | `--color-primary`, `--color-primary-fg` | Brand fill and the text on it |
90
+ | `--color-accent` | Secondary brand color; unused by components, defaults to primary |
91
+ | `--color-bg` | Page background |
92
+ | `--color-surface`, `-surface-raised`, `-surface-muted` | Cards and panels; raised (tooltips, sheets); muted (hover fills, secondary button) |
93
+ | `--color-fg`, `-fg-strong`, `-fg-muted`, `-fg-subtle` | Text, from strongest to quietest (`fg-subtle` stays >=4.5:1) |
94
+ | `--color-border`, `-border-strong` | Dividers and hairlines |
95
+ | `--color-ring`, `-ring-deep` | 1px button and tab-list rings |
96
+ | `--color-inverse`, `-inverse-fg` | `dark` button, tooltips |
97
+ | `--color-focus` | Keyboard focus outline (blue) |
98
+ | `--color-input-bg`, `-input-bg-hover` | Text field fill |
99
+ | `--color-input-border`, `-input-border-hover` | Text field border; also the off track of Switch and box of Checkbox |
100
+ | `--color-input-placeholder` | Placeholder text |
101
+ | `--color-input-focus` | Focused field border. Default: `var(--color-primary)` |
102
+ | `--color-input-halo` | 3px ring around a focused field. Default: input-focus at 14% |
103
+ | `--color-input-halo-invalid`, `-invalid-focus` | Same ring for invalid fields (danger at 12% / 22%) |
104
+ | `--color-input-highlight` | Inset top highlight on fields |
105
+ | `--color-control-checked`, `-control-checked-fg` | Checked Switch/Checkbox fill and its mark. Default: primary / primary-fg |
106
+ | `--color-popover-bg`, `-popover-border`, `-popover-highlight` | Select picker, dialogs; highlight is the active row |
107
+ | `--color-dialog-backdrop` | Dimmed page behind Dialog and Drawer |
108
+ | `--color-{success,warning,danger,info,neutral}` | Status tone: solid fill / icon |
109
+ | `--color-{tone}-fg` | Text on the solid fill |
110
+ | `--color-{tone}-subtle-bg`, `-subtle-fg`, `-border` | Tinted badge, its text and hairline |
111
+ | `--shadow-input`, `-input-lift`, `-switch-thumb`, `-popover`, `-dialog`, `-whisper` | Elevation (`shadow-*` utilities) |
112
+ | `--container-page` | `max-w-page` (1200px) |
113
+
114
+ ### Optional looks
115
+
116
+ ```css
117
+ @import 'tailwindcss';
118
+ @import '@weichenlin/swell-ui/styles.css';
119
+ @import '@weichenlin/swell-ui/themes/warm.css'; /* parchment surfaces, brown primary */
120
+ @import '@weichenlin/swell-ui/typography.css'; /* h1-h3 in the serif stack, weight 500 */
121
+ ```
122
+
123
+ Both are independent; your own `@theme` overrides go after them.
124
+
125
+ ### Fonts
126
+
127
+ swell-ui sets no font names: text uses Tailwind's `--font-sans` (the system UI stack), and
128
+ `font-serif` / `font-mono` are Tailwind's defaults too. To use a web font, ship its files with
129
+ your app, for example with [Fontsource](https://fontsource.org):
130
+
131
+ ```sh
132
+ pnpm add @fontsource-variable/inter
133
+ ```
134
+
135
+ ```css
136
+ @import 'tailwindcss';
137
+ @import '@fontsource-variable/inter';
138
+ @import '@weichenlin/swell-ui/styles.css';
139
+
140
+ @theme {
141
+ --font-sans: 'Inter Variable', ui-sans-serif, system-ui, sans-serif;
62
142
  }
63
143
  ```
64
144
 
65
- Note that the theme replaces Tailwind's `rounded-sm/md/lg` values (6, 8 and 12px) and adds
66
- `rounded-sharp` (4px), shadows (`shadow-popover`, `shadow-dialog`, ...) and `max-w-page`.
145
+ ### Radius
146
+
147
+ swell-ui does not touch Tailwind's radius scale; components use the stock `rounded-*` classes, so
148
+ `rounded-md` is still 6px in your app. Changing `--radius-*` in your `@theme` changes the
149
+ components too.
150
+
151
+ Coming from 0.1, which replaced the scale: shift each class one step up to keep the old size.
152
+
153
+ | 0.1 class | px | Now |
154
+ | --------------- | --- | ------------- |
155
+ | `rounded-sharp` | 4 | `rounded-sm` |
156
+ | `rounded-sm` | 6 | `rounded-md` |
157
+ | `rounded-md` | 8 | `rounded-lg` |
158
+ | `rounded-lg` | 12 | `rounded-xl` |
159
+ | `rounded-xl` | 16 | `rounded-2xl` |
160
+ | `rounded-2xl` | 24 | `rounded-3xl` |
161
+ | `rounded-3xl` | 32 | `rounded-4xl` |
67
162
 
68
163
  ### Tokens only
69
164
 
@@ -78,7 +173,8 @@ instead:
78
173
  ### `cn`
79
174
 
80
175
  `cn` (clsx + tailwind-merge) is exported and knows the custom tokens, so
81
- `cn('rounded-sharp', 'rounded-md')` gives `rounded-md`.
176
+ `cn('text-fg-muted', 'text-danger')` gives `text-danger` and `cn('shadow-popover', 'shadow-none')`
177
+ gives `shadow-none`.
82
178
 
83
179
  ## Dark mode
84
180
 
@@ -86,6 +182,10 @@ Without a `data-theme` attribute the theme follows the system (`prefers-color-sc
86
182
  `data-theme="light"` or `data-theme="dark"` on `<html>` forces one. The `dark:` variant follows the
87
183
  same rule.
88
184
 
185
+ Under the hood the root's `color-scheme` is `light dark`, `light` or `dark` accordingly, and every
186
+ color token picks its side with `light-dark()`. Browsers support it natively from Chrome 123,
187
+ Safari 17.5 and Firefox 120; Tailwind's production build rewrites it for older targets.
188
+
89
189
  `useTheme` persists the choice and sets the attribute; `ThemeSwitcher` is the toggle button:
90
190
 
91
191
  ```tsx
@@ -177,11 +277,11 @@ pnpm test # vitest (jsdom)
177
277
  pnpm typecheck # tsc -b
178
278
  pnpm lint # vp check: format, lint, type-aware lint
179
279
  pnpm knip # unused files, exports and dependencies
180
- pnpm build # vp pack -> dist/index.js, index.d.ts, theme.css, styles.css
280
+ pnpm build # vp pack -> dist/index.js, index.d.ts and the CSS files
181
281
  pnpm build-storybook
182
282
  ```
183
283
 
184
- `src/styles/theme.css` and `src/styles/styles.css` are copied into `dist/` as they are, so the
284
+ The CSS files under `src/styles/` are copied into `dist/` as they are, so the
185
285
  `@source "./*.js"` in `styles.css` is relative to `dist/`. Storybook and the lint use
186
286
  `src/styles/dev.css`, which adds Tailwind and scans `src/` and `stories/`.
187
287
 
package/dist/index.js CHANGED
@@ -57,15 +57,6 @@ const twMerge = extendTailwindMerge({ extend: { theme: {
57
57
  "neutral"
58
58
  ].flatMap(tone)
59
59
  ],
60
- radius: [
61
- "sharp",
62
- "sm",
63
- "md",
64
- "lg",
65
- "xl",
66
- "2xl",
67
- "3xl"
68
- ],
69
60
  shadow: [
70
61
  "input",
71
62
  "input-lift",
@@ -129,7 +120,7 @@ function Badge({ tone = "neutral", variant = "subtle", size = "md", icon, busy =
129
120
  return /* @__PURE__ */ jsxs("span", {
130
121
  ...props,
131
122
  "data-slot": "badge",
132
- className: cn("inline-flex items-center gap-1 rounded-sm border text-xs leading-none font-medium whitespace-nowrap", "forced-colors:border-[CanvasText]", sizeClasses$3[size].base, hasIcon && sizeClasses$3[size].withIcon, toneClass, className),
123
+ className: cn("inline-flex items-center gap-1 rounded-md border text-xs leading-none font-medium whitespace-nowrap", "forced-colors:border-[CanvasText]", sizeClasses$3[size].base, hasIcon && sizeClasses$3[size].withIcon, toneClass, className),
133
124
  children: [hasIcon && /* @__PURE__ */ jsx("span", {
134
125
  "aria-hidden": "true",
135
126
  "data-slot": "badge-icon",
@@ -374,7 +365,7 @@ function Tooltip({ content, side = "top", describe = true, className, children }
374
365
  }[placed]),
375
366
  children: /* @__PURE__ */ jsx("span", {
376
367
  ref: bubbleRef,
377
- className: cn("block w-max max-w-xs rounded-sm border border-border-strong bg-inverse px-2 py-1", "text-xs/snug font-medium text-inverse-fg shadow-whisper", "transition-[opacity,translate] duration-150 ease-out starting:opacity-0", {
368
+ className: cn("block w-max max-w-xs rounded-md border border-border-strong bg-inverse px-2 py-1", "text-xs/snug font-medium text-inverse-fg shadow-whisper", "transition-[opacity,translate] duration-150 ease-out starting:opacity-0", {
378
369
  top: "starting:translate-y-0.5",
379
370
  bottom: "starting:-translate-y-0.5",
380
371
  left: "starting:translate-x-0.5",
@@ -401,11 +392,11 @@ const buttonVariants = cva([
401
392
  ], {
402
393
  variants: {
403
394
  intent: {
404
- primary: "bg-primary text-primary-fg rounded-md shadow-[0_0_0_1px_var(--color-primary)] hover:not-aria-disabled:opacity-90",
405
- secondary: "bg-surface-muted text-fg-strong rounded-md shadow-[0_0_0_1px_var(--color-ring)] hover:not-aria-disabled:bg-ring",
406
- surface: "bg-surface-raised text-fg rounded-lg hover:not-aria-disabled:bg-surface-muted",
407
- dark: "bg-inverse text-inverse-fg rounded-md shadow-[0_0_0_1px_var(--color-ring)] hover:not-aria-disabled:opacity-90",
408
- ghost: "bg-transparent text-fg rounded-md hover:not-aria-disabled:bg-surface-muted"
395
+ primary: "bg-primary text-primary-fg rounded-lg shadow-[0_0_0_1px_var(--color-primary)] hover:not-aria-disabled:opacity-90",
396
+ secondary: "bg-surface-muted text-fg-strong rounded-lg shadow-[0_0_0_1px_var(--color-ring)] hover:not-aria-disabled:bg-ring",
397
+ surface: "bg-surface-raised text-fg rounded-xl hover:not-aria-disabled:bg-surface-muted",
398
+ dark: "bg-inverse text-inverse-fg rounded-lg shadow-[0_0_0_1px_var(--color-ring)] hover:not-aria-disabled:opacity-90",
399
+ ghost: "bg-transparent text-fg rounded-lg hover:not-aria-disabled:bg-surface-muted"
409
400
  },
410
401
  size: {
411
402
  sm: "h-8.5 px-3 text-sm",
@@ -616,7 +607,7 @@ function Card({ as: Comp = "div", className, ...props }) {
616
607
  "aria-labelledby": (Comp === "section" || Comp === "article") && titled && !props["aria-label"] ? titleId : void 0,
617
608
  ...props,
618
609
  "data-slot": "card",
619
- className: cn("rounded-lg border border-border-strong bg-surface", className)
610
+ className: cn("rounded-xl border border-border-strong bg-surface", className)
620
611
  })
621
612
  });
622
613
  }
@@ -738,7 +729,7 @@ function Notch({ ring, size, disabled, children }) {
738
729
  const halo = ring === "halo";
739
730
  return /* @__PURE__ */ jsx("fieldset", {
740
731
  "aria-hidden": true,
741
- className: cn("pointer-events-none absolute m-0 min-w-0 p-0", "transition-[border-color] duration-200 ease-in-out motion-reduce:transition-none", notchPadding[ring][size], halo ? ["-inset-1 rounded-[calc(var(--radius-md)+3px)] border-[3px] border-(--field-halo)", "forced-colors:invisible forced-colors:group-has-[:is(input,select):is(:focus,:open)]/control:visible forced-colors:border-[Highlight]"] : ["-inset-px rounded-[inherit] border border-(--field-border)", disabled && "forced-colors:border-[GrayText]"]),
732
+ className: cn("pointer-events-none absolute m-0 min-w-0 p-0", "transition-[border-color] duration-200 ease-in-out motion-reduce:transition-none", notchPadding[ring][size], halo ? ["-inset-1 rounded-[calc(var(--radius-lg)+3px)] border-[3px] border-(--field-halo)", "forced-colors:invisible forced-colors:group-has-[:is(input,select):is(:focus,:open)]/control:visible forced-colors:border-[Highlight]"] : ["-inset-px rounded-[inherit] border border-(--field-border)", disabled && "forced-colors:border-[GrayText]"]),
742
733
  children: /* @__PURE__ */ jsx("legend", {
743
734
  className: cn("invisible float-none block w-auto max-w-[0.01px] overflow-hidden p-0 text-xs whitespace-nowrap", halo ? "h-0.75" : "h-px", "transition-[max-width] duration-50 ease-out motion-reduce:transition-none", "[[data-slot=input-control]:is([data-start],:has(input:focus),:has(input:not(:placeholder-shown)),:has(input:autofill),:has(input:-webkit-autofill),:has(select:focus),:has(select:open),:has(select):not(:has(option[value=\"\"]:checked)))_&]:max-w-full", "[[data-slot=input-control]:is([data-start],:has(input:focus),:has(input:not(:placeholder-shown)),:has(input:autofill),:has(input:-webkit-autofill),:has(select:focus),:has(select:open),:has(select):not(:has(option[value=\"\"]:checked)))_&]:delay-50", "[[data-slot=input-control]:is([data-start],:has(input:focus),:has(input:not(:placeholder-shown)),:has(input:autofill),:has(input:-webkit-autofill),:has(select:focus),:has(select:open),:has(select):not(:has(option[value=\"\"]:checked)))_&]:duration-100"),
744
735
  children: /* @__PURE__ */ jsx("span", {
@@ -805,7 +796,7 @@ function FieldShell({ label, floating = false, helperText, error, size, id: idPr
805
796
  "data-slot": "input-control",
806
797
  "data-start": hasStart ? "" : void 0,
807
798
  onMouseDown: forwardClick,
808
- className: cn("group/control relative isolate flex items-center rounded-md border", floatingLabel ? "bg-transparent" : "[--field-bg:var(--color-input-bg)] bg-(--field-bg)", "transition-[border-color,box-shadow,background-color] duration-200 ease-in-out motion-reduce:transition-none", controlHeight[size], invalid ? ["[--field-border:var(--color-danger)] [--field-halo:var(--color-input-halo-invalid)]", "has-[:is(input,select):is(:focus,:open)]:[--field-halo:var(--color-input-halo-invalid-focus)]"] : ["[--field-border:var(--color-input-border)] [--field-halo:transparent]", "has-[:is(input,select):is(:focus,:open)]:[--field-border:var(--color-input-focus)] has-[:is(input,select):is(:focus,:open)]:[--field-halo:var(--color-input-halo)]"], "[--field-lift:0_0_#0000]", !invalid && !disabled && !readOnly && [
799
+ className: cn("group/control relative isolate flex items-center rounded-lg border", floatingLabel ? "bg-transparent" : "[--field-bg:var(--color-input-bg)] bg-(--field-bg)", "transition-[border-color,box-shadow,background-color] duration-200 ease-in-out motion-reduce:transition-none", controlHeight[size], invalid ? ["[--field-border:var(--color-danger)] [--field-halo:var(--color-input-halo-invalid)]", "has-[:is(input,select):is(:focus,:open)]:[--field-halo:var(--color-input-halo-invalid-focus)]"] : ["[--field-border:var(--color-input-border)] [--field-halo:transparent]", "has-[:is(input,select):is(:focus,:open)]:[--field-border:var(--color-input-focus)] has-[:is(input,select):is(:focus,:open)]:[--field-halo:var(--color-input-halo)]"], "[--field-lift:0_0_#0000]", !invalid && !disabled && !readOnly && [
809
800
  "hover:not-has-[:is(input,select):is(:focus,:open)]:[--field-border:var(--color-input-border-hover)]",
810
801
  !floatingLabel && "hover:not-has-[:is(input,select):is(:focus,:open)]:[--field-bg:var(--color-input-bg-hover)]",
811
802
  "hover:not-has-[:is(input,select):is(:focus,:open)]:[--field-lift:var(--shadow-input-lift)]"
@@ -1006,7 +997,7 @@ function Checkbox({ label, description, error, indeterminate = false, className,
1006
997
  "aria-labelledby": props["aria-labelledby"] ?? row.labelledBy,
1007
998
  "aria-describedby": row.describedBy,
1008
999
  "aria-invalid": row.invalid ? true : ariaInvalid,
1009
- className: cn("peer col-start-1 row-start-1 m-0 size-4 cursor-pointer appearance-none rounded-sharp border outline-hidden", "disabled:cursor-not-allowed", "border-(--checkbox-border) bg-(--checkbox-bg) [box-shadow:0_0_0_3px_var(--checkbox-halo)]", "transition-[background-color,border-color,box-shadow] duration-200 ease-in-out", "[--checkbox-bg:var(--color-input-bg)] [--checkbox-halo:transparent]", "checked:[--checkbox-bg:var(--color-control-checked)] indeterminate:[--checkbox-bg:var(--color-control-checked)]", row.invalid ? [
1000
+ className: cn("peer col-start-1 row-start-1 m-0 size-4 cursor-pointer appearance-none rounded-sm border outline-hidden", "disabled:cursor-not-allowed", "border-(--checkbox-border) bg-(--checkbox-bg) [box-shadow:0_0_0_3px_var(--checkbox-halo)]", "transition-[background-color,border-color,box-shadow] duration-200 ease-in-out", "[--checkbox-bg:var(--color-input-bg)] [--checkbox-halo:transparent]", "checked:[--checkbox-bg:var(--color-control-checked)] indeterminate:[--checkbox-bg:var(--color-control-checked)]", row.invalid ? [
1010
1001
  "[--checkbox-border:var(--color-danger)] [--checkbox-halo:var(--color-input-halo-invalid)]",
1011
1002
  "focus-visible:[--checkbox-halo:var(--color-input-halo-invalid-focus)]",
1012
1003
  "not-forced-colors:checked:focus-visible:outline-danger not-forced-colors:indeterminate:focus-visible:outline-danger"
@@ -1331,7 +1322,7 @@ function Dialog({ size = "md", className, children, ...props }) {
1331
1322
  className: cn("m-auto w-[calc(100%-2rem)] max-h-[calc(100dvh-64px)] overflow-visible border-0 bg-transparent p-0 text-fg", "overscroll-contain outline-none", sizeClasses$2[size], motionClasses$1, sheetClasses, className),
1332
1323
  children: /* @__PURE__ */ jsx("div", {
1333
1324
  "data-slot": "dialog-panel",
1334
- className: cn("flex max-h-[inherit] flex-col rounded-lg border border-popover-border bg-popover-bg shadow-dialog", "forced-colors:border-[CanvasText]", "max-sm:rounded-b-none max-sm:border-b-0 max-sm:pb-[env(safe-area-inset-bottom)]"),
1325
+ className: cn("flex max-h-[inherit] flex-col rounded-xl border border-popover-border bg-popover-bg shadow-dialog", "forced-colors:border-[CanvasText]", "max-sm:rounded-b-none max-sm:border-b-0 max-sm:pb-[env(safe-area-inset-bottom)]"),
1335
1326
  children
1336
1327
  })
1337
1328
  });
@@ -1349,7 +1340,7 @@ function DialogHeader({ title, description, closeButton, className, ...props })
1349
1340
  className: "min-w-0 flex-1",
1350
1341
  children: [/* @__PURE__ */ jsx("h2", {
1351
1342
  id: context.titleId,
1352
- className: "m-0 text-xl/tight text-fg",
1343
+ className: "m-0 text-xl/tight font-medium text-fg",
1353
1344
  children: title
1354
1345
  }), hasDescription && /* @__PURE__ */ jsx("p", {
1355
1346
  id: context.descriptionId,
@@ -1455,7 +1446,7 @@ function DrawerHeader({ title, description, className, ...props }) {
1455
1446
  className: "min-w-0 flex-1",
1456
1447
  children: [/* @__PURE__ */ jsx("h2", {
1457
1448
  id: context.titleId,
1458
- className: "m-0 text-xl/tight wrap-anywhere text-fg",
1449
+ className: "m-0 text-xl/tight font-medium wrap-anywhere text-fg",
1459
1450
  children: title
1460
1451
  }), hasDescription && /* @__PURE__ */ jsx("p", {
1461
1452
  id: context.descriptionId,
@@ -1708,9 +1699,9 @@ const labelRestingWidth = {
1708
1699
  lg: "max-w-[calc(100%-50px)]"
1709
1700
  };
1710
1701
  const isGroup = (item) => "options" in item;
1711
- const pickerClasses = cn("supports-[appearance:base-select]:[@media(hover:hover)_and_(pointer:fine)]:[appearance:base-select]", "supports-[appearance:base-select]:[@media(hover:hover)_and_(pointer:fine)]:[&::picker(select)]:[appearance:base-select]", "supports-[appearance:base-select]:[@media(hover:hover)_and_(pointer:fine)]:[&::picker-icon]:hidden", "supports-[appearance:base-select]:[@media(hover:hover)_and_(pointer:fine)]:[&::picker(select)]:[position-area:block-end_span-inline-end]", "supports-[appearance:base-select]:[@media(hover:hover)_and_(pointer:fine)]:[&::picker(select)]:[position-try-fallbacks:flip-block]", "supports-[appearance:base-select]:[@media(hover:hover)_and_(pointer:fine)]:[&::picker(select)]:mt-1", "supports-[appearance:base-select]:[@media(hover:hover)_and_(pointer:fine)]:[&::picker(select)]:min-w-[anchor-size(width)]", "supports-[appearance:base-select]:[@media(hover:hover)_and_(pointer:fine)]:[&::picker(select)]:max-h-[max(10rem,calc(100%-12px))]", "supports-[appearance:base-select]:[@media(hover:hover)_and_(pointer:fine)]:[&::picker(select)]:overflow-y-auto", "supports-[appearance:base-select]:[@media(hover:hover)_and_(pointer:fine)]:[&::picker(select)]:rounded-lg", "supports-[appearance:base-select]:[@media(hover:hover)_and_(pointer:fine)]:[&::picker(select)]:border", "supports-[appearance:base-select]:[@media(hover:hover)_and_(pointer:fine)]:[&::picker(select)]:border-popover-border", "supports-[appearance:base-select]:[@media(hover:hover)_and_(pointer:fine)]:[&::picker(select)]:bg-popover-bg", "supports-[appearance:base-select]:[@media(hover:hover)_and_(pointer:fine)]:[&::picker(select)]:p-1", "supports-[appearance:base-select]:[@media(hover:hover)_and_(pointer:fine)]:[&::picker(select)]:shadow-popover", "supports-[appearance:base-select]:[@media(hover:hover)_and_(pointer:fine)]:[&::picker(select)]:text-fg", "supports-[appearance:base-select]:[@media(hover:hover)_and_(pointer:fine)]:[&::picker(select)]:origin-top", "supports-[appearance:base-select]:[@media(hover:hover)_and_(pointer:fine)]:[&::picker(select)]:opacity-0", "supports-[appearance:base-select]:[@media(hover:hover)_and_(pointer:fine)]:[&::picker(select)]:scale-96", "supports-[appearance:base-select]:[@media(hover:hover)_and_(pointer:fine)]:[&::picker(select)]:transition-[opacity,scale,display,overlay]", "supports-[appearance:base-select]:[@media(hover:hover)_and_(pointer:fine)]:[&::picker(select)]:transition-discrete", "supports-[appearance:base-select]:[@media(hover:hover)_and_(pointer:fine)]:[&::picker(select)]:duration-80", "supports-[appearance:base-select]:[@media(hover:hover)_and_(pointer:fine)]:[&::picker(select)]:ease-out", "supports-[appearance:base-select]:[@media(hover:hover)_and_(pointer:fine)]:open:[&::picker(select)]:opacity-100", "supports-[appearance:base-select]:[@media(hover:hover)_and_(pointer:fine)]:open:[&::picker(select)]:scale-100", "supports-[appearance:base-select]:[@media(hover:hover)_and_(pointer:fine)]:open:[&::picker(select)]:duration-120", "supports-[appearance:base-select]:[@media(hover:hover)_and_(pointer:fine)]:open:[&::picker(select)]:starting:opacity-0", "supports-[appearance:base-select]:[@media(hover:hover)_and_(pointer:fine)]:open:[&::picker(select)]:starting:scale-96", "supports-[appearance:base-select]:[@media(hover:hover)_and_(pointer:fine)]:motion-reduce:[&::picker(select)]:transition-none");
1702
+ const pickerClasses = cn("supports-[appearance:base-select]:[@media(hover:hover)_and_(pointer:fine)]:[appearance:base-select]", "supports-[appearance:base-select]:[@media(hover:hover)_and_(pointer:fine)]:[&::picker(select)]:[appearance:base-select]", "supports-[appearance:base-select]:[@media(hover:hover)_and_(pointer:fine)]:[&::picker-icon]:hidden", "supports-[appearance:base-select]:[@media(hover:hover)_and_(pointer:fine)]:[&::picker(select)]:[position-area:block-end_span-inline-end]", "supports-[appearance:base-select]:[@media(hover:hover)_and_(pointer:fine)]:[&::picker(select)]:[position-try-fallbacks:flip-block]", "supports-[appearance:base-select]:[@media(hover:hover)_and_(pointer:fine)]:[&::picker(select)]:mt-1", "supports-[appearance:base-select]:[@media(hover:hover)_and_(pointer:fine)]:[&::picker(select)]:min-w-[anchor-size(width)]", "supports-[appearance:base-select]:[@media(hover:hover)_and_(pointer:fine)]:[&::picker(select)]:max-h-[max(10rem,calc(100%-12px))]", "supports-[appearance:base-select]:[@media(hover:hover)_and_(pointer:fine)]:[&::picker(select)]:overflow-y-auto", "supports-[appearance:base-select]:[@media(hover:hover)_and_(pointer:fine)]:[&::picker(select)]:rounded-xl", "supports-[appearance:base-select]:[@media(hover:hover)_and_(pointer:fine)]:[&::picker(select)]:border", "supports-[appearance:base-select]:[@media(hover:hover)_and_(pointer:fine)]:[&::picker(select)]:border-popover-border", "supports-[appearance:base-select]:[@media(hover:hover)_and_(pointer:fine)]:[&::picker(select)]:bg-popover-bg", "supports-[appearance:base-select]:[@media(hover:hover)_and_(pointer:fine)]:[&::picker(select)]:p-1", "supports-[appearance:base-select]:[@media(hover:hover)_and_(pointer:fine)]:[&::picker(select)]:shadow-popover", "supports-[appearance:base-select]:[@media(hover:hover)_and_(pointer:fine)]:[&::picker(select)]:text-fg", "supports-[appearance:base-select]:[@media(hover:hover)_and_(pointer:fine)]:[&::picker(select)]:origin-top", "supports-[appearance:base-select]:[@media(hover:hover)_and_(pointer:fine)]:[&::picker(select)]:opacity-0", "supports-[appearance:base-select]:[@media(hover:hover)_and_(pointer:fine)]:[&::picker(select)]:scale-96", "supports-[appearance:base-select]:[@media(hover:hover)_and_(pointer:fine)]:[&::picker(select)]:transition-[opacity,scale,display,overlay]", "supports-[appearance:base-select]:[@media(hover:hover)_and_(pointer:fine)]:[&::picker(select)]:transition-discrete", "supports-[appearance:base-select]:[@media(hover:hover)_and_(pointer:fine)]:[&::picker(select)]:duration-80", "supports-[appearance:base-select]:[@media(hover:hover)_and_(pointer:fine)]:[&::picker(select)]:ease-out", "supports-[appearance:base-select]:[@media(hover:hover)_and_(pointer:fine)]:open:[&::picker(select)]:opacity-100", "supports-[appearance:base-select]:[@media(hover:hover)_and_(pointer:fine)]:open:[&::picker(select)]:scale-100", "supports-[appearance:base-select]:[@media(hover:hover)_and_(pointer:fine)]:open:[&::picker(select)]:duration-120", "supports-[appearance:base-select]:[@media(hover:hover)_and_(pointer:fine)]:open:[&::picker(select)]:starting:opacity-0", "supports-[appearance:base-select]:[@media(hover:hover)_and_(pointer:fine)]:open:[&::picker(select)]:starting:scale-96", "supports-[appearance:base-select]:[@media(hover:hover)_and_(pointer:fine)]:motion-reduce:[&::picker(select)]:transition-none");
1712
1703
  function optionClasses(size, mono) {
1713
- return cn("bg-popover-bg text-fg checked:font-medium disabled:text-fg-subtle", textSize[size], mono && "font-mono", "supports-[appearance:base-select]:[@media(hover:hover)_and_(pointer:fine)]:flex", "supports-[appearance:base-select]:[@media(hover:hover)_and_(pointer:fine)]:cursor-default", "supports-[appearance:base-select]:[@media(hover:hover)_and_(pointer:fine)]:items-center", "supports-[appearance:base-select]:[@media(hover:hover)_and_(pointer:fine)]:gap-2", "supports-[appearance:base-select]:[@media(hover:hover)_and_(pointer:fine)]:rounded-md", "supports-[appearance:base-select]:[@media(hover:hover)_and_(pointer:fine)]:bg-transparent", "supports-[appearance:base-select]:[@media(hover:hover)_and_(pointer:fine)]:px-2", "supports-[appearance:base-select]:[@media(hover:hover)_and_(pointer:fine)]:py-1.5", "supports-[appearance:base-select]:[@media(hover:hover)_and_(pointer:fine)]:not-disabled:hover:bg-popover-highlight", "supports-[appearance:base-select]:[@media(hover:hover)_and_(pointer:fine)]:focus-visible:bg-popover-highlight", "supports-[appearance:base-select]:[@media(hover:hover)_and_(pointer:fine)]:focus-visible:outline-hidden", "supports-[appearance:base-select]:[@media(hover:hover)_and_(pointer:fine)]:disabled:text-fg", "supports-[appearance:base-select]:[@media(hover:hover)_and_(pointer:fine)]:disabled:opacity-50", "supports-[appearance:base-select]:[@media(hover:hover)_and_(pointer:fine)]:[&::checkmark]:order-1", "supports-[appearance:base-select]:[@media(hover:hover)_and_(pointer:fine)]:[&::checkmark]:ms-auto");
1704
+ return cn("bg-popover-bg text-fg checked:font-medium disabled:text-fg-subtle", textSize[size], mono && "font-mono", "supports-[appearance:base-select]:[@media(hover:hover)_and_(pointer:fine)]:flex", "supports-[appearance:base-select]:[@media(hover:hover)_and_(pointer:fine)]:cursor-default", "supports-[appearance:base-select]:[@media(hover:hover)_and_(pointer:fine)]:items-center", "supports-[appearance:base-select]:[@media(hover:hover)_and_(pointer:fine)]:gap-2", "supports-[appearance:base-select]:[@media(hover:hover)_and_(pointer:fine)]:rounded-lg", "supports-[appearance:base-select]:[@media(hover:hover)_and_(pointer:fine)]:bg-transparent", "supports-[appearance:base-select]:[@media(hover:hover)_and_(pointer:fine)]:px-2", "supports-[appearance:base-select]:[@media(hover:hover)_and_(pointer:fine)]:py-1.5", "supports-[appearance:base-select]:[@media(hover:hover)_and_(pointer:fine)]:not-disabled:hover:bg-popover-highlight", "supports-[appearance:base-select]:[@media(hover:hover)_and_(pointer:fine)]:focus-visible:bg-popover-highlight", "supports-[appearance:base-select]:[@media(hover:hover)_and_(pointer:fine)]:focus-visible:outline-hidden", "supports-[appearance:base-select]:[@media(hover:hover)_and_(pointer:fine)]:disabled:text-fg", "supports-[appearance:base-select]:[@media(hover:hover)_and_(pointer:fine)]:disabled:opacity-50", "supports-[appearance:base-select]:[@media(hover:hover)_and_(pointer:fine)]:[&::checkmark]:order-1", "supports-[appearance:base-select]:[@media(hover:hover)_and_(pointer:fine)]:[&::checkmark]:ms-auto");
1714
1705
  }
1715
1706
  function openPicker(select) {
1716
1707
  select.focus();
@@ -1882,7 +1873,7 @@ const listVariant = {
1882
1873
  "before:bottom-0 before:h-0.5 before:bg-fg before:[translate:var(--tabs-indicator-x)_0]",
1883
1874
  "before:transition-[translate,width]",
1884
1875
  "forced-colors:before:bg-[Highlight] forced-colors:before:[forced-color-adjust:none]",
1885
- "after:pointer-events-none after:absolute after:top-0 after:left-0 after:-z-1 after:rounded-md",
1876
+ "after:pointer-events-none after:absolute after:top-0 after:left-0 after:-z-1 after:rounded-lg",
1886
1877
  "after:bg-fg/7 after:opacity-0 data-hover:after:opacity-100",
1887
1878
  "after:h-(--tabs-hover-h) after:w-(--tabs-hover-w)",
1888
1879
  "after:[translate:var(--tabs-hover-x)_var(--tabs-hover-y)]",
@@ -1891,8 +1882,8 @@ const listVariant = {
1891
1882
  "data-hover-enter:after:transition-opacity"
1892
1883
  ],
1893
1884
  segmented: [
1894
- "inline-flex w-fit rounded-sm p-1 shadow-[inset_0_0_0_1px_var(--color-ring)]",
1895
- "before:top-0 before:h-(--tabs-indicator-h) before:rounded-sharp before:bg-fg/8",
1885
+ "inline-flex w-fit rounded-md p-1 shadow-[inset_0_0_0_1px_var(--color-ring)]",
1886
+ "before:top-0 before:h-(--tabs-indicator-h) before:rounded-sm before:bg-fg/8",
1896
1887
  "before:[translate:var(--tabs-indicator-x)_var(--tabs-indicator-y)]",
1897
1888
  "before:transition-[translate,width,height]",
1898
1889
  "forced-colors:before:border-2 forced-colors:before:border-[Highlight]"
@@ -1930,8 +1921,8 @@ const itemBase = [
1930
1921
  "forced-colors:aria-disabled:text-[GrayText] forced-colors:aria-disabled:opacity-100"
1931
1922
  ];
1932
1923
  const itemVariant = {
1933
- line: ["rounded-md data-selected:font-medium aria-[current=page]:font-medium"],
1934
- segmented: ["h-full rounded-sharp font-medium"]
1924
+ line: ["rounded-lg data-selected:font-medium aria-[current=page]:font-medium"],
1925
+ segmented: ["h-full rounded-sm font-medium"]
1935
1926
  };
1936
1927
  const itemSize = {
1937
1928
  line: {
@@ -2254,7 +2245,7 @@ function useHoverPill() {
2254
2245
  }
2255
2246
  const bodyClasses = [
2256
2247
  "relative isolate flex min-h-0 flex-1 flex-col gap-4 overflow-y-auto overscroll-contain px-3 py-2",
2257
- "after:pointer-events-none after:absolute after:top-0 after:left-0 after:-z-1 after:rounded-sm",
2248
+ "after:pointer-events-none after:absolute after:top-0 after:left-0 after:-z-1 after:rounded-md",
2258
2249
  "after:bg-fg/5 after:opacity-0 data-hover:after:opacity-100",
2259
2250
  "after:h-(--sidebar-hover-h) after:w-(--sidebar-hover-w)",
2260
2251
  "after:[translate:var(--sidebar-hover-x)_var(--sidebar-hover-y)]",
@@ -2309,7 +2300,7 @@ function SidebarGroup({ label, className, children, ...props }) {
2309
2300
  });
2310
2301
  }
2311
2302
  const itemClasses = [
2312
- "flex h-9 w-full items-center gap-2 rounded-sm ps-2.5 pe-2",
2303
+ "flex h-9 w-full items-center gap-2 rounded-md ps-2.5 pe-2",
2313
2304
  "text-sm leading-5 font-medium text-fg-muted no-underline select-none",
2314
2305
  "touch-manipulation [-webkit-tap-highlight-color:transparent]",
2315
2306
  "transition-[color,background-color] duration-150 ease-[cubic-bezier(0.4,0,0.2,1)]",
@@ -3402,7 +3393,7 @@ function ToastCard({ record, layout, total, expanded, paused, frontHeight, ownHe
3402
3393
  onPointerMove: handlePointerMove,
3403
3394
  onPointerUp: (event) => endDrag(event, false),
3404
3395
  onPointerCancel: (event) => endDrag(event, true),
3405
- className: cn("group/toast absolute inset-x-0 rounded-lg outline-none select-none", edge === "bottom" ? "bottom-0 origin-top" : "top-0 origin-bottom", "transform-[translateY(var(--toast-y))_scale(var(--toast-scale))]", "[translate:var(--toast-swipe-x,0px)_var(--toast-swipe-y,0px)]", "transition-[transform,translate,scale,opacity] duration-350 ease-[cubic-bezier(0.25,0.75,0.6,0.98)]", enterFrom[edge], "starting:opacity-0", "after:absolute after:inset-x-0 after:h-3.75", edge === "bottom" ? "after:bottom-full" : "after:top-full", "data-[visible=false]:pointer-events-none data-[visible=false]:opacity-0", "data-removed:pointer-events-none data-removed:scale-99 data-removed:opacity-0 data-removed:duration-150", "data-removed:data-swipe-out:duration-200 data-removed:data-swipe-out:ease-out", "data-[swipe-out=right]:[translate:calc(100%+24px)_0]", "data-[swipe-out=left]:[translate:calc(-100%-24px)_0]", "data-[swipe-out=bottom]:[translate:0_calc(100%+24px)]", "data-[swipe-out=top]:[translate:0_calc(-100%-24px)]", "data-swiping:transition-none", "group-data-moving/viewport:data-entered:transition-none", "focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-focus", touchAction),
3396
+ className: cn("group/toast absolute inset-x-0 rounded-xl outline-none select-none", edge === "bottom" ? "bottom-0 origin-top" : "top-0 origin-bottom", "transform-[translateY(var(--toast-y))_scale(var(--toast-scale))]", "[translate:var(--toast-swipe-x,0px)_var(--toast-swipe-y,0px)]", "transition-[transform,translate,scale,opacity] duration-350 ease-[cubic-bezier(0.25,0.75,0.6,0.98)]", enterFrom[edge], "starting:opacity-0", "after:absolute after:inset-x-0 after:h-3.75", edge === "bottom" ? "after:bottom-full" : "after:top-full", "data-[visible=false]:pointer-events-none data-[visible=false]:opacity-0", "data-removed:pointer-events-none data-removed:scale-99 data-removed:opacity-0 data-removed:duration-150", "data-removed:data-swipe-out:duration-200 data-removed:data-swipe-out:ease-out", "data-[swipe-out=right]:[translate:calc(100%+24px)_0]", "data-[swipe-out=left]:[translate:calc(-100%-24px)_0]", "data-[swipe-out=bottom]:[translate:0_calc(100%+24px)]", "data-[swipe-out=top]:[translate:0_calc(-100%-24px)]", "data-swiping:transition-none", "group-data-moving/viewport:data-entered:transition-none", "focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-focus", touchAction),
3406
3397
  children: /* @__PURE__ */ jsx("div", {
3407
3398
  "data-slot": "toast-card",
3408
3399
  style: { height },
package/dist/theme.css CHANGED
@@ -1,267 +1,145 @@
1
1
  /*
2
- * swell-ui design tokens, dark theme and base styles.
2
+ * swell-ui design tokens and base styles. Neutral grays; brand color is one knob (--color-primary).
3
3
  * Import right after tailwindcss. Contains no utilities and no @source:
4
4
  * use styles.css to also have Tailwind scan the components.
5
+ *
6
+ * Every color is written once as light-dark(<light>, <dark>). Which side applies follows
7
+ * `color-scheme` on the root: the system by default, forced by data-theme="light" / "dark".
8
+ * Tailwind's build polyfills light-dark() for browsers that lack it.
5
9
  */
6
10
 
7
- /* Tokens. Light values live on bare :root (and @theme). */
8
11
  @theme {
9
- --color-bg: #f5f4ed; /* Parchment */
10
- --color-surface: #faf9f5; /* Ivory */
11
- --color-surface-raised: #ffffff; /* Pure White */
12
- --color-surface-muted: #e8e6dc; /* Warm Sand */
13
- --color-fg: #141413; /* Near Black */
14
- --color-fg-strong: #4d4c48; /* Charcoal Warm */
15
- --color-fg-muted: #5e5d59; /* Olive Gray */
16
- --color-fg-subtle: #686761; /* Warm gray: >=4.5:1 even on surface-muted */
17
- --color-border: #f0eee6; /* Border Cream */
18
- --color-border-strong: #e8e6dc; /* Border Warm */
19
- --color-ring: #d1cfc5; /* Ring Warm */
20
- --color-ring-deep: #c2c0b6; /* Ring Deep */
21
- --color-primary: #8b5e3c; /* Warm Brown */
22
- --color-primary-fg: #faf9f5;
23
- --color-accent: #c49a6c; /* Warm Tan */
24
- --color-inverse: #30302e; /* Dark Surface */
25
- --color-inverse-fg: #faf9f5;
26
- --color-danger: #b53333; /* Error Crimson */
27
- --color-danger-fg: #faf9f5;
12
+ /*
13
+ * Brand. The only two tokens to override for a rebrand: input focus, checked controls and
14
+ * their halos derive from these at runtime (plain var() references, no baked copies).
15
+ * Default is neutral: near-black on light, near-white on dark.
16
+ */
17
+ --color-primary: light-dark(#141414, #f9f9f9);
18
+ --color-primary-fg: light-dark(#f9f9f9, #141414);
19
+ /* Secondary brand color; no component uses it, so it just follows primary unless set */
20
+ --color-accent: var(--color-primary);
21
+
22
+ /* Surfaces and text: pure grays, contrast-checked (fg-subtle >=4.5:1 on every surface) */
23
+ --color-bg: light-dark(#f4f4f4, #141414);
24
+ --color-surface: light-dark(#f9f9f9, #303030);
25
+ --color-surface-raised: light-dark(#ffffff, #303030);
26
+ --color-surface-muted: light-dark(#e6e6e6, #262626); /* dark: sunk below surface */
27
+ --color-fg: light-dark(#141414, #f9f9f9);
28
+ --color-fg-strong: light-dark(#4c4c4c, #aeaeae);
29
+ --color-fg-muted: light-dark(#5d5d5d, #aeaeae);
30
+ --color-fg-subtle: light-dark(#676767, #aaaaaa);
31
+ --color-border: light-dark(#eeeeee, #383838); /* row dividers */
32
+ --color-border-strong: light-dark(#e6e6e6, #3d3d3d);
33
+ --color-ring: light-dark(#cfcfcf, #3d3d3d);
34
+ --color-ring-deep: light-dark(#c0c0c0, #4c4c4c);
35
+ --color-inverse: light-dark(#303030, #141414);
36
+ --color-inverse-fg: light-dark(#f9f9f9, #aeaeae);
37
+ /* Light red + dark text on dark: one danger token can't pass 4.5:1 both as fill and as text */
38
+ --color-danger: light-dark(#b53333, #ee8a80);
39
+ --color-danger-fg: light-dark(#f9f9f9, #141414);
28
40
  --color-focus: #3898ec; /* Focus Blue */
41
+
29
42
  /*
30
- * Text fields: a slightly sunk solid fill.
31
- * Borders are fg mixed over bg in oklab, kept >=3:1 against both the page and the fill (WCAG 1.4.11).
32
- * Focus is neutral, not blue: border steps up to near-fg plus a soft halo.
43
+ * Text fields: a slightly sunk solid fill (fg 4% over bg in oklab, light; white 6%, dark).
44
+ * Borders are kept >=3:1 against both the page and the fill (WCAG 1.4.11).
45
+ * Hover stacks cues (stronger border, lifted fill, faint drop shadow): a border step alone is too subtle.
33
46
  */
34
- /* Fill = fg 4% over bg in oklab, baked to hex: a color-mix token gets a wrong static fallback per theme */
35
- --color-input-bg: #ebeae3;
36
- --color-input-border: #7f7e7b;
37
- --color-input-placeholder: #6a6964;
38
- /* Hover stacks cues (stronger border, lifted fill, faint drop shadow): a border step alone is too subtle */
39
- --color-input-border-hover: #4d4c48;
40
- --color-input-bg-hover: #f3f2ec;
41
- --color-input-focus: #8b5e3c; /* Warm Brown (primary) */
42
- --color-input-highlight: rgb(0 0 0 / 0.03);
47
+ --color-input-bg: light-dark(#eaeaea, #1f1f1f);
48
+ --color-input-border: light-dark(#7e7e7e, #818181);
49
+ --color-input-placeholder: light-dark(#696969, #989898);
50
+ --color-input-border-hover: light-dark(#4c4c4c, #aeaeae);
51
+ --color-input-bg-hover: light-dark(#f2f2f2, #262626);
52
+ --color-input-focus: var(--color-primary);
53
+ --color-input-highlight: light-dark(rgb(0 0 0 / 0.03), rgb(255 255 255 / 0.04));
43
54
  --shadow-input: inset 0 1px 0 var(--color-input-highlight);
44
- --shadow-input-lift: 0 1px 2px rgb(0 0 0 / 0.06);
55
+ /* A drop shadow doesn't read on near-black, so it fades out on dark */
56
+ --shadow-input-lift: 0 1px 2px light-dark(rgb(0 0 0 / 0.06), transparent);
45
57
  /* Focus/error halos: decorative 3px rings outside the border */
46
58
  --color-input-halo: color-mix(in oklab, var(--color-input-focus) 14%, transparent);
47
59
  --color-input-halo-invalid: color-mix(in oklab, var(--color-danger) 12%, transparent);
48
60
  --color-input-halo-invalid-focus: color-mix(in oklab, var(--color-danger) 22%, transparent);
61
+
49
62
  /*
50
63
  * Selection controls (Switch, Checkbox): the checked fill and the mark drawn on it (thumb, check, dash).
51
64
  * The off track/box reuses the input border, which is already >=3:1 on the page and the fill.
52
65
  */
53
- --color-control-checked: #8b5e3c; /* Warm Brown (primary) */
54
- --color-control-checked-fg: #ffffff; /* 5.58:1 on the fill */
55
- /* Thumb: a hairline plus a soft drop straight down. Geist's sideways offset made the small thumb
56
- * look shifted, so the shadow stays horizontally symmetric. */
66
+ --color-control-checked: var(--color-primary);
67
+ --color-control-checked-fg: var(--color-primary-fg);
68
+ /* Thumb: a hairline plus a soft drop straight down (a sideways offset makes the small thumb look shifted) */
57
69
  --shadow-switch-thumb: 0 0 0 0.5px rgb(0 0 0 / 0.08), 0 1px 2px rgb(0 0 0 / 0.2);
70
+
58
71
  /*
59
- * Popovers (the Select picker): raised above the page with a warm hairline and a soft shadow.
72
+ * Popovers (the Select picker): raised above the page with a hairline and a soft shadow.
60
73
  * Highlight is the hovered/keyboard-active row; same fill for both so they never disagree.
74
+ * Dark shadows are darker and denser: the surface itself already separates from near-black.
61
75
  */
62
- --color-popover-bg: #ffffff; /* Pure White */
63
- --color-popover-border: #e0ded4; /* between Border Warm and Ring Warm: Border Warm vanishes on white */
64
- --color-popover-highlight: #efede5;
65
- --shadow-popover: 0 8px 24px rgb(0 0 0 / 0.08), 0 1px 3px rgb(0 0 0 / 0.06);
76
+ --color-popover-bg: light-dark(#ffffff, #303030);
77
+ --color-popover-border: light-dark(#dedede, #3d3d3d);
78
+ --color-popover-highlight: light-dark(#ededed, #3d3d3d);
79
+ --shadow-popover:
80
+ 0 8px 24px light-dark(rgb(0 0 0 / 0.08), rgb(0 0 0 / 0.4)),
81
+ 0 1px 3px light-dark(rgb(0 0 0 / 0.06), rgb(0 0 0 / 0.3));
66
82
  /*
67
- * Dialogs: same surface and hairline as popovers, one elevation step higher (a wider, softer
68
- * drop) since they sit over a dimmed page. Backdrop is warm near-black, never blurred.
83
+ * Dialogs: same surface and hairline as popovers, one elevation step higher. The backdrop is
84
+ * never blurred; it is darker on dark because a 40% dim barely reads over a near-black page.
69
85
  */
70
- --shadow-dialog: 0 16px 48px rgb(0 0 0 / 0.12), 0 2px 6px rgb(0 0 0 / 0.06);
71
- --color-dialog-backdrop: rgb(20 20 19 / 0.4);
86
+ --shadow-dialog:
87
+ 0 16px 48px light-dark(rgb(0 0 0 / 0.12), rgb(0 0 0 / 0.5)),
88
+ 0 2px 6px light-dark(rgb(0 0 0 / 0.06), rgb(0 0 0 / 0.3));
89
+ --color-dialog-backdrop: light-dark(rgb(20 20 20 / 0.4), rgb(0 0 0 / 0.6));
90
+
72
91
  /*
73
- * Status tones (Badge, later Dialog/Toast/table state). Per tone: main = solid fill / standalone icon,
92
+ * Status tones (Badge, Toast, ...). Per tone: main = solid fill / standalone icon,
74
93
  * -fg = text on a solid fill, -subtle-bg/-subtle-fg = tinted badge, -border = its hairline.
75
- * subtle-bg is main mixed over bg in oklab (14% light / 16% dark), baked to hex; subtle-fg is main
76
- * pushed along lightness until >=5:1 on it and >=4.5:1 on the translucent equivalent over any surface.
77
- * Meaning is carried by text + icon; hue/lightness are spaced so success vs danger also survives red-green CVD.
78
- * danger main/-fg already exist above; only its subtle-* are new.
94
+ * subtle-bg is main mixed over bg in oklab (14% light / 16% dark), border likewise (35% / 40%),
95
+ * baked to hex; subtle-fg is >=5:1 on subtle-bg. Meaning is carried by text + icon; hue and
96
+ * lightness are spaced so success vs danger also survives red-green CVD.
79
97
  */
80
- /* success: olive, hue 125; deepest tone so it separates from danger by lightness for red-green CVD */
81
- --color-success: #394d00;
82
- --color-success-fg: #faf9f5;
83
- --color-success-subtle-bg: #d8dbcc;
84
- --color-success-subtle-fg: #3a4c0e;
85
- --color-success-border: #aeb79b;
86
- /* warning: ochre/amber, hue 78; lighter than danger; dark solid-fg because ivory on amber fails 4.5:1 */
87
- --color-warning: #a5761d;
88
- --color-warning-fg: #141413;
89
- --color-warning-subtle-bg: #eae2d1;
90
- --color-warning-subtle-fg: #7e5600;
91
- --color-warning-border: #d9c7a9;
92
- /* danger: existing Error Crimson (unchanged) */
93
- --color-danger-subtle-bg: #f0dad2;
94
- --color-danger-subtle-fg: #a2302f;
95
- --color-danger-border: #e5b3aa;
96
- /* info: dusty teal-blue, hue 230, chroma 0.065 (muted; the only cool-leaning tone besides Focus Blue) */
97
- --color-info: #3e7087;
98
- --color-info-fg: #faf9f5;
99
- --color-info-subtle-bg: #dbe1de;
100
- --color-info-subtle-fg: #356075;
101
- --color-info-border: #b4c4c9;
102
- /* neutral: warm gray (Olive Gray family) */
103
- --color-neutral: #66635c;
104
- --color-neutral-fg: #faf9f5;
105
- --color-neutral-subtle-bg: #e0ded7;
106
- --color-neutral-subtle-fg: #5e5b55;
107
- --color-neutral-border: #c0bfb8;
108
-
109
- --font-sans: "Anthropic Sans", system-ui, Inter, Arial, sans-serif;
110
- --font-serif: "Anthropic Serif", Georgia, serif;
111
- --font-mono: "Anthropic Mono", ui-monospace, Menlo, monospace;
112
-
113
- /* Radius scale */
114
- --radius-sharp: 4px;
115
- --radius-sm: 6px;
116
- --radius-md: 8px;
117
- --radius-lg: 12px;
118
- --radius-xl: 16px;
119
- --radius-2xl: 24px;
120
- --radius-3xl: 32px;
121
-
98
+ /* success: olive; deepest tone so it separates from danger by lightness */
99
+ --color-success: light-dark(#394d00, #758e48);
100
+ --color-success-fg: light-dark(#f9f9f9, #141414);
101
+ --color-success-subtle-bg: light-dark(#d7dbd2, #22251d);
102
+ --color-success-subtle-fg: light-dark(#3a4c0e, #98b070);
103
+ --color-success-border: light-dark(#adb79f, #384129);
104
+ /* warning: amber; dark solid-fg because light text on amber fails 4.5:1 */
105
+ --color-warning: light-dark(#a5761d, #e6b463);
106
+ --color-warning-fg: #141414;
107
+ --color-warning-subtle-bg: light-dark(#e9e2d8, #302a21);
108
+ --color-warning-subtle-fg: light-dark(#7e5600, #e2b56e);
109
+ --color-warning-border: light-dark(#d9c7ad, #5f4e34);
110
+ /* danger: main/-fg are above */
111
+ --color-danger-subtle-bg: light-dark(#efdad8, #322523);
112
+ --color-danger-subtle-fg: light-dark(#a2302f, #f0958c);
113
+ --color-danger-border: light-dark(#e5b3ae, #63403c);
114
+ /* info: muted teal-blue */
115
+ --color-info: light-dark(#3e7087, #74a6bf);
116
+ --color-info-fg: light-dark(#f9f9f9, #141414);
117
+ --color-info-subtle-bg: light-dark(#dae1e4, #22282b);
118
+ --color-info-subtle-fg: light-dark(#356075, #87b4cb);
119
+ --color-info-border: light-dark(#b3c4cd, #384952);
120
+ /* neutral: gray */
121
+ --color-neutral: light-dark(#636363, #aeaeae);
122
+ --color-neutral-fg: light-dark(#f9f9f9, #141414);
123
+ --color-neutral-subtle-bg: light-dark(#dedede, #292929);
124
+ --color-neutral-subtle-fg: light-dark(#5b5b5b, #b2b2b2);
125
+ --color-neutral-border: light-dark(#bfbfbf, #4c4c4c);
126
+
127
+ /* Fonts and radii are Tailwind's defaults; see the README to change them */
122
128
  --shadow-whisper: rgba(0, 0, 0, 0.05) 0 4px 24px;
123
129
  --container-page: 1200px;
124
130
  }
125
131
 
132
+ /* Follow the system unless data-theme forces one; light-dark() above reads this */
126
133
  :root {
127
- color-scheme: light;
134
+ color-scheme: light dark;
128
135
  }
129
136
 
130
- /* Dark values */
131
- :root:not([data-theme="light"]) {
132
- @media (prefers-color-scheme: dark) {
133
- color-scheme: dark;
134
- --color-bg: #141413;
135
- --color-surface: #30302e;
136
- --color-surface-raised: #30302e;
137
- --color-surface-muted: #262625; /* sunk below Dark Surface: header rows, hover fills */
138
- --color-fg: #faf9f5;
139
- --color-fg-strong: #b0aea5;
140
- --color-fg-muted: #b0aea5;
141
- --color-fg-subtle: #a8a79e; /* >=4.5:1 on raised and highlighted surfaces */
142
- --color-border: #383836; /* between Dark Surface and Dark Warm: row dividers */
143
- --color-border-strong: #3d3d3a; /* Dark Warm */
144
- --color-ring: #3d3d3a; /* Dark Warm */
145
- --color-ring-deep: #4d4c48; /* Charcoal Warm */
146
- --color-inverse: #141413;
147
- --color-inverse-fg: #b0aea5;
148
- --color-danger: #ee8a80;
149
- --color-danger-fg: #141413;
150
- --color-input-bg: #1f1f1e; /* white 6% over bg in oklab */
151
- --color-input-border: #81817e;
152
- --color-input-placeholder: #9a998f;
153
- --color-input-border-hover: #b0aea5;
154
- --color-input-bg-hover: #262625;
155
- --color-input-focus: #c49a6c; /* Warm Tan: primary is too dark on dark surfaces */
156
- --color-control-checked: #c49a6c; /* Warm Tan, as the focus color */
157
- --color-control-checked-fg: #141413; /* white on tan is only ~2.6:1; near-black is 7.2:1 */
158
- --color-input-highlight: rgb(255 255 255 / 0.04);
159
- --shadow-input-lift: 0 0 #0000; /* a drop shadow doesn't read on near-black */
160
- --color-popover-bg: #30302e; /* Dark Surface */
161
- --color-popover-border: #3d3d3a; /* Dark Warm */
162
- --color-popover-highlight: #3d3d3a;
163
- /* Darker and denser than light: the surface itself already separates from near-black */
164
- --shadow-popover: 0 8px 24px rgb(0 0 0 / 0.4), 0 1px 3px rgb(0 0 0 / 0.3);
165
- --shadow-dialog: 0 16px 48px rgb(0 0 0 / 0.5), 0 2px 6px rgb(0 0 0 / 0.3);
166
- /* Darker than light: a 40% dim barely reads over a near-black page */
167
- --color-dialog-backdrop: rgb(0 0 0 / 0.6);
168
- /* Status tones: lightened for dark surfaces, solid-fg flips to near-black */
169
- /* success: olive, lightened for dark; kept darker than danger for CVD */
170
- --color-success: #758e48;
171
- --color-success-fg: #141413;
172
- --color-success-subtle-bg: #22251c;
173
- --color-success-subtle-fg: #98b070;
174
- --color-success-border: #384128;
175
- /* warning: amber */
176
- --color-warning: #e6b463;
177
- --color-warning-fg: #141413;
178
- --color-warning-subtle-bg: #302a20;
179
- --color-warning-subtle-fg: #e2b56e;
180
- --color-warning-border: #5f4e33;
181
- /* danger: existing (unchanged) */
182
- --color-danger-subtle-bg: #322522;
183
- --color-danger-subtle-fg: #f0958c;
184
- --color-danger-border: #63403b;
185
- /* info: dusty teal-blue */
186
- --color-info: #74a6bf;
187
- --color-info-fg: #141413;
188
- --color-info-subtle-bg: #22282a;
189
- --color-info-subtle-fg: #87b4cb;
190
- --color-info-border: #384951;
191
- /* neutral: Warm Silver */
192
- --color-neutral: #b0aea5;
193
- --color-neutral-fg: #141413;
194
- --color-neutral-subtle-bg: #292927;
195
- --color-neutral-subtle-fg: #b4b2aa;
196
- --color-neutral-border: #4c4c48;
197
- }
137
+ :root[data-theme="light"] {
138
+ color-scheme: light;
198
139
  }
199
140
 
200
141
  :root[data-theme="dark"] {
201
142
  color-scheme: dark;
202
- --color-bg: #141413;
203
- --color-surface: #30302e;
204
- --color-surface-raised: #30302e;
205
- --color-surface-muted: #262625; /* sunk below Dark Surface: header rows, hover fills */
206
- --color-fg: #faf9f5;
207
- --color-fg-strong: #b0aea5;
208
- --color-fg-muted: #b0aea5;
209
- --color-fg-subtle: #a8a79e; /* >=4.5:1 on raised and highlighted surfaces */
210
- --color-border: #383836; /* between Dark Surface and Dark Warm: row dividers */
211
- --color-border-strong: #3d3d3a; /* Dark Warm */
212
- --color-ring: #3d3d3a; /* Dark Warm */
213
- --color-ring-deep: #4d4c48; /* Charcoal Warm */
214
- --color-inverse: #141413;
215
- --color-inverse-fg: #b0aea5;
216
- /* Light red + dark text: a single danger token can't pass 4.5:1 both as fill and as text on dark */
217
- --color-danger: #ee8a80;
218
- --color-danger-fg: #141413;
219
- --color-input-bg: #1f1f1e; /* white 6% over bg in oklab */
220
- --color-input-border: #81817e;
221
- --color-input-placeholder: #9a998f;
222
- --color-input-border-hover: #b0aea5;
223
- --color-input-bg-hover: #262625;
224
- --color-input-focus: #c49a6c; /* Warm Tan: primary is too dark on dark surfaces */
225
- --color-control-checked: #c49a6c; /* Warm Tan, as the focus color */
226
- --color-control-checked-fg: #141413; /* white on tan is only ~2.6:1; near-black is 7.2:1 */
227
- --color-input-highlight: rgb(255 255 255 / 0.04);
228
- --shadow-input-lift: 0 0 #0000; /* a drop shadow doesn't read on near-black */
229
- --color-popover-bg: #30302e; /* Dark Surface */
230
- --color-popover-border: #3d3d3a; /* Dark Warm */
231
- --color-popover-highlight: #3d3d3a;
232
- /* Darker and denser than light: the surface itself already separates from near-black */
233
- --shadow-popover: 0 8px 24px rgb(0 0 0 / 0.4), 0 1px 3px rgb(0 0 0 / 0.3);
234
- --shadow-dialog: 0 16px 48px rgb(0 0 0 / 0.5), 0 2px 6px rgb(0 0 0 / 0.3);
235
- --color-dialog-backdrop: rgb(0 0 0 / 0.6);
236
- /* Status tones: lightened for dark surfaces, solid-fg flips to near-black */
237
- /* success: olive, lightened for dark; kept darker than danger for CVD */
238
- --color-success: #758e48;
239
- --color-success-fg: #141413;
240
- --color-success-subtle-bg: #22251c;
241
- --color-success-subtle-fg: #98b070;
242
- --color-success-border: #384128;
243
- /* warning: amber */
244
- --color-warning: #e6b463;
245
- --color-warning-fg: #141413;
246
- --color-warning-subtle-bg: #302a20;
247
- --color-warning-subtle-fg: #e2b56e;
248
- --color-warning-border: #5f4e33;
249
- /* danger: existing (unchanged) */
250
- --color-danger-subtle-bg: #322522;
251
- --color-danger-subtle-fg: #f0958c;
252
- --color-danger-border: #63403b;
253
- /* info: dusty teal-blue */
254
- --color-info: #74a6bf;
255
- --color-info-fg: #141413;
256
- --color-info-subtle-bg: #22282a;
257
- --color-info-subtle-fg: #87b4cb;
258
- --color-info-border: #384951;
259
- /* neutral: Warm Silver */
260
- --color-neutral: #b0aea5;
261
- --color-neutral-fg: #141413;
262
- --color-neutral-subtle-bg: #292927;
263
- --color-neutral-subtle-fg: #b4b2aa;
264
- --color-neutral-border: #4c4c48;
265
143
  }
266
144
 
267
145
  /* `dark:` follows the tokens: data-theme="dark" on the root, or the system when not forced light */
@@ -305,7 +183,7 @@
305
183
  }
306
184
  }
307
185
 
308
- /* Layered so the app's own utilities (e.g. a body background, `font-sans` on a title) win */
186
+ /* Layered so the app's own utilities (e.g. a body background) win */
309
187
  @layer base {
310
188
  body {
311
189
  margin: 0;
@@ -314,14 +192,6 @@
314
192
  font-family: var(--font-sans);
315
193
  line-height: 1.6;
316
194
  }
317
-
318
- /* Dialog and Drawer titles rely on this */
319
- h1,
320
- h2,
321
- h3 {
322
- font-family: var(--font-serif);
323
- font-weight: 500;
324
- }
325
195
  }
326
196
 
327
197
  /* Unlayered: beats Tailwind's layered utilities (e.g. `transition-*`) on cascade order alone */
@@ -0,0 +1,63 @@
1
+ /*
2
+ * Warm theme: parchment surfaces, warm grays and a brown primary (swell-ui's original look).
3
+ * Import after swell-ui's styles.css (or theme.css); add typography.css for the serif headings.
4
+ * Overrides the color tokens only; input focus and checked controls still derive from primary.
5
+ */
6
+ @theme {
7
+ /* Warm Brown; Warm Tan on dark, where the brown is too dark against the page */
8
+ --color-primary: light-dark(#8b5e3c, #c49a6c);
9
+ --color-primary-fg: light-dark(#faf9f5, #141413);
10
+ --color-accent: #c49a6c; /* Warm Tan */
11
+
12
+ --color-bg: light-dark(#f5f4ed, #141413); /* Parchment */
13
+ --color-surface: light-dark(#faf9f5, #30302e); /* Ivory / Dark Surface */
14
+ --color-surface-raised: light-dark(#ffffff, #30302e);
15
+ --color-surface-muted: light-dark(#e8e6dc, #262625); /* Warm Sand */
16
+ --color-fg: light-dark(#141413, #faf9f5);
17
+ --color-fg-strong: light-dark(#4d4c48, #b0aea5);
18
+ --color-fg-muted: light-dark(#5e5d59, #b0aea5);
19
+ --color-fg-subtle: light-dark(#686761, #a8a79e);
20
+ --color-border: light-dark(#f0eee6, #383836);
21
+ --color-border-strong: light-dark(#e8e6dc, #3d3d3a);
22
+ --color-ring: light-dark(#d1cfc5, #3d3d3a);
23
+ --color-ring-deep: light-dark(#c2c0b6, #4d4c48);
24
+ --color-inverse: light-dark(#30302e, #141413);
25
+ --color-inverse-fg: light-dark(#faf9f5, #b0aea5);
26
+ --color-danger: light-dark(#b53333, #ee8a80); /* Error Crimson */
27
+ --color-danger-fg: light-dark(#faf9f5, #141413);
28
+
29
+ --color-input-bg: light-dark(#ebeae3, #1f1f1e);
30
+ --color-input-border: light-dark(#7f7e7b, #81817e);
31
+ --color-input-placeholder: light-dark(#6a6964, #9a998f);
32
+ --color-input-border-hover: light-dark(#4d4c48, #b0aea5);
33
+ --color-input-bg-hover: light-dark(#f3f2ec, #262625);
34
+
35
+ --color-popover-bg: light-dark(#ffffff, #30302e);
36
+ --color-popover-border: light-dark(#e0ded4, #3d3d3a);
37
+ --color-popover-highlight: light-dark(#efede5, #3d3d3a);
38
+ --color-dialog-backdrop: light-dark(rgb(20 20 19 / 0.4), rgb(0 0 0 / 0.6));
39
+
40
+ --color-success: light-dark(#394d00, #758e48);
41
+ --color-success-fg: light-dark(#faf9f5, #141413);
42
+ --color-success-subtle-bg: light-dark(#d8dbcc, #22251c);
43
+ --color-success-subtle-fg: light-dark(#3a4c0e, #98b070);
44
+ --color-success-border: light-dark(#aeb79b, #384128);
45
+ --color-warning: light-dark(#a5761d, #e6b463);
46
+ --color-warning-fg: #141413;
47
+ --color-warning-subtle-bg: light-dark(#eae2d1, #302a20);
48
+ --color-warning-subtle-fg: light-dark(#7e5600, #e2b56e);
49
+ --color-warning-border: light-dark(#d9c7a9, #5f4e33);
50
+ --color-danger-subtle-bg: light-dark(#f0dad2, #322522);
51
+ --color-danger-subtle-fg: light-dark(#a2302f, #f0958c);
52
+ --color-danger-border: light-dark(#e5b3aa, #63403b);
53
+ --color-info: light-dark(#3e7087, #74a6bf);
54
+ --color-info-fg: light-dark(#faf9f5, #141413);
55
+ --color-info-subtle-bg: light-dark(#dbe1de, #22282a);
56
+ --color-info-subtle-fg: light-dark(#356075, #87b4cb);
57
+ --color-info-border: light-dark(#b4c4c9, #384951);
58
+ --color-neutral: light-dark(#66635c, #b0aea5);
59
+ --color-neutral-fg: light-dark(#faf9f5, #141413);
60
+ --color-neutral-subtle-bg: light-dark(#e0ded7, #292927);
61
+ --color-neutral-subtle-fg: light-dark(#5e5b55, #b4b2aa);
62
+ --color-neutral-border: light-dark(#c0bfb8, #4c4c48);
63
+ }
@@ -0,0 +1,12 @@
1
+ /*
2
+ * Opt-in editorial headings: h1-h3 in the serif stack at weight 500.
3
+ * Import after swell-ui's styles.css (or theme.css). Layered, so utilities on a heading still win.
4
+ */
5
+ @layer base {
6
+ h1,
7
+ h2,
8
+ h3 {
9
+ font-family: var(--font-serif);
10
+ font-weight: 500;
11
+ }
12
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@weichenlin/swell-ui",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Hand-built React components styled with Tailwind CSS v4.",
5
5
  "files": [
6
6
  "dist"
@@ -16,6 +16,8 @@
16
16
  },
17
17
  "./theme.css": "./dist/theme.css",
18
18
  "./styles.css": "./dist/styles.css",
19
+ "./typography.css": "./dist/typography.css",
20
+ "./themes/warm.css": "./dist/themes/warm.css",
19
21
  "./package.json": "./package.json"
20
22
  },
21
23
  "publishConfig": {