@warmio/design 1.7.0 → 1.8.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.
@@ -0,0 +1,24 @@
1
+ Fluid Functionalism
2
+ https://github.com/mickadesign/fluid-functionalism
3
+
4
+ MIT License
5
+
6
+ Copyright (c) 2026 Micka Touillaud
7
+
8
+ Permission is hereby granted, free of charge, to any person obtaining a copy
9
+ of this software and associated documentation files (the "Software"), to deal
10
+ in the Software without restriction, including without limitation the rights
11
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12
+ copies of the Software, and to permit persons to whom the Software is
13
+ furnished to do so, subject to the following conditions:
14
+
15
+ The above copyright notice and this permission notice shall be included in all
16
+ copies or substantial portions of the Software.
17
+
18
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
23
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
24
+ SOFTWARE.
package/README.md CHANGED
@@ -25,6 +25,11 @@ Package ships `components.css` (token-only styles for the primitives) and `dist/
25
25
 
26
26
  ## Releases
27
27
 
28
+ The next version, `1.8.0`, adds Fluid-derived surface-only Button press feedback.
29
+ The label, hit target, and layout stay fixed. Reduced motion removes surface travel.
30
+ The implementation uses native CSS and adds no JavaScript dependency.
31
+ Upstream attribution is in `LICENSES/fluid-functionalism.txt`.
32
+
28
33
  Bump this `package.json` version for additive tokens (incl. motion/surface), fonts,
29
34
  spacing, or primitive changes. Follow the exact sequence in
30
35
  [docs/reference/DESIGN_PACKAGE.md](../../docs/reference/DESIGN_PACKAGE.md)
package/components.css CHANGED
@@ -1,11 +1,13 @@
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, 4 sizes + full + states produce matching computed geometry/styles.
1
+ /* @warmio/design components.css - Warm Button/Logo recipes.
2
+ * Colors, typography, geometry, and edges remain Warm-owned.
3
+ * Surface-only press feedback adapted from Fluid Functionalism (MIT):
4
+ * https://www.fluidfunctionalism.com/r/base/button.json
5
+ * Native CSS keeps the published primitive free of a motion runtime.
6
6
  */
7
7
 
8
8
  [data-warm-button] {
9
+ position: relative;
10
+ isolation: isolate;
9
11
  /* Strip native button chrome when this stylesheet is used without preflight.
10
12
  * `appearance` is deliberately left at its UA default (matches the
11
13
  * pre-migration PrimitiveButton/buttonVariants recipe, which never applied
@@ -41,6 +43,27 @@
41
43
  /* border handled by variant shadows/tokens; no default regression border */
42
44
  }
43
45
 
46
+ /* Move only the painted surface, never the label or the hit target. Fluid's
47
+ * fixed 1px-per-edge collapse avoids width-dependent scaling of wide buttons.
48
+ * Warm gradients and edge shadows stay on this layer at their original bounds. */
49
+ [data-warm-button]::before {
50
+ content: '';
51
+ position: absolute;
52
+ z-index: -1;
53
+ inset: 0;
54
+ border-radius: inherit;
55
+ background-color: transparent;
56
+ background-image: none;
57
+ background-clip: inherit;
58
+ box-shadow: none;
59
+ pointer-events: none;
60
+ transition-property: var(--button-surface-transition-property);
61
+ transition-duration:
62
+ calc(var(--motion-duration-fast) * 1.8), calc(var(--motion-duration-fast) * 0.8),
63
+ calc(var(--motion-duration-fast) * 1.8);
64
+ transition-timing-function: var(--button-press-easing);
65
+ }
66
+
44
67
  [data-warm-button]:focus-visible {
45
68
  box-shadow: 0 0 0 var(--button-focus-ring)
46
69
  color-mix(in oklab, var(--ring) var(--button-mix-50), transparent);
@@ -50,89 +73,102 @@
50
73
  width: var(--button-width-full);
51
74
  }
52
75
 
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);
76
+ [data-warm-button]:not(:disabled):not([aria-disabled='true']):not(
77
+ [data-variant='arrow']
78
+ ):active::before {
79
+ inset: var(--button-press-inset);
80
+ transition-duration: calc(var(--motion-duration-fast) * 0.8);
56
81
  }
57
82
 
58
83
  @media (prefers-reduced-motion: reduce) {
59
- [data-warm-button] {
84
+ [data-warm-button],
85
+ [data-warm-button]::before {
60
86
  transition: none;
61
87
  }
62
- [data-warm-button]:active:not([aria-haspopup]) {
63
- transform: none;
88
+ [data-warm-button]:not(:disabled):not([aria-disabled='true']):not(
89
+ [data-variant='arrow']
90
+ ):active::before {
91
+ inset: 0;
64
92
  }
65
93
  }
66
94
 
67
95
  /* Variants (from cva + overrides + edge; tokens for color/shadow/radius) */
68
96
  [data-warm-button][data-variant='default'] {
97
+ color: var(--button-primary-foreground);
98
+ }
99
+ [data-warm-button][data-variant='default']::before {
69
100
  background-color: var(--button-primary-bg);
70
101
  background-image: var(--button-primary-gradient);
71
- color: var(--button-primary-foreground);
72
102
  box-shadow: var(--button-primary-shadow);
73
103
  /* no explicit border to avoid regression; shadow provides chrome */
74
104
  }
75
- [data-warm-button][data-variant='default']:hover {
105
+ [data-warm-button][data-variant='default']:hover::before {
76
106
  background-color: var(--button-primary-hover);
77
107
  background-image: var(--button-primary-gradient-hover);
78
108
  }
79
- [data-warm-button][data-variant='default']:active {
109
+ [data-warm-button][data-variant='default']:active::before {
80
110
  background-color: var(--button-primary-active);
81
111
  }
82
112
 
83
113
  [data-warm-button][data-variant='outline'] {
114
+ border-style: none;
115
+ }
116
+ [data-warm-button][data-variant='outline']::before {
84
117
  background-color: var(--background);
85
118
  box-shadow: var(--button-outline-shadow);
86
- border-style: none;
87
119
  }
88
120
  [data-warm-button][data-variant='outline']:hover {
89
- background-color: var(--muted);
90
121
  color: var(--foreground);
91
122
  }
92
- .dark [data-warm-button][data-variant='outline'] {
123
+ [data-warm-button][data-variant='outline']:hover::before {
124
+ background-color: var(--muted);
125
+ }
126
+ .dark [data-warm-button][data-variant='outline']::before {
93
127
  background-color: var(--warm-surface-raised-dark, var(--input));
94
128
  box-shadow: var(--button-outline-shadow-inset);
95
129
  }
96
- .dark [data-warm-button][data-variant='outline']:hover {
130
+ .dark [data-warm-button][data-variant='outline']:hover::before {
97
131
  background-color: var(--warm-surface-raised-hover-dark, var(--muted));
98
132
  }
99
133
  [data-warm-button][data-variant='outline']:disabled {
100
134
  opacity: 1;
101
- background-color: var(--muted);
102
135
  color: var(--muted-foreground);
136
+ }
137
+ [data-warm-button][data-variant='outline']:disabled::before {
138
+ background-color: var(--muted);
103
139
  box-shadow: var(--button-outline-shadow);
104
140
  }
105
- .dark [data-warm-button][data-variant='outline']:disabled {
141
+ .dark [data-warm-button][data-variant='outline']:disabled::before {
106
142
  background-color: color-mix(in oklab, var(--input) var(--button-mix-20), transparent);
107
143
  box-shadow: var(--button-outline-shadow-inset);
108
144
  }
109
145
 
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). */
146
+ [data-warm-button][data-variant='ghost']::before {
115
147
  background-color: transparent;
116
148
  }
117
149
  [data-warm-button][data-variant='ghost']:hover {
118
- background-color: var(--muted);
119
150
  color: var(--foreground);
120
151
  }
121
- .dark [data-warm-button][data-variant='ghost']:hover {
152
+ [data-warm-button][data-variant='ghost']:hover::before {
153
+ background-color: var(--muted);
154
+ }
155
+ .dark [data-warm-button][data-variant='ghost']:hover::before {
122
156
  background-color: color-mix(in oklab, var(--muted) var(--button-mix-50), transparent);
123
157
  }
124
158
 
125
159
  [data-warm-button][data-variant='destructive'] {
126
- background-color: color-mix(in oklab, var(--destructive) var(--button-mix-10), transparent);
127
160
  color: var(--destructive);
128
161
  }
129
- [data-warm-button][data-variant='destructive']:hover {
162
+ [data-warm-button][data-variant='destructive']::before {
163
+ background-color: color-mix(in oklab, var(--destructive) var(--button-mix-10), transparent);
164
+ }
165
+ [data-warm-button][data-variant='destructive']:hover::before {
130
166
  background-color: color-mix(in oklab, var(--destructive) var(--button-mix-20), transparent);
131
167
  }
132
- .dark [data-warm-button][data-variant='destructive'] {
168
+ .dark [data-warm-button][data-variant='destructive']::before {
133
169
  background-color: color-mix(in oklab, var(--destructive) var(--button-mix-20), transparent);
134
170
  }
135
- .dark [data-warm-button][data-variant='destructive']:hover {
171
+ .dark [data-warm-button][data-variant='destructive']:hover::before {
136
172
  background-color: color-mix(in oklab, var(--destructive) var(--button-mix-30), transparent);
137
173
  }
138
174
 
@@ -148,8 +184,10 @@
148
184
  shadow is deliberately NOT `!important`: promptCategory's own
149
185
  `shadow-xs` legitimately overrides apple's outline-style shadow (see
150
186
  the promptCategory rules above), matching origin exactly. */
151
- background-color: var(--apple-button-background) !important;
152
187
  color: var(--apple-button-foreground) !important;
188
+ }
189
+ [data-warm-button][data-variant='apple']::before {
190
+ background-color: var(--apple-button-background) !important;
153
191
  box-shadow: var(--button-outline-shadow);
154
192
  }
155
193
  button[data-warm-button][data-variant='apple'] {
@@ -157,12 +195,14 @@ button[data-warm-button][data-variant='apple'] {
157
195
  background-clip: padding-box;
158
196
  }
159
197
  [data-warm-button][data-variant='apple']:hover {
198
+ color: var(--apple-button-foreground);
199
+ }
200
+ [data-warm-button][data-variant='apple']:hover::before {
160
201
  background-color: color-mix(
161
202
  in oklab,
162
203
  var(--apple-button-hover) var(--button-mix-90),
163
204
  transparent
164
205
  );
165
- color: var(--apple-button-foreground);
166
206
  }
167
207
  [data-warm-button][data-variant='arrow'] {
168
208
  border-width: 0;
@@ -229,18 +269,6 @@ button[data-warm-button][data-variant='apple'] {
229
269
  height: var(--button-size-xs);
230
270
  padding: var(--button-padding-y-md);
231
271
  }
232
- [data-warm-button][data-presentation='assistantIcon']:not([data-explicit-size]):active:not(
233
- [aria-haspopup]
234
- ) {
235
- transform: scale(0.9);
236
- }
237
- @media (prefers-reduced-motion: reduce) {
238
- [data-warm-button][data-presentation='assistantIcon']:not([data-explicit-size]):active:not(
239
- [aria-haspopup]
240
- ) {
241
- transform: none;
242
- }
243
- }
244
272
  [data-warm-button][data-presentation='assistantRoundIcon']:not([data-explicit-size]) {
245
273
  /* old icon-round 36px */
246
274
  width: var(--button-height-icon);
@@ -271,7 +299,6 @@ button[data-warm-button][data-variant='apple'] {
271
299
  [data-warm-button][data-presentation='promptCategory']:not([data-explicit-size]) {
272
300
  height: var(--button-height-default);
273
301
  gap: var(--button-gap);
274
- background-color: var(--background);
275
302
  padding-left: var(--button-padding-x);
276
303
  padding-right: var(--button-padding-x);
277
304
  font-size: var(--button-font-size);
@@ -279,11 +306,14 @@ button[data-warm-button][data-variant='apple'] {
279
306
  color: var(--foreground);
280
307
  transition-property: var(--button-transition-property-colors);
281
308
  }
309
+ [data-warm-button][data-presentation='promptCategory']:not([data-explicit-size])::before {
310
+ background-color: var(--background);
311
+ }
282
312
  /* legacy's `getButtonEdgeClassName` applies `shadow-xs!` (Tailwind's `!`
283
313
  important modifier) to every `default`-variant promptCategory button
284
314
  regardless of explicit/implicit sizing, overriding the default variant's
285
315
  own gradient shadow unconditionally; mirror that with `!important` here. */
286
- [data-warm-button][data-presentation='promptCategory'][data-variant='default'] {
316
+ [data-warm-button][data-presentation='promptCategory'][data-variant='default']::before {
287
317
  box-shadow: var(--shadow-xs) !important;
288
318
  }
289
319
  /* buttonPresentationClasses.promptCategory's own (non-!important) `shadow-xs`
@@ -293,10 +323,10 @@ button[data-warm-button][data-variant='apple'] {
293
323
  default is already covered, unconditionally, by the rule above. */
294
324
  [data-warm-button][data-presentation='promptCategory']:not([data-explicit-size]):not(
295
325
  [data-variant='arrow']
296
- ) {
326
+ )::before {
297
327
  box-shadow: var(--shadow-xs) !important;
298
328
  }
299
- [data-warm-button][data-presentation='promptCategory']:hover {
329
+ [data-warm-button][data-presentation='promptCategory']:hover::before {
300
330
  background-color: var(--muted);
301
331
  }
302
332
  [data-warm-button][data-presentation='promptOption']:not([data-explicit-size]) {
@@ -313,9 +343,11 @@ button[data-warm-button][data-variant='apple'] {
313
343
  background-clip: border-box;
314
344
  }
315
345
  [data-warm-button][data-presentation='sidebarIcon']:hover {
316
- background-color: var(--sidebar-accent);
317
346
  color: var(--sidebar-accent-foreground);
318
347
  }
348
+ [data-warm-button][data-presentation='sidebarIcon']:hover::before {
349
+ background-color: var(--sidebar-accent);
350
+ }
319
351
  [data-warm-button][data-presentation='tableHeader']:not([data-explicit-size]) {
320
352
  height: auto;
321
353
  min-height: 0;
@@ -332,9 +364,11 @@ button[data-warm-button][data-variant='apple'] {
332
364
  color: var(--muted-foreground);
333
365
  }
334
366
  [data-warm-button][data-presentation='tableHeader']:hover {
335
- background-color: transparent;
336
367
  color: var(--foreground);
337
368
  }
369
+ [data-warm-button][data-presentation='tableHeader']:hover::before {
370
+ background-color: transparent;
371
+ }
338
372
  [data-warm-button][data-presentation='touch']:not([data-explicit-size]) {
339
373
  min-height: var(--button-min-height-touch);
340
374
  }
@@ -352,7 +386,7 @@ button[data-warm-button][data-variant='apple'] {
352
386
  font-size: var(--button-font-size);
353
387
  font-weight: 400;
354
388
  }
355
- [data-warm-button][data-presentation='threadNew']:hover {
389
+ [data-warm-button][data-presentation='threadNew']:hover::before {
356
390
  background-color: var(--muted);
357
391
  }
358
392
  [data-warm-button][data-presentation='compact']:not([data-explicit-size]) {
@@ -400,9 +434,6 @@ button[data-warm-button][data-variant='apple'] {
400
434
  height: auto;
401
435
  padding: 0;
402
436
  gap: var(--button-gap);
403
- background-color: transparent;
404
- background-image: none;
405
- box-shadow: none;
406
437
  border-radius: 0;
407
438
  color: var(--foreground);
408
439
  font-size: var(--button-font-size-base);
@@ -416,6 +447,11 @@ button[data-warm-button][data-variant='apple'] {
416
447
  line-height: var(--button-line-height-base);
417
448
  font-weight: 700;
418
449
  }
450
+ [data-warm-button][data-variant='arrow'][data-variant='arrow']::before {
451
+ background-color: transparent;
452
+ background-image: none;
453
+ box-shadow: none;
454
+ }
419
455
 
420
456
  /* SVG rules (from cva) */
421
457
  [data-warm-button] svg {
@@ -475,9 +511,12 @@ button[data-warm-button][data-variant='apple'] {
475
511
  }
476
512
  [data-warm-button][data-variant='ghost'][aria-expanded='true'],
477
513
  [data-warm-button][data-variant='outline'][aria-expanded='true'] {
478
- background-color: var(--muted);
479
514
  color: var(--foreground);
480
515
  }
516
+ [data-warm-button][data-variant='ghost'][aria-expanded='true']::before,
517
+ [data-warm-button][data-variant='outline'][aria-expanded='true']::before {
518
+ background-color: var(--muted);
519
+ }
481
520
 
482
521
  /* Disabled base + variant overrides (keep full shadow/border for some) */
483
522
  [data-warm-button]:disabled {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@warmio/design",
3
- "version": "1.7.0",
3
+ "version": "1.8.0",
4
4
  "description": "Warm's public design contract: CSS tokens and Button/Logo primitives.",
5
5
  "license": "MIT",
6
6
  "type": "module",
package/theme.css CHANGED
@@ -58,10 +58,10 @@
58
58
  SURFACES — From brand-neutral
59
59
  Backgrounds, cards, borders - subtle tinted neutrals.
60
60
  ══════════════════════════════════════════════════════════════════════════ */
61
- --background: oklch(1 0 0);
61
+ --background: rgba(230, 230, 230, 1);
62
62
  --foreground: oklch(0.147 0 0);
63
- --card: oklch(1 0 0);
64
- --card-gradient-start: oklch(1 0 0);
63
+ --card: rgba(240, 240, 240, 1);
64
+ --card-gradient-start: rgba(240, 240, 240, 1);
65
65
  --card-foreground: var(--foreground);
66
66
  --popover: var(--card);
67
67
  --popover-foreground: var(--foreground);
@@ -100,7 +100,7 @@
100
100
  /* ══════════════════════════════════════════════════════════════════════════
101
101
  SIDEBAR — Transparent to show body texture
102
102
  ══════════════════════════════════════════════════════════════════════════ */
103
- --sidebar: oklch(0.986 0 0);
103
+ --sidebar: rgba(230, 230, 230, 1);
104
104
  --sidebar-foreground: var(--foreground);
105
105
  --sidebar-primary: var(--primary);
106
106
  --sidebar-primary-foreground: var(--primary-foreground);
@@ -227,6 +227,8 @@
227
227
  --motion-distance-sm: 0.25rem;
228
228
  --motion-distance-md: 0.5rem;
229
229
  --motion-distance-lg: 1rem;
230
+ --button-press-inset: 1px;
231
+ --button-press-easing: cubic-bezier(0.23, 1, 0.32, 1);
230
232
 
231
233
  /* Aliases keep one scale; legacy names resolve to motion; chart uses slow (nonessential scale) */
232
234
  --transition-fast: var(--motion-duration-fast);
@@ -304,6 +306,7 @@
304
306
  --logo-letter-spacing: 0.025em;
305
307
  --logo-gap: 0.5rem;
306
308
  --button-transition-property: color, background-color, box-shadow, border-color, transform;
309
+ --button-surface-transition-property: inset, background-color, box-shadow;
307
310
  --button-transition-property-colors:
308
311
  color, background-color, border-color, outline-color, text-decoration-color, fill, stroke,
309
312
  --tw-gradient-from, --tw-gradient-via, --tw-gradient-to;
@@ -554,12 +557,12 @@
554
557
  ════════════════════════════════════════════════════════════════════════════ */
555
558
  .light {
556
559
  /* ─── Surfaces (light backgrounds from neutral) ─── */
557
- --background: oklch(0.99 0 0);
560
+ --background: rgba(230, 230, 230, 1);
558
561
  --foreground: oklch(0.35 0 0);
559
- --card: oklch(0.998 0 0);
560
- --card-gradient-start: oklch(1 0 0);
561
- --card-foreground: var(--foreground);
562
+ --card: rgba(240, 240, 240, 1);
563
+ --card-gradient-start: rgba(240, 240, 240, 1);
562
564
  --muted: oklch(0.917 0 0);
565
+ --sidebar: rgba(230, 230, 230, 1);
563
566
  --muted-foreground: oklch(0.532 0 0 / 0.8);
564
567
  --border: oklch(0.852 0 0);
565
568
 
@@ -796,5 +799,19 @@
796
799
  --shadow-lg: var(--shadow-lg);
797
800
  --shadow-xl: var(--shadow-xl);
798
801
  --shadow-2xl: var(--shadow-2xl);
799
- --shadow-card-edge: var(--shadow-border);
802
+ --shadow-card-edge:
803
+ var(--shadow-border), inset 0 1px rgba(255, 255, 255, 1), inset 1px 0 rgba(255, 255, 255, 0.5),
804
+ inset -1px 0 rgba(255, 255, 255, 0.5);
805
+ }
806
+
807
+ /* Dark cards use an outer highlight instead of the light inset highlight. */
808
+ .dark {
809
+ --shadow-card-edge:
810
+ var(--shadow-border), 0 1px rgba(255, 255, 255, 0.2), 1px 0 rgba(255, 255, 255, 0.1),
811
+ -1px 0 rgba(255, 255, 255, 0.1);
812
+ }
813
+
814
+ /* Dark cards use one simple outer edge highlight. */
815
+ .dark .shadow-card-edge {
816
+ --tw-shadow: 0 -1px 0 #333;
800
817
  }