@warmio/design 1.4.0 → 1.5.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
@@ -1,25 +1,35 @@
1
1
  # @warmio/design
2
2
 
3
- Warm's public, CSS-only design contract. It contains the shared theme, base
4
- styles, and pinned local font assets used by Warm products.
3
+ Warm's public design contract: CSS tokens (fonts, colors, spacing, radii, shadows, motion,
4
+ reduced-motion, marketing surfaces + dot patterns) and Button/Logo primitives.
5
5
 
6
6
  ```css
7
7
  @import '@warmio/design/globals.css';
8
8
  ```
9
9
 
10
+ Import primitives directly:
11
+
12
+ ```ts
13
+ import { Button, ButtonLink } from '@warmio/design/button';
14
+ import { Logo, WarmLogoIcon } from '@warmio/design/logo';
15
+ ```
16
+
10
17
  The default product typefaces are Geist and Geist Mono. The `font-editorial`
11
18
  utility uses Inter and should be reserved for deliberate editorial accents.
12
19
 
13
20
  `--accent-solid` / `--accent-solid-text` are the chromatic accent for links,
14
21
  badges, and highlights. They derive from `--brand-accent-solid` in `themes.css`.
15
22
 
16
- This package has no JavaScript entry point or runtime dependencies.
23
+ Package ships `components.css` (token-only styles for the primitives), `assets/`,
24
+ and `dist/` (ESM + .d.ts). `dependencies` and `devDependencies` are empty.
25
+ `peerDependencies.react` is `^19.0.0`. No root JS export.
17
26
 
18
27
  ## Releases
19
28
 
20
- The `1.0.0` cutover is done. Bump this `package.json` version whenever fonts, colors,
21
- spacing, or theme tokens in this folder change, then follow
22
- [docs/reference/DESIGN_PACKAGE.md](../../docs/reference/DESIGN_PACKAGE.md).
29
+ Bump this `package.json` version for additive tokens (incl. motion/surface), fonts,
30
+ spacing, or primitive changes. Follow the exact sequence in
31
+ [docs/reference/DESIGN_PACKAGE.md](../../docs/reference/DESIGN_PACKAGE.md)
32
+ including `build`, inventory refresh, pack:check, and dual publish readbacks.
23
33
 
24
34
  App uses the workspace package immediately. Chat and Warm Static stay on the last
25
- published npm version until that release is bumped the same day.
35
+ published npm version.
@@ -0,0 +1,8 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="27" height="15" viewBox="0 0 27 15">
2
+ <defs>
3
+ <pattern id="dots" width="3" height="3" patternUnits="userSpaceOnUse">
4
+ <rect width="1" height="1" fill="#fff" fill-opacity=".14" />
5
+ </pattern>
6
+ </defs>
7
+ <rect width="27" height="15" fill="url(#dots)" />
8
+ </svg>
Binary file
package/components.css ADDED
@@ -0,0 +1,495 @@
1
+ /* @warmio/design components.css - token-only, parity with prior Button/Logo contract.
2
+ * Rebuilt from exact UIWarm primitive/button.variants + presentation maps + variant overrides + button-edge recipes.
3
+ * Uses only design tokens + data attrs. No literals where tokens exist.
4
+ * Motion via --motion-* (aliased); reduced-motion zeros non-essentials.
5
+ * All 16 presentations, 6 variants, 3 sizes + full + states produce matching computed geometry/styles.
6
+ */
7
+
8
+ [data-warm-button] {
9
+ /* Strip native button chrome when this stylesheet is used without preflight.
10
+ * `appearance` is deliberately left at its UA default (matches the
11
+ * pre-migration PrimitiveButton/buttonVariants recipe, which never applied
12
+ * `appearance-none`): every other property below already fully overrides
13
+ * native border/background/padding rendering, and resetting `appearance`
14
+ * here would diverge from the origin recipe's actual computed style. */
15
+ border: 0 solid;
16
+ background: transparent;
17
+ background-clip: padding-box;
18
+ padding: 0;
19
+ margin: 0;
20
+ font: inherit;
21
+ color: inherit;
22
+ display: inline-flex;
23
+ align-items: center;
24
+ justify-content: center;
25
+ border-radius: var(--radius-full);
26
+ font-size: var(--button-font-size);
27
+ font-weight: 400;
28
+ line-height: var(--button-line-height);
29
+ white-space: nowrap;
30
+ transition-property: var(--button-transition-property);
31
+ /* Exact legacy duration (200ms), derived from the locked motion scale
32
+ (fast=100ms) rather than a literal: fast x 2 = 200ms exactly, and it
33
+ zeroes correctly under reduced motion since `--motion-duration-fast`
34
+ itself is zeroed there (see the reduced-motion block below). */
35
+ transition-duration: calc(var(--motion-duration-fast) * 2);
36
+ transition-timing-function: ease-out;
37
+ outline: none;
38
+ user-select: none;
39
+ box-sizing: border-box;
40
+ flex-shrink: 0;
41
+ /* border handled by variant shadows/tokens; no default regression border */
42
+ }
43
+
44
+
45
+ [data-warm-button]:focus-visible {
46
+ box-shadow: 0 0 0 var(--button-focus-ring) color-mix(in oklab, var(--ring) var(--button-mix-50), transparent);
47
+ }
48
+
49
+ [data-warm-button][data-width="full"] {
50
+ width: var(--button-width-full);
51
+ }
52
+
53
+ /* Active scale + full motion reduce (legacy: active:not-aria-[haspopup]:scale-[0.97] motion-reduce:transition-none motion-reduce:active:scale-100) */
54
+ [data-warm-button]:active:not([aria-haspopup]) {
55
+ transform: scale(0.97);
56
+ }
57
+
58
+ @media (prefers-reduced-motion: reduce) {
59
+ [data-warm-button] {
60
+ transition: none;
61
+ }
62
+ [data-warm-button]:active:not([aria-haspopup]) {
63
+ transform: none;
64
+ }
65
+ }
66
+
67
+ /* Variants (from cva + overrides + edge; tokens for color/shadow/radius) */
68
+ [data-warm-button][data-variant="default"] {
69
+ background-color: var(--button-primary-bg);
70
+ background-image: var(--button-primary-gradient);
71
+ color: var(--button-primary-foreground);
72
+ box-shadow: var(--button-primary-shadow);
73
+ /* no explicit border to avoid regression; shadow provides chrome */
74
+ }
75
+ [data-warm-button][data-variant="default"]:hover {
76
+ background-color: var(--button-primary-hover);
77
+ background-image: var(--button-primary-gradient-hover);
78
+ }
79
+ [data-warm-button][data-variant="default"]:active {
80
+ background-color: var(--button-primary-active);
81
+ }
82
+
83
+ [data-warm-button][data-variant="outline"] {
84
+ background-color: var(--background);
85
+ box-shadow: var(--button-outline-shadow);
86
+ border-style: none;
87
+ }
88
+ [data-warm-button][data-variant="outline"]:hover {
89
+ background-color: var(--muted);
90
+ color: var(--foreground);
91
+ }
92
+ .dark [data-warm-button][data-variant="outline"] {
93
+ background-color: var(--warm-surface-raised-dark, var(--input));
94
+ box-shadow: var(--button-outline-shadow-inset);
95
+ }
96
+ .dark [data-warm-button][data-variant="outline"]:hover {
97
+ background-color: var(--warm-surface-raised-hover-dark, var(--muted));
98
+ }
99
+ [data-warm-button][data-variant="outline"]:disabled {
100
+ opacity: 1;
101
+ background-color: var(--muted);
102
+ color: var(--muted-foreground);
103
+ box-shadow: var(--button-outline-shadow);
104
+ }
105
+ .dark [data-warm-button][data-variant="outline"]:disabled {
106
+ background-color: color-mix(in oklab, var(--input) var(--button-mix-20), transparent);
107
+ box-shadow: var(--button-outline-shadow-inset);
108
+ }
109
+
110
+ [data-warm-button][data-variant="ghost"] {
111
+ /* `background-color` (not the `background` shorthand): the shorthand
112
+ implicitly resets `background-clip` to its initial `border-box`,
113
+ diverging from the base recipe's `padding-box` (origin: `bg-clip-padding`
114
+ with no resting-state Tailwind `bg-*` utility on ghost). */
115
+ background-color: transparent;
116
+ }
117
+ [data-warm-button][data-variant="ghost"]:hover {
118
+ background-color: var(--muted);
119
+ color: var(--foreground);
120
+ }
121
+ .dark [data-warm-button][data-variant="ghost"]:hover {
122
+ background-color: color-mix(in oklab, var(--muted) var(--button-mix-50), transparent);
123
+ }
124
+
125
+ [data-warm-button][data-variant="destructive"] {
126
+ background-color: color-mix(in oklab, var(--destructive) var(--button-mix-10), transparent);
127
+ color: var(--destructive);
128
+ }
129
+ [data-warm-button][data-variant="destructive"]:hover {
130
+ background-color: color-mix(in oklab, var(--destructive) var(--button-mix-20), transparent);
131
+ }
132
+ .dark [data-warm-button][data-variant="destructive"] {
133
+ background-color: color-mix(in oklab, var(--destructive) var(--button-mix-20), transparent);
134
+ }
135
+ .dark [data-warm-button][data-variant="destructive"]:hover {
136
+ background-color: color-mix(in oklab, var(--destructive) var(--button-mix-30), transparent);
137
+ }
138
+
139
+ [data-warm-button][data-variant="apple"] {
140
+ /* `!important` on color/background: legacy's `buttonVariantClasses.apple`
141
+ overlay always applies after any presentation overlay in `cn()` order
142
+ (e.g. promptCategory's own `bg-background`/`text-foreground`,
143
+ sidebarIcon's `text-sidebar-foreground`, tableHeader's
144
+ `text-muted-foreground`), so apple's colors always win regardless of
145
+ presentation. Presentation rules here use `:not([data-explicit-size])`,
146
+ which raises their specificity above a plain variant selector, so
147
+ `!important` is required to reproduce that same always-wins order. Box
148
+ shadow is deliberately NOT `!important`: promptCategory's own
149
+ `shadow-xs` legitimately overrides apple's outline-style shadow (see
150
+ the promptCategory rules above), matching origin exactly. */
151
+ background-color: var(--apple-button-background) !important;
152
+ color: var(--apple-button-foreground) !important;
153
+ box-shadow: var(--button-outline-shadow);
154
+ }
155
+ button[data-warm-button][data-variant="apple"] {
156
+ appearance: button;
157
+ background-clip: padding-box;
158
+ }
159
+ [data-warm-button][data-variant="apple"]:hover {
160
+ background-color: color-mix(in oklab, var(--apple-button-hover) var(--button-mix-90), transparent);
161
+ color: var(--apple-button-foreground);
162
+ }
163
+ [data-warm-button][data-variant="arrow"] {
164
+ border-width: 0;
165
+ text-decoration: underline;
166
+ text-underline-offset: var(--button-underline-offset);
167
+ }
168
+ [data-warm-button][data-variant="arrow"] svg {
169
+ width: var(--button-icon-size);
170
+ height: var(--button-icon-size);
171
+ transition: transform calc(var(--motion-duration-fast) * 2);
172
+ }
173
+ [data-warm-button][data-variant="arrow"]:hover svg {
174
+ transform: translate(calc(var(--motion-distance-sm) / 2), calc(-1 * var(--motion-distance-sm) / 2));
175
+ }
176
+ @media (prefers-reduced-motion: reduce) {
177
+ [data-warm-button][data-variant="arrow"]:hover svg {
178
+ transform: none;
179
+ transition: none;
180
+ }
181
+ }
182
+
183
+ /* Sizes (public only; presentation overrides suppressed by explicit size via data attr, matches old resolveButton) */
184
+ [data-warm-button][data-size="default"] {
185
+ height: var(--button-height-default);
186
+ padding-left: var(--button-padding-x);
187
+ padding-right: var(--button-padding-x);
188
+ gap: var(--button-gap);
189
+ }
190
+ [data-warm-button][data-size="jumbo"] {
191
+ height: var(--button-height-jumbo);
192
+ padding-left: var(--button-padding-x);
193
+ padding-right: var(--button-padding-x);
194
+ font-size: var(--button-font-size-jumbo);
195
+ font-weight: 700;
196
+ line-height: var(--button-line-height-jumbo);
197
+ gap: var(--button-gap);
198
+ }
199
+ [data-warm-button][data-size="icon"] {
200
+ width: var(--button-height-icon);
201
+ height: var(--button-height-icon);
202
+ }
203
+ /* Explicit-size 'default' gets an extra vertical inset (legacy
204
+ buttonInputAlignmentClasses.default = 'bg-clip-border py-2'); this only
205
+ applies when the caller passes size="default" explicitly, never when
206
+ 'default' is reached through presentation-driven implicit sizing. */
207
+ [data-warm-button][data-size="default"][data-explicit-size] {
208
+ padding-top: var(--button-padding-y-md);
209
+ padding-bottom: var(--button-padding-y-md);
210
+ background-clip: border-box;
211
+ }
212
+
213
+ /* Presentations: private legacy maps drive resolve to public data-size; geometry only when !data-explicit-size. Exact from old buttonPresentationClasses + primitive sizes (icon-round=36px etc) + edge + variants. */
214
+ [data-warm-button][data-presentation="assistantIcon"]:not([data-explicit-size]) {
215
+ width: var(--button-size-xs);
216
+ height: var(--button-size-xs);
217
+ padding: var(--button-padding-y-md);
218
+ }
219
+ [data-warm-button][data-presentation="assistantIcon"]:not([data-explicit-size]):active:not([aria-haspopup]) {
220
+ transform: scale(0.9);
221
+ }
222
+ @media (prefers-reduced-motion: reduce) {
223
+ [data-warm-button][data-presentation="assistantIcon"]:not([data-explicit-size]):active:not(
224
+ [aria-haspopup]
225
+ ) {
226
+ transform: none;
227
+ }
228
+ }
229
+ [data-warm-button][data-presentation="assistantRoundIcon"]:not([data-explicit-size]) {
230
+ /* old icon-round 36px */
231
+ width: var(--button-height-icon);
232
+ height: var(--button-height-icon);
233
+ }
234
+ [data-warm-button][data-presentation="composerIcon"]:not([data-explicit-size]) {
235
+ width: var(--button-size-sm);
236
+ height: var(--button-size-sm);
237
+ padding: var(--button-padding-y-md);
238
+ }
239
+ [data-warm-button][data-presentation="editPill"]:not([data-explicit-size]) {
240
+ /* old sm 32px */
241
+ height: var(--button-size-md);
242
+ padding-left: var(--button-padding-x);
243
+ padding-right: var(--button-padding-x);
244
+ }
245
+ [data-warm-button][data-presentation="hero"]:not([data-explicit-size]) {
246
+ height: var(--button-size-lg);
247
+ padding-left: var(--button-padding-x);
248
+ padding-right: var(--button-padding-x);
249
+ padding-top: var(--button-padding-y-md);
250
+ padding-bottom: var(--button-padding-y-md);
251
+ font-size: var(--button-font-size-base);
252
+ font-weight: 700;
253
+ line-height: var(--button-line-height-base);
254
+ background-clip: border-box;
255
+ }
256
+ [data-warm-button][data-presentation="promptCategory"]:not([data-explicit-size]) {
257
+ height: var(--button-height-default);
258
+ gap: var(--button-gap);
259
+ background-color: var(--background);
260
+ padding-left: var(--button-padding-x);
261
+ padding-right: var(--button-padding-x);
262
+ font-size: var(--button-font-size);
263
+ font-weight: 400;
264
+ color: var(--foreground);
265
+ transition-property: var(--button-transition-property-colors);
266
+ }
267
+ /* legacy's `getButtonEdgeClassName` applies `shadow-xs!` (Tailwind's `!`
268
+ important modifier) to every `default`-variant promptCategory button
269
+ regardless of explicit/implicit sizing, overriding the default variant's
270
+ own gradient shadow unconditionally; mirror that with `!important` here. */
271
+ [data-warm-button][data-presentation="promptCategory"][data-variant="default"] {
272
+ box-shadow: var(--shadow-xs) !important;
273
+ }
274
+ /* buttonPresentationClasses.promptCategory's own (non-!important) `shadow-xs`
275
+ utility: only present in implicit mode (resolveLegacyButton's `!size`
276
+ branch), and only survives the cascade for variants without their own
277
+ later shadow override — arrow forces `shadow-none` and must keep winning;
278
+ default is already covered, unconditionally, by the rule above. */
279
+ [data-warm-button][data-presentation="promptCategory"]:not([data-explicit-size]):not([data-variant="arrow"]) {
280
+ box-shadow: var(--shadow-xs) !important;
281
+ }
282
+ [data-warm-button][data-presentation="promptCategory"]:hover {
283
+ background-color: var(--muted);
284
+ }
285
+ [data-warm-button][data-presentation="promptOption"]:not([data-explicit-size]) {
286
+ height: auto;
287
+ min-height: var(--button-min-height-option);
288
+ align-items: flex-start;
289
+ justify-content: flex-start;
290
+ gap: var(--button-gap);
291
+ padding: var(--button-padding-x);
292
+ font-size: var(--button-font-size);
293
+ }
294
+ [data-warm-button][data-presentation="sidebarIcon"]:not([data-explicit-size]) {
295
+ color: var(--sidebar-foreground);
296
+ background-clip: border-box;
297
+ }
298
+ [data-warm-button][data-presentation="sidebarIcon"]:hover {
299
+ background-color: var(--sidebar-accent);
300
+ color: var(--sidebar-accent-foreground);
301
+ }
302
+ [data-warm-button][data-presentation="tableHeader"]:not([data-explicit-size]) {
303
+ height: auto;
304
+ min-height: 0;
305
+ width: var(--button-width-full);
306
+ justify-content: flex-start;
307
+ gap: var(--button-gap);
308
+ padding: 0;
309
+ font-weight: 400;
310
+ /* text-sm's line-height: tableHeader is the only non-arrow presentation
311
+ relying on `height: auto` with real text content, so the base rule's
312
+ `line-height: 1` (matching cva's other fixed-height presentations,
313
+ where line-height is irrelevant) would otherwise under-measure it. */
314
+ line-height: var(--button-line-height);
315
+ color: var(--muted-foreground);
316
+ }
317
+ [data-warm-button][data-presentation="tableHeader"]:hover {
318
+ background-color: transparent;
319
+ color: var(--foreground);
320
+ }
321
+ [data-warm-button][data-presentation="touch"]:not([data-explicit-size]) {
322
+ min-height: var(--button-min-height-touch);
323
+ }
324
+ [data-warm-button][data-presentation="threadMore"]:not([data-explicit-size]) {
325
+ width: var(--button-size-xs);
326
+ height: var(--button-size-xs);
327
+ padding: 0;
328
+ }
329
+ [data-warm-button][data-presentation="threadNew"]:not([data-explicit-size]) {
330
+ height: var(--button-size-md);
331
+ justify-content: flex-start;
332
+ gap: var(--button-gap);
333
+ padding-left: var(--button-padding-x-sm);
334
+ padding-right: var(--button-padding-x-sm);
335
+ font-size: var(--button-font-size);
336
+ font-weight: 400;
337
+ }
338
+ [data-warm-button][data-presentation="threadNew"]:hover {
339
+ background-color: var(--muted);
340
+ }
341
+ [data-warm-button][data-presentation="compact"]:not([data-explicit-size]) {
342
+ height: var(--button-size-md);
343
+ padding-left: var(--button-padding-x-sm);
344
+ padding-right: var(--button-padding-x-sm);
345
+ }
346
+ /* 'default' and 'controlPeer' both resolve to private size 'default' with no
347
+ own `buttonPresentationClasses` entry, so origin's `resolveLegacyButton`
348
+ falls through to `buttonInputAlignmentClasses.default = 'bg-clip-border
349
+ py-2'` even with no explicit size passed. Mirror that here (the explicit
350
+ `[data-size="default"][data-explicit-size]` rule below only fires once a
351
+ caller passes size="default" explicitly, so implicit 'default'/'controlPeer'
352
+ need their own copy of the same two properties). */
353
+ [data-warm-button][data-presentation="default"]:not([data-explicit-size]),
354
+ [data-warm-button][data-presentation="controlPeer"]:not([data-explicit-size]) {
355
+ padding-top: var(--button-padding-y-md);
356
+ padding-bottom: var(--button-padding-y-md);
357
+ background-clip: border-box;
358
+ }
359
+ [data-warm-button][data-presentation="menuOption"]:not([data-explicit-size]) {
360
+ /* old option min-height/auto 64px */
361
+ height: auto;
362
+ min-height: var(--button-min-height-option);
363
+ align-items: flex-start;
364
+ justify-content: flex-start;
365
+ gap: var(--button-gap);
366
+ padding: var(--button-padding-x);
367
+ }
368
+
369
+ /* Arrow overrides any presentation-driven box geometry declared above it:
370
+ legacy's cn(presentationClassName, variantClassName, ...) always merges the
371
+ arrow variant classes last, so arrow's box reset wins whenever it conflicts
372
+ with a presentation, and height is always content-driven (verified against
373
+ the real compiled legacy recipe: every presentation x size combination
374
+ renders the same auto height once variant="arrow"). Width is intentionally
375
+ left alone everywhere: arrow's own recipe never sets it, so an explicit
376
+ public size's fixed width (e.g. `[data-size="icon"]`) or a presentation's
377
+ own private width (e.g. assistantIcon's 1.5rem) still applies underneath
378
+ it. The selector repeats `[data-variant="arrow"]` to match the (0,3,0)
379
+ specificity of the `:not([data-explicit-size])` presentation rules and the
380
+ `[data-size][data-explicit-size]` size rule above, so this later rule wins
381
+ the cascade instead of losing on specificity. */
382
+ [data-warm-button][data-variant="arrow"][data-variant="arrow"] {
383
+ height: auto;
384
+ padding: 0;
385
+ gap: var(--button-gap);
386
+ background-color: transparent;
387
+ background-image: none;
388
+ box-shadow: none;
389
+ border-radius: 0;
390
+ color: var(--foreground);
391
+ font-size: var(--button-font-size-base);
392
+ /* text-base's line-height (verified to win over any competing presentation
393
+ or size text utility, e.g. jumbo's text-lg, in the real compiled legacy
394
+ recipe). Only affects buttons with a real text node: an icon-only arrow
395
+ button has no inline content to lay out a line box for, so this is a
396
+ no-op there and height still resolves from the icon's own box (verified:
397
+ icon-only arrow buttons measure 16/20px tall matching their glyph, never
398
+ the 24px text strut). */
399
+ line-height: var(--button-line-height-base);
400
+ font-weight: 700;
401
+ }
402
+
403
+ /* SVG rules (from cva) */
404
+ [data-warm-button] svg {
405
+ pointer-events: none;
406
+ flex-shrink: 0;
407
+ }
408
+ [data-warm-button] svg:not([class*="size-"]) {
409
+ width: var(--button-icon-size);
410
+ height: var(--button-icon-size);
411
+ }
412
+ [data-warm-button][data-size="jumbo"] svg:not([class*="size-"]) {
413
+ width: var(--button-icon-size-jumbo);
414
+ height: var(--button-icon-size-jumbo);
415
+ }
416
+ /* `hero`'s private size (`lg`) publicly maps to the same `data-size="jumbo"`
417
+ bucket as the genuine cva `jumbo` size (both being the "larger" public
418
+ size per `resolveButton`), but legacy's real `lg` primitive size carries no
419
+ svg-shrink/grow override at all, unlike `jumbo`'s own `size-5`. Restore the
420
+ unscaled glyph for `hero` specifically so it doesn't inherit jumbo's larger
421
+ icon. Gated to implicit sizing only: if the caller explicitly passes
422
+ `size="jumbo"` on a hero-presentation button, that is a real request for
423
+ the genuine cva `jumbo` size (own `size-5` icon included), not hero's
424
+ private `lg` behavior. `:not([data-explicit-size])` also raises this rule's
425
+ specificity above the plain jumbo rule above, so it wins unconditionally
426
+ whenever it matches. */
427
+ [data-warm-button][data-presentation="hero"]:not([data-explicit-size]) svg:not([class*="size-"]) {
428
+ width: var(--button-icon-size);
429
+ height: var(--button-icon-size);
430
+ }
431
+ /* Arrow's own icon size (legacy `[&_svg]:size-4`) wins over the base and
432
+ default-size svg rules above via source order, but intentionally has lower
433
+ specificity than the jumbo icon rule below: legacy's own jumbo svg rule
434
+ (`[&_svg:not([class*='size-'])]:size-5`) carries one more selector
435
+ component than arrow's (`[&_svg]:size-4`), so jumbo's larger glyph size
436
+ wins whenever both apply (verified against the real compiled recipe). */
437
+ [data-warm-button][data-variant="arrow"] svg {
438
+ width: var(--button-icon-size);
439
+ height: var(--button-icon-size);
440
+ }
441
+ /* icon-xs primitive size (assistantIcon, threadMore) shrinks the icon glyph;
442
+ suppressed when the caller passes an explicit public size. */
443
+ [data-warm-button][data-presentation="assistantIcon"]:not([data-explicit-size]) svg:not([class*="size-"]),
444
+ [data-warm-button][data-presentation="threadMore"]:not([data-explicit-size]) svg:not([class*="size-"]) {
445
+ width: var(--button-icon-size-xs);
446
+ height: var(--button-icon-size-xs);
447
+ }
448
+ /* Aria states (from cva aria-invalid + aria-expanded + dark) */
449
+ [data-warm-button][aria-invalid] {
450
+ box-shadow: 0 0 0 var(--button-focus-ring) color-mix(in oklab, var(--destructive) var(--button-mix-20), transparent);
451
+ }
452
+ .dark [data-warm-button][aria-invalid] {
453
+ box-shadow: 0 0 0 var(--button-focus-ring) color-mix(in oklab, var(--destructive) var(--button-mix-40), transparent);
454
+ }
455
+ [data-warm-button][data-variant="ghost"][aria-expanded="true"],
456
+ [data-warm-button][data-variant="outline"][aria-expanded="true"] {
457
+ background-color: var(--muted);
458
+ color: var(--foreground);
459
+ }
460
+
461
+ /* Disabled base + variant overrides (keep full shadow/border for some) */
462
+ [data-warm-button]:disabled {
463
+ pointer-events: none;
464
+ opacity: 0.5;
465
+ }
466
+ [data-warm-button][data-variant="apple"]:disabled,
467
+ [data-warm-button][data-variant="outline"]:disabled {
468
+ opacity: 1;
469
+ }
470
+
471
+ /* Logo - keep data-warm-logo on wrapper div AND svg; unambiguous element selectors + data to avoid sizing wrapper or constraining width; wrapper inline-flex (not inline-block); preserve gap/text/typography/tone/icon sizes from old */
472
+ [data-warm-logo] {
473
+ display: inline-flex;
474
+ align-items: center;
475
+ gap: var(--logo-gap);
476
+ }
477
+ [data-warm-logo] > span {
478
+ font-size: var(--logo-font-size);
479
+ font-weight: 700;
480
+ letter-spacing: var(--logo-letter-spacing);
481
+ }
482
+ svg[data-warm-logo] {
483
+ display: inline-block;
484
+ vertical-align: middle;
485
+ fill: currentColor;
486
+ flex-shrink: 0;
487
+ }
488
+ [data-warm-logo][data-size="badge"] svg[data-warm-logo] { width: var(--logo-size-badge); height: var(--logo-size-badge); }
489
+ [data-warm-logo][data-size="default"] svg[data-warm-logo] { width: var(--logo-size-default); height: var(--logo-size-default); }
490
+ [data-warm-logo][data-size="feature"] svg[data-warm-logo] { width: var(--logo-size-feature); height: var(--logo-size-feature); }
491
+ [data-warm-logo][data-size="hero"] svg[data-warm-logo] { width: var(--logo-size-hero); height: var(--logo-size-hero); }
492
+
493
+ [data-warm-logo][data-tone="foreground"] { color: var(--foreground); }
494
+ [data-warm-logo][data-tone="sidebar"] { color: var(--sidebar-foreground); }
495
+
@@ -0,0 +1,49 @@
1
+ import * as React from 'react';
2
+ export type ButtonPresentation = 'assistantIcon' | 'assistantRoundIcon' | 'default' | 'composerIcon' | 'compact' | 'controlPeer' | 'editPill' | 'hero' | 'menuOption' | 'promptCategory' | 'promptOption' | 'sidebarIcon' | 'tableHeader' | 'threadMore' | 'threadNew' | 'touch';
3
+ export type ButtonVariant = 'default' | 'outline' | 'ghost' | 'destructive' | 'apple' | 'arrow';
4
+ export type ButtonSize = 'default' | 'jumbo' | 'icon';
5
+ export type ButtonProps = Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, 'className' | 'style'> & {
6
+ asChild?: never;
7
+ className?: never;
8
+ presentation?: ButtonPresentation;
9
+ render?: never;
10
+ size?: ButtonSize;
11
+ style?: never;
12
+ variant?: ButtonVariant;
13
+ width?: 'full';
14
+ };
15
+ export type ButtonLinkProps = Omit<React.AnchorHTMLAttributes<HTMLAnchorElement>, 'className' | 'disabled' | 'style'> & {
16
+ href: string;
17
+ presentation?: ButtonPresentation;
18
+ size?: ButtonSize;
19
+ variant?: ButtonVariant;
20
+ width?: 'full';
21
+ asChild?: never;
22
+ className?: never;
23
+ disabled?: never;
24
+ render?: never;
25
+ style?: never;
26
+ };
27
+ declare const Button: React.ForwardRefExoticComponent<Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, "className" | "style"> & {
28
+ asChild?: never;
29
+ className?: never;
30
+ presentation?: ButtonPresentation;
31
+ render?: never;
32
+ size?: ButtonSize;
33
+ style?: never;
34
+ variant?: ButtonVariant;
35
+ width?: 'full';
36
+ } & React.RefAttributes<HTMLButtonElement>>;
37
+ declare const ButtonLink: React.ForwardRefExoticComponent<Omit<React.AnchorHTMLAttributes<HTMLAnchorElement>, "className" | "disabled" | "style"> & {
38
+ href: string;
39
+ presentation?: ButtonPresentation;
40
+ size?: ButtonSize;
41
+ variant?: ButtonVariant;
42
+ width?: 'full';
43
+ asChild?: never;
44
+ className?: never;
45
+ disabled?: never;
46
+ render?: never;
47
+ style?: never;
48
+ } & React.RefAttributes<HTMLAnchorElement>>;
49
+ export { Button, ButtonLink };
package/dist/button.js ADDED
@@ -0,0 +1,49 @@
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import * as React from 'react';
3
+ const presentationToPrivateSize = {
4
+ assistantIcon: 'icon-xs',
5
+ assistantRoundIcon: 'icon-round',
6
+ composerIcon: 'icon-sm',
7
+ default: 'default',
8
+ compact: 'sm',
9
+ controlPeer: 'default',
10
+ editPill: 'sm',
11
+ hero: 'lg',
12
+ menuOption: 'option',
13
+ promptCategory: 'default',
14
+ promptOption: 'option',
15
+ sidebarIcon: 'icon',
16
+ tableHeader: 'default',
17
+ threadMore: 'icon-xs',
18
+ threadNew: 'sm',
19
+ touch: 'default',
20
+ };
21
+ function resolveButton(presentation, size) {
22
+ const privateSize = presentationToPrivateSize[presentation];
23
+ const resolvedSize = size ??
24
+ (privateSize === 'lg'
25
+ ? 'jumbo'
26
+ : privateSize === 'icon' ||
27
+ privateSize === 'icon-round' ||
28
+ privateSize === 'icon-sm' ||
29
+ privateSize === 'icon-xs'
30
+ ? 'icon'
31
+ : 'default');
32
+ return { resolvedSize, explicitSize: size ? resolvedSize : undefined };
33
+ }
34
+ function withArrow(variant, children) {
35
+ if (variant !== 'arrow')
36
+ return children;
37
+ return (_jsxs(_Fragment, { children: [children, _jsxs("svg", { "aria-hidden": "true", "data-icon": "inline-end", width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [_jsx("path", { d: "M7 7h10v10" }), _jsx("path", { d: "M7 17L17 7" })] })] }));
38
+ }
39
+ const Button = React.forwardRef(function Button({ children, className: _className, presentation = 'default', render: _render, size, style: _style, type, variant = 'default', width, asChild: _asChild, ...props }, ref) {
40
+ const { resolvedSize, explicitSize } = resolveButton(presentation, size);
41
+ return (_jsx("button", { ...props, ref: ref, type: type ?? 'button', "data-warm-button": "", "data-slot": "button", "data-presentation": presentation, "data-variant": variant, "data-size": resolvedSize, "data-explicit-size": explicitSize, "data-width": width, children: withArrow(variant, children) }));
42
+ });
43
+ Button.displayName = 'WarmButton';
44
+ const ButtonLink = React.forwardRef(function ButtonLink({ children, className: _className, disabled: _disabled, presentation = 'default', render: _render, size, style: _style, variant = 'default', width, asChild: _asChild, ...props }, ref) {
45
+ const { resolvedSize, explicitSize } = resolveButton(presentation, size);
46
+ return (_jsx("a", { ...props, ref: ref, "data-warm-button": "", "data-slot": "button", "data-presentation": presentation, "data-variant": variant, "data-size": resolvedSize, "data-explicit-size": explicitSize, "data-width": width, children: withArrow(variant, children) }));
47
+ });
48
+ ButtonLink.displayName = 'WarmButtonLink';
49
+ export { Button, ButtonLink };
package/dist/logo.d.ts ADDED
@@ -0,0 +1,33 @@
1
+ import * as React from 'react';
2
+ export type WarmLogoSize = 'badge' | 'default' | 'feature' | 'hero';
3
+ export type WarmLogoTone = 'default' | 'foreground' | 'sidebar';
4
+ export type WarmLogoIconProps = {
5
+ size?: WarmLogoSize;
6
+ tone?: WarmLogoTone;
7
+ decorative?: boolean;
8
+ className?: never;
9
+ style?: never;
10
+ asChild?: never;
11
+ render?: never;
12
+ } & Omit<React.SVGProps<SVGSVGElement>, 'className' | 'style'>;
13
+ export type LogoProps = {
14
+ size?: WarmLogoSize;
15
+ tone?: WarmLogoTone;
16
+ showText?: boolean;
17
+ className?: never;
18
+ style?: never;
19
+ textClass?: never;
20
+ asChild?: never;
21
+ render?: never;
22
+ } & Omit<React.HTMLAttributes<HTMLDivElement>, 'className' | 'style' | 'textClass'>;
23
+ export declare function WarmLogoIcon({ size, tone, decorative, className: _className, style: _style, asChild: _asChild, render: _render, ...props }: WarmLogoIconProps): import("react/jsx-runtime").JSX.Element;
24
+ export declare const Logo: React.ForwardRefExoticComponent<{
25
+ size?: WarmLogoSize;
26
+ tone?: WarmLogoTone;
27
+ showText?: boolean;
28
+ className?: never;
29
+ style?: never;
30
+ textClass?: never;
31
+ asChild?: never;
32
+ render?: never;
33
+ } & Omit<React.HTMLAttributes<HTMLDivElement>, "className" | "style" | "textClass"> & React.RefAttributes<HTMLDivElement>>;
package/dist/logo.js ADDED
@@ -0,0 +1,10 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import * as React from 'react';
3
+ const WShape = (_jsxs("g", { fill: "currentColor", children: [_jsx("path", { d: "M 33.500 40.660 C 20.206 42.744, 0 48.560, -0 50.302 C -0 51.759, 123.955 348.889, 127.738 356.500 C 142.182 385.561, 169.450 410.275, 198.946 421.040 C 218.922 428.330, 243.683 430.825, 264.526 427.648 C 277.151 425.724, 294.628 420.011, 294.194 417.952 C 293.790 416.040, 172.732 124.599, 168.522 115.405 C 161.779 100.679, 149.168 84.171, 135.093 71.646 C 119.358 57.644, 98.880 47.362, 77 42.478 C 69.645 40.836, 40.220 39.606, 33.500 40.660" }), _jsx("path", { d: "M 229.651 41.597 C 217.916 43.718, 202.443 49.144, 202.744 51.035 C 203.418 55.268, 329.689 356.584, 333.687 363.500 C 340.525 375.327, 345.293 381.483, 355.408 391.538 C 374.603 410.619, 398.878 423.014, 425.907 427.533 C 439.649 429.831, 463.594 429.132, 476 426.072 C 489.892 422.645, 497.188 419.815, 496.703 418.043 C 495.416 413.331, 371.058 116.094, 367.406 109 C 349.467 74.149, 313.008 47.875, 273.752 41.509 C 262.281 39.648, 240.195 39.693, 229.651 41.597" }), _jsx("path", { d: "M 463 40.593 C 451.670 42.545, 447.288 43.569, 439 46.203 C 433.775 47.864, 429.338 49.372, 429.140 49.555 C 428.942 49.737, 434.670 64.021, 441.868 81.297 C 455.574 114.190, 462.215 131.660, 468.447 151.208 C 475.360 172.892, 486.124 186.213, 503.834 194.997 C 530.956 208.450, 564.982 201.679, 583.654 179.112 C 604.399 154.041, 605.407 121.944, 586.291 95.155 C 567.827 69.280, 539.107 50.114, 508 42.907 C 500.680 41.212, 494.713 40.644, 482 40.434 C 472.925 40.284, 464.375 40.356, 463 40.593" })] }));
4
+ export function WarmLogoIcon({ size = 'default', tone = 'default', decorative = false, className: _className, style: _style, asChild: _asChild, render: _render, ...props }) {
5
+ const label = decorative ? undefined : 'Warm logo';
6
+ return (_jsx("svg", { xmlns: "http://www.w3.org/2000/svg", viewBox: "0 0 600 469", "data-warm-logo": true, "data-size": size, "data-tone": tone, width: size === 'hero' ? 56 : size === 'feature' ? 32 : size === 'default' ? 28 : 12, height: size === 'hero' ? 56 : size === 'feature' ? 32 : size === 'default' ? 28 : 12, "aria-hidden": decorative ? true : undefined, "aria-label": label, ...props, children: WShape }));
7
+ }
8
+ export const Logo = React.forwardRef(function Logo({ size = 'default', tone = 'default', showText = true, className: _className, style: _style, textClass: _textClass, asChild: _asChild, render: _render, ...props }, ref) {
9
+ return (_jsxs("div", { ref: ref, "data-warm-logo": true, "data-size": size, "data-tone": tone, ...props, children: [_jsx(WarmLogoIcon, { size: size, tone: tone, decorative: showText }), showText ? _jsx("span", { children: "Warm" }) : null] }));
10
+ });
package/globals.css CHANGED
@@ -2,3 +2,4 @@
2
2
  @import './theme.css';
3
3
  @import './base.css';
4
4
  @import './interfaces.css';
5
+ @import './components.css';
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@warmio/design",
3
- "version": "1.4.0",
4
- "description": "Warm's public CSS-only design contract.",
3
+ "version": "1.5.0",
4
+ "description": "Warm's public design contract: CSS tokens and Button/Logo primitives.",
5
5
  "license": "MIT",
6
6
  "type": "module",
7
7
  "sideEffects": [
@@ -23,7 +23,10 @@
23
23
  "interfaces.css",
24
24
  "theme.css",
25
25
  "themes.css",
26
+ "components.css",
26
27
  "fonts",
28
+ "assets",
29
+ "dist",
27
30
  "LICENSES"
28
31
  ],
29
32
  "exports": {
@@ -32,9 +35,22 @@
32
35
  "./fonts.css": "./fonts.css",
33
36
  "./theme.css": "./theme.css",
34
37
  "./themes.css": "./themes.css",
35
- "./interfaces.css": "./interfaces.css"
38
+ "./interfaces.css": "./interfaces.css",
39
+ "./components.css": "./components.css",
40
+ "./button": {
41
+ "types": "./dist/button.d.ts",
42
+ "import": "./dist/button.js"
43
+ },
44
+ "./logo": {
45
+ "types": "./dist/logo.d.ts",
46
+ "import": "./dist/logo.js"
47
+ }
48
+ },
49
+ "peerDependencies": {
50
+ "react": "^19.0.0"
36
51
  },
37
52
  "scripts": {
53
+ "build": "tsc -p tsconfig.json",
38
54
  "pack:check": "node ../../tools/tasks/check-design-package.mjs"
39
55
  }
40
56
  }
package/theme.css CHANGED
@@ -73,6 +73,11 @@
73
73
  /* Alternating section backgrounds for marketing pages */
74
74
  --section-alt: oklch(0.986 0 0);
75
75
 
76
+ /* Marketing surface and dot patterns (owned by design) */
77
+ --marketing-surface: #f7f7f7;
78
+ --marketing-pattern-dots: url('./assets/subtle-dots.png');
79
+ --marketing-pattern-dots-dark: url('./assets/subtle-dots-dark.svg');
80
+
76
81
  /* Charts */
77
82
  --chart-1: oklch(0.32 0 none);
78
83
  --chart-2: oklch(0.41 0 none);
@@ -84,7 +89,7 @@
84
89
  --logo-accent: var(--logo-primary);
85
90
 
86
91
  /* ══════════════════════════════════════════════════════════════════════════
87
- SIDEBAR — Transparent to show body texture
92
+ SIDEBAR — Transparent to show body texture
88
93
  ══════════════════════════════════════════════════════════════════════════ */
89
94
  --sidebar: oklch(0.986 0 0);
90
95
  --sidebar-foreground: var(--foreground);
@@ -99,7 +104,6 @@
99
104
  FIXED SEMANTIC COLORS
100
105
  These are universal - green=success, amber=warning, red=error, blue=info
101
106
  ══════════════════════════════════════════════════════════════════════════ */
102
-
103
107
  /* Success = Green (H: 145) */
104
108
  --success: oklch(0.65 0.16 145);
105
109
  --success-text: oklch(0.35 0.16 145);
@@ -203,13 +207,23 @@
203
207
  --spacing-xl: 1.5rem;
204
208
  --spacing-2xl: 2rem;
205
209
 
206
- /* Transitions */
207
- --transition-fast: 100ms;
208
- --transition-normal: 300ms;
209
- --transition-slow: 600ms;
210
- --chart-animation-duration: 1000ms;
211
-
212
- /* Button Specific */
210
+ /* Motion tokens (single source of truth; package-wide reduced-motion contract)
211
+ * --motion-duration-essential unused by default (requires audited justification + exception)
212
+ * nonessentials (chart uses slow) zero under reduced-motion
213
+ */
214
+ --motion-duration-fast: 100ms;
215
+ --motion-duration-normal: 300ms;
216
+ --motion-duration-slow: 600ms;
217
+ --motion-duration-essential: 1000ms;
218
+ --motion-distance-sm: 0.25rem;
219
+ --motion-distance-md: 0.5rem;
220
+ --motion-distance-lg: 1rem;
221
+
222
+ /* Aliases keep one scale; legacy names resolve to motion; chart uses slow (nonessential scale) */
223
+ --transition-fast: var(--motion-duration-fast);
224
+ --transition-normal: var(--motion-duration-normal);
225
+ --transition-slow: var(--motion-duration-slow);
226
+ --chart-animation-duration: var(--motion-duration-slow);
213
227
  --button-padding-x-sm: 0.5rem;
214
228
  --button-padding-y-sm: 0.25rem;
215
229
  --button-padding-x-md: 0.75rem;
@@ -247,6 +261,53 @@
247
261
  --apple-button-background: var(--foreground);
248
262
  --apple-button-foreground: var(--background);
249
263
  --apple-button-hover: var(--apple-button-background);
264
+ /* ══════════════════════════════════════════════════════════════════════════
265
+ CANONICAL BUTTON / LOGO / COMPONENT GEOMETRY TOKENS (small coherent set)
266
+ Theme owns ALL; components.css uses ONLY var() / token-derived calc() for
267
+ nonzero lengths/colors/shadows/radii/motion/timings. No local -- decls.
268
+ */
269
+ --button-height-default: 2.25rem;
270
+ --button-height-jumbo: 3.5rem;
271
+ --button-height-icon: 2.25rem;
272
+ --button-min-height-touch: 2.75rem;
273
+ --button-min-height-option: 4rem;
274
+ --button-padding-x: 1rem;
275
+ --button-font-size-base: 1rem;
276
+ --button-font-size-jumbo: 1.125rem;
277
+ --button-line-height-base: 1.5rem;
278
+ --button-line-height: 1.25rem;
279
+ --button-line-height-jumbo: 1.75rem;
280
+ --button-line-height-hero: 1.5rem;
281
+ --button-icon-size: 1rem;
282
+ --button-icon-size-jumbo: 1.25rem;
283
+ --button-icon-size-xs: 0.75rem;
284
+ --button-size-xs: 1.5rem;
285
+ --button-size-sm: 1.75rem;
286
+ --button-size-md: 2rem;
287
+ --button-size-lg: 3rem;
288
+ --button-underline-offset: 0.25rem;
289
+ --button-focus-ring: 3px;
290
+ --logo-size-badge: 0.75rem;
291
+ --logo-size-default: 1.75rem;
292
+ --logo-size-feature: 2rem;
293
+ --logo-size-hero: 3.5rem;
294
+ --logo-font-size: 0.875rem;
295
+ --logo-letter-spacing: 0.025em;
296
+ --logo-gap: 0.5rem;
297
+ --button-transition-property: color, background-color, box-shadow, border-color, transform;
298
+ --button-transition-property-colors: color, background-color, border-color, outline-color, text-decoration-color, fill, stroke, --tw-gradient-from, --tw-gradient-via, --tw-gradient-to;
299
+ --button-width-full: 100%;
300
+ --warm-surface-raised-dark: var(--input);
301
+ --warm-surface-raised-hover-dark: var(--muted);
302
+ /* canonical mix percents and dark outline inset (resolve dark outline via tokens) */
303
+ --button-mix-10: 10%;
304
+ --button-mix-20: 20%;
305
+ --button-mix-30: 30%;
306
+ --button-mix-40: 40%;
307
+ --button-mix-50: 50%;
308
+ --button-mix-90: 90%;
309
+ --radius-full: 33554400px;
310
+ --button-outline-shadow-inset: inset 0 1px 0 rgb(255 255 255 / 0.1);
250
311
 
251
312
  /* Form Specific */
252
313
  --input-height: 2.25rem;
@@ -307,6 +368,21 @@
307
368
  --breakpoint-xl: theme('screens.xl');
308
369
  --breakpoint-2xl: theme('screens.2xl');
309
370
  }
371
+ @media (prefers-reduced-motion: reduce) {
372
+ :root {
373
+ --motion-duration-fast: 0ms;
374
+ --motion-duration-normal: 0ms;
375
+ --motion-duration-slow: 0ms;
376
+ --motion-distance-sm: 0px;
377
+ --motion-distance-md: 0px;
378
+ --motion-distance-lg: 0px;
379
+ --transition-fast: 0ms;
380
+ --transition-normal: 0ms;
381
+ --transition-slow: 0ms;
382
+ --chart-animation-duration: 0ms;
383
+ }
384
+ }
385
+
310
386
 
311
387
  /* ════════════════════════════════════════════════════════════════════════════
312
388
  DARK MODE
@@ -326,6 +402,9 @@
326
402
  /* Alternating section backgrounds - notably darker for visual distinction */
327
403
  --section-alt: oklch(0.2 0 0);
328
404
 
405
+ /* Marketing surface (dark) */
406
+ --marketing-surface: #111;
407
+ /* Date-group header band comment follows */
329
408
  /* Date-group header band: a higher-contrast card surface so grouped date rows
330
409
  read as clear section dividers in dark mode (light mode uses bg-muted/40). */
331
410
  --date-group-surface: color-mix(in oklch, var(--card), white 8%);
@@ -371,6 +450,7 @@
371
450
  --apple-button-background: oklch(0 0 0);
372
451
  --apple-button-foreground: oklch(1 0 0);
373
452
  --apple-button-hover: var(--apple-button-background);
453
+ --button-outline-shadow-inset: inset 0 1px 0 rgb(255 255 255 / 0.1);
374
454
 
375
455
  /* Sidebar = solid dark surface */
376
456
  --sidebar: var(--background);