@nqmcreative/ui 0.4.2 → 0.4.4

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/cli/index.mjs CHANGED
@@ -389,9 +389,11 @@ async function init() {
389
389
  `@import '@nqmcreative/ui/${style.name}/theme.css';`,
390
390
  ...(style.fonts ? [`@import '@nqmcreative/ui/${style.name}/fonts.css';`] : []),
391
391
  ``,
392
- `/* Tailwind v4 skips node_modules — point it at this style's folder so`,
393
- ` the class names used inside its components are generated. */`,
394
- `@source '${source}/styles/${style.name}';`
392
+ `/* Tailwind v4 skips node_modules — point it at this style's folder and`,
393
+ ` the shared core, so the class names used inside the components (tone`,
394
+ ` colours and the like live in core) are generated. */`,
395
+ `@source '${source}/styles/${style.name}';`,
396
+ `@source '${source}/core';`
395
397
  ];
396
398
 
397
399
  const css = await readOr(project.css);
@@ -63,7 +63,10 @@
63
63
  }: Props = $props();
64
64
 
65
65
  const base =
66
- 'inline-flex items-center justify-center gap-2 border font-sans font-medium whitespace-nowrap transition-[color,background-color,border-color,box-shadow,gap] duration-150 ease-brand-out disabled:opacity-50 disabled:pointer-events-none aria-disabled:opacity-50 aria-disabled:pointer-events-none';
66
+ // The press: matte can't sink or scale, so a click reads as the whole
67
+ // button dimming a notch (brightness, not a shadow or a transform).
68
+ // Snaps in at 75ms, settles back over the base 150ms on release.
69
+ 'inline-flex items-center justify-center gap-2 border font-sans font-medium whitespace-nowrap transition-[color,background-color,border-color,box-shadow,gap,filter] duration-150 ease-brand-out active:brightness-90 active:duration-75 disabled:opacity-50 disabled:pointer-events-none aria-disabled:opacity-50 aria-disabled:pointer-events-none';
67
70
 
68
71
  const sizes: Record<ButtonSize, string> = {
69
72
  sm: 'h-8 px-3.5 text-[13px]',
@@ -214,7 +214,7 @@
214
214
  use:clickOutside={{ onoutside: () => (open = false), ignore: [wrapper] }}
215
215
  id="{id ?? 'combobox'}-listbox"
216
216
  role="listbox"
217
- class="z-50 max-h-64 overflow-y-auto border border-hairline bg-bg py-1 font-sans"
217
+ class="z-50 max-h-64 animate-reveal overflow-y-auto border border-hairline bg-bg py-1 font-sans"
218
218
  >
219
219
  {#if flat.length === 0}
220
220
  <p class="px-3 py-6 text-center text-sm text-text-muted">
@@ -160,7 +160,7 @@
160
160
  onclick={(event) => {
161
161
  if (event.target === dialog) open = false;
162
162
  }}
163
- class="mx-auto mt-[12vh] mb-auto w-[min(36rem,calc(100vw-2rem))] max-w-none border border-hairline bg-bg p-0 text-text backdrop:bg-text/40 {className}"
163
+ class="mx-auto mt-[12vh] mb-auto w-[min(36rem,calc(100vw-2rem))] max-w-none animate-enter border border-hairline bg-bg p-0 text-text backdrop:bg-text/40 {className}"
164
164
  >
165
165
  <div use:focusTrap={{ autofocus: false }} onkeydown={onKeydown} role="none" class="flex flex-col">
166
166
  <div class="flex items-center gap-3 border-b border-hairline px-3.5">
@@ -78,7 +78,7 @@
78
78
  role="menu"
79
79
  aria-label={label ?? 'Menu'}
80
80
  tabindex="-1"
81
- class="z-50 flex max-h-[min(24rem,80vh)] min-w-44 flex-col overflow-y-auto border border-hairline bg-bg py-1 font-sans {menuClass}"
81
+ class="z-50 flex max-h-[min(24rem,80vh)] min-w-44 animate-reveal flex-col overflow-y-auto border border-hairline bg-bg py-1 font-sans {menuClass}"
82
82
  >
83
83
  {@render menu()}
84
84
  </div>
@@ -169,7 +169,7 @@
169
169
  use:anchored={{ anchor: wrapper, placement: 'bottom-start', offset: 4 }}
170
170
  use:clickOutside={{ onoutside: () => (open = false), ignore: [wrapper] }}
171
171
  id="{id ?? 'datepicker'}-calendar"
172
- class="z-50 border border-hairline bg-bg"
172
+ class="z-50 animate-reveal border border-hairline bg-bg"
173
173
  >
174
174
  <Calendar
175
175
  bind:value
@@ -84,7 +84,7 @@
84
84
  role="menu"
85
85
  aria-label={label ?? 'Menu'}
86
86
  tabindex="-1"
87
- class="z-50 flex max-h-[min(24rem,80vh)] min-w-44 flex-col overflow-y-auto border border-hairline bg-bg py-1 font-sans {className}"
87
+ class="z-50 flex max-h-[min(24rem,80vh)] min-w-44 animate-reveal flex-col overflow-y-auto border border-hairline bg-bg py-1 font-sans {className}"
88
88
  >
89
89
  {@render children()}
90
90
  </div>
@@ -73,7 +73,7 @@
73
73
  oncancel={(event) => {
74
74
  if (!dismissible) event.preventDefault();
75
75
  }}
76
- class="m-auto w-[calc(100%-2rem)] border border-hairline bg-bg p-0 text-text backdrop:bg-text/40 {sizes[
76
+ class="m-auto w-[calc(100%-2rem)] animate-enter border border-hairline bg-bg p-0 text-text backdrop:bg-text/40 {sizes[
77
77
  size
78
78
  ]} {className}"
79
79
  {...rest}
@@ -222,7 +222,7 @@
222
222
  id="{id ?? 'multiselect'}-listbox"
223
223
  role="listbox"
224
224
  aria-multiselectable="true"
225
- class="z-50 max-h-64 overflow-y-auto border border-hairline bg-bg py-1 font-sans"
225
+ class="z-50 max-h-64 animate-reveal overflow-y-auto border border-hairline bg-bg py-1 font-sans"
226
226
  >
227
227
  {#if flat.length === 0}
228
228
  <p class="px-3 py-6 text-center text-sm text-text-muted">
@@ -77,7 +77,7 @@
77
77
  role="dialog"
78
78
  aria-label={title}
79
79
  tabindex="-1"
80
- class="z-50 flex max-w-xs flex-col gap-2 border border-hairline bg-bg p-4 font-sans {className}"
80
+ class="z-50 flex max-w-xs animate-reveal flex-col gap-2 border border-hairline bg-bg p-4 font-sans {className}"
81
81
  >
82
82
  {#if title}
83
83
  <p class="font-heading text-sm font-medium text-text">{title}</p>
@@ -78,9 +78,37 @@
78
78
  --font-heading: 'Sora', ui-sans-serif, system-ui, sans-serif;
79
79
  --font-mono: 'JetBrains Mono', 'Cascadia Code', 'Fira Code', monospace;
80
80
 
81
- /* --- motion --- */
81
+ /* --- motion ---
82
+ * matte enters by fading and revealing along an axis — never by moving or
83
+ * scaling (the style's first law). One token per overlay kind: `enter` for
84
+ * dialog panels + backdrops, `reveal` for anchored menus and popovers.
85
+ * paper and sprout define these same two names their own way, so an
86
+ * overlay's markup never changes between styles — only these values do.
87
+ */
82
88
  --ease-brand: cubic-bezier(0.16, 1, 0.3, 1);
83
89
  --ease-brand-out: cubic-bezier(0.25, 1, 0.5, 1);
90
+ --animate-enter: nqm-fade 150ms var(--ease-brand-out);
91
+ --animate-reveal: nqm-shutter 160ms var(--ease-brand-out);
92
+ }
93
+
94
+ /* Enter-only, matching the Drawer: the native <dialog> and the {#if}-mounted
95
+ * popover each play these once on mount and leave instantly. */
96
+ @keyframes nqm-fade {
97
+ from {
98
+ opacity: 0;
99
+ }
100
+ }
101
+ @keyframes nqm-shutter {
102
+ from {
103
+ opacity: 0;
104
+ clip-path: inset(0 0 100% 0);
105
+ }
106
+ to {
107
+ opacity: 1;
108
+ /* Both ends must be inset() — animating to `none` (the default) can't
109
+ * interpolate and the panel would snap open at the midpoint instead. */
110
+ clip-path: inset(0 0 0 0);
111
+ }
84
112
  }
85
113
 
86
114
  /*
@@ -218,6 +246,20 @@
218
246
  * a wash — the text underneath keeps its own colour and its own contrast.
219
247
  */
220
248
  @layer base {
249
+ /* The one overlay surface a class can't reach: every <dialog> backdrop
250
+ * fades with its panel. */
251
+ dialog::backdrop {
252
+ animation: nqm-fade 150ms var(--ease-brand-out);
253
+ }
254
+
255
+ @media (prefers-reduced-motion: reduce) {
256
+ .animate-enter,
257
+ .animate-reveal,
258
+ dialog::backdrop {
259
+ animation: none;
260
+ }
261
+ }
262
+
221
263
  ::selection {
222
264
  background-color: color-mix(in oklab, var(--color-brand) 24%, transparent);
223
265
  }
@@ -4,7 +4,7 @@
4
4
  import { toneText, type Tone } from '../../core/tones.js';
5
5
  import { iconMd } from './icon.js';
6
6
 
7
- interface Props extends HTMLAttributes<HTMLDetailsElement> {
7
+ interface Props extends HTMLAttributes<HTMLDivElement> {
8
8
  title: string;
9
9
  open?: boolean;
10
10
  tone?: Tone;
@@ -25,27 +25,54 @@
25
25
  children,
26
26
  ...rest
27
27
  }: Props = $props();
28
+
29
+ const uid = $props.id();
28
30
  </script>
29
31
 
30
- <details bind:open class="group border-b border-hairline last:border-b-0 {className}" {...rest}>
31
- <summary
32
- class="flex cursor-pointer list-none items-center gap-4 px-4 py-3.5 font-sans text-sm font-medium text-text transition-colors duration-150 ease-brand-out select-none hover:bg-bg-alt focus-visible:outline-2 focus-visible:outline-offset-[-2px] focus-visible:outline-current [&::-webkit-details-marker]:hidden"
32
+ <!--
33
+ A disclosure, not a native <details>: the panel is a grid whose single row
34
+ animates from 0fr to 1fr, which is the one height animation that works in
35
+ every browser (auto-height keyframes don't interpolate). The <button> keeps
36
+ the keyboard and focus behaviour <summary> gave us for free; aria-expanded
37
+ and the region wiring replace what the open attribute carried.
38
+ -->
39
+ <div class="border-b border-hairline last:border-b-0 {className}" {...rest}>
40
+ <button
41
+ type="button"
42
+ id="{uid}-trigger"
43
+ aria-expanded={open}
44
+ aria-controls="{uid}-panel"
45
+ onclick={() => (open = !open)}
46
+ class="flex w-full cursor-pointer items-center gap-4 px-4 py-3.5 text-left font-sans text-sm font-medium text-text transition-colors duration-150 ease-brand-out select-none hover:bg-bg-alt focus-visible:outline-2 focus-visible:outline-offset-[-2px] focus-visible:outline-current"
33
47
  >
34
48
  {#if icon}<span class={iconMd}>{@render icon()}</span>{/if}
35
49
  <span class="flex-1">{title}</span>
36
50
  {#if meta}<span class="text-[13px] font-normal text-text-muted">{meta}</span>{/if}
37
51
  <svg
38
- class="size-4 shrink-0 transition-transform duration-200 ease-brand-out group-open:rotate-180 {toneText[
39
- tone
40
- ]}"
52
+ class="size-4 shrink-0 transition-transform duration-200 ease-brand-out {open
53
+ ? 'rotate-180'
54
+ : ''} {toneText[tone]}"
41
55
  viewBox="0 0 16 16"
42
56
  fill="none"
43
57
  aria-hidden="true"
44
58
  >
45
59
  <path d="m4 6 4 4 4-4" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" />
46
60
  </svg>
47
- </summary>
48
- <div class="px-4 pb-4 font-sans text-sm leading-relaxed text-text-secondary">
49
- {@render children()}
61
+ </button>
62
+ <div
63
+ id="{uid}-panel"
64
+ role="region"
65
+ aria-labelledby="{uid}-trigger"
66
+ inert={!open}
67
+ class="grid transition-[grid-template-rows] duration-200 ease-brand-out"
68
+ style:grid-template-rows={open ? '1fr' : '0fr'}
69
+ >
70
+ <!-- overflow-hidden gives this grid row a min height of 0 so 0fr can
71
+ collapse it fully, and clips the content while it slides. -->
72
+ <div class="overflow-hidden">
73
+ <div class="px-4 pb-4 font-sans text-sm leading-relaxed text-text-secondary">
74
+ {@render children()}
75
+ </div>
76
+ </div>
50
77
  </div>
51
- </details>
78
+ </div>
@@ -1,7 +1,7 @@
1
1
  import type { Snippet } from 'svelte';
2
2
  import type { HTMLAttributes } from 'svelte/elements';
3
3
  import { type Tone } from '../../core/tones.js';
4
- interface Props extends HTMLAttributes<HTMLDetailsElement> {
4
+ interface Props extends HTMLAttributes<HTMLDivElement> {
5
5
  title: string;
6
6
  open?: boolean;
7
7
  tone?: Tone;
@@ -63,7 +63,10 @@
63
63
  }: Props = $props();
64
64
 
65
65
  const base =
66
- 'inline-flex items-center justify-center gap-2 rounded-md border font-sans font-medium whitespace-nowrap transition-colors duration-150 ease-brand-out disabled:opacity-50 disabled:pointer-events-none aria-disabled:opacity-50 aria-disabled:pointer-events-none';
66
+ // The press: shadow is paper's language, so a click flattens the button
67
+ // onto the page (shadow drops) and dims it a notch. Snaps in at 75ms,
68
+ // settles back over the base 150ms on release.
69
+ 'inline-flex items-center justify-center gap-2 rounded-md border font-sans font-medium whitespace-nowrap transition-[color,background-color,border-color,box-shadow,filter] duration-150 ease-brand-out active:shadow-none active:brightness-95 active:duration-75 disabled:opacity-50 disabled:pointer-events-none aria-disabled:opacity-50 aria-disabled:pointer-events-none';
67
70
 
68
71
  const sizes: Record<ButtonSize, string> = {
69
72
  sm: 'h-8 px-3 text-[13px]',
@@ -219,7 +219,7 @@
219
219
  use:clickOutside={{ onoutside: () => (open = false), ignore: [wrapper] }}
220
220
  id="{id ?? 'combobox'}-listbox"
221
221
  role="listbox"
222
- class="z-50 max-h-64 overflow-y-auto rounded-lg border border-hairline bg-bg py-1 font-sans shadow-lg"
222
+ class="z-50 max-h-64 animate-reveal overflow-y-auto rounded-lg border border-hairline bg-bg py-1 font-sans shadow-lg"
223
223
  >
224
224
  {#if flat.length === 0}
225
225
  <p class="px-3 py-6 text-center text-sm text-text-muted">
@@ -160,7 +160,7 @@
160
160
  onclick={(event) => {
161
161
  if (event.target === dialog) open = false;
162
162
  }}
163
- class="mx-auto mt-[12vh] mb-auto w-[min(36rem,calc(100vw-2rem))] max-w-none rounded-xl border border-hairline bg-bg p-0 text-text shadow-2xl backdrop:bg-text/30 backdrop:backdrop-blur-[1px] {className}"
163
+ class="mx-auto mt-[12vh] mb-auto w-[min(36rem,calc(100vw-2rem))] max-w-none animate-enter rounded-xl border border-hairline bg-bg p-0 text-text shadow-2xl backdrop:bg-text/30 backdrop:backdrop-blur-[1px] {className}"
164
164
  >
165
165
  <div use:focusTrap={{ autofocus: false }} onkeydown={onKeydown} role="none" class="flex flex-col">
166
166
  <div class="flex items-center gap-3 border-b border-hairline px-4">
@@ -78,7 +78,7 @@
78
78
  role="menu"
79
79
  aria-label={label ?? 'Menu'}
80
80
  tabindex="-1"
81
- class="z-50 flex max-h-[min(24rem,80vh)] min-w-48 flex-col overflow-y-auto rounded-lg border border-hairline bg-bg py-1 font-sans shadow-lg {menuClass}"
81
+ class="z-50 flex max-h-[min(24rem,80vh)] min-w-48 animate-reveal flex-col overflow-y-auto rounded-lg border border-hairline bg-bg py-1 font-sans shadow-lg {menuClass}"
82
82
  >
83
83
  {@render menu()}
84
84
  </div>
@@ -179,7 +179,7 @@
179
179
  use:anchored={{ anchor: wrapper, placement: 'bottom-start', offset: 4 }}
180
180
  use:clickOutside={{ onoutside: () => (open = false), ignore: [wrapper] }}
181
181
  id="{id ?? 'datepicker'}-calendar"
182
- class="z-50 rounded-lg border border-hairline bg-bg shadow-lg"
182
+ class="z-50 animate-reveal rounded-lg border border-hairline bg-bg shadow-lg"
183
183
  >
184
184
  <Calendar
185
185
  bind:value
@@ -84,7 +84,7 @@
84
84
  role="menu"
85
85
  aria-label={label ?? 'Menu'}
86
86
  tabindex="-1"
87
- class="z-50 flex max-h-[min(24rem,80vh)] min-w-48 flex-col overflow-y-auto rounded-lg border border-hairline bg-bg py-1 font-sans shadow-lg {className}"
87
+ class="z-50 flex max-h-[min(24rem,80vh)] min-w-48 animate-reveal flex-col overflow-y-auto rounded-lg border border-hairline bg-bg py-1 font-sans shadow-lg {className}"
88
88
  >
89
89
  {@render children()}
90
90
  </div>
@@ -73,7 +73,7 @@
73
73
  oncancel={(event) => {
74
74
  if (!dismissible) event.preventDefault();
75
75
  }}
76
- class="m-auto w-[calc(100%-2rem)] rounded-xl border border-hairline bg-bg p-0 text-text shadow-2xl backdrop:bg-text/30 backdrop:backdrop-blur-[1px] {sizes[
76
+ class="m-auto w-[calc(100%-2rem)] animate-enter rounded-xl border border-hairline bg-bg p-0 text-text shadow-2xl backdrop:bg-text/30 backdrop:backdrop-blur-[1px] {sizes[
77
77
  size
78
78
  ]} {className}"
79
79
  {...rest}
@@ -232,7 +232,7 @@
232
232
  id="{id ?? 'multiselect'}-listbox"
233
233
  role="listbox"
234
234
  aria-multiselectable="true"
235
- class="z-50 max-h-64 overflow-y-auto rounded-lg border border-hairline bg-bg py-1 font-sans shadow-lg"
235
+ class="z-50 max-h-64 animate-reveal overflow-y-auto rounded-lg border border-hairline bg-bg py-1 font-sans shadow-lg"
236
236
  >
237
237
  {#if flat.length === 0}
238
238
  <p class="px-3 py-6 text-center text-sm text-text-muted">
@@ -77,7 +77,7 @@
77
77
  role="dialog"
78
78
  aria-label={title}
79
79
  tabindex="-1"
80
- class="z-50 flex max-w-xs flex-col gap-2 rounded-lg border border-hairline bg-bg p-4 font-sans shadow-lg {className}"
80
+ class="z-50 flex max-w-xs animate-reveal flex-col gap-2 rounded-lg border border-hairline bg-bg p-4 font-sans shadow-lg {className}"
81
81
  >
82
82
  {#if title}
83
83
  <p class="font-heading text-sm font-semibold text-text">{title}</p>
@@ -29,10 +29,10 @@
29
29
  --color-text-inverse: #ffffff;
30
30
 
31
31
  /* --- brand / primary (indigo) --- */
32
- --color-brand: #4f46e5;
33
- --color-brand-hover: #4338ca;
34
- --color-brand-light: #eef2ff;
35
- --color-brand-border: #c7d2fe;
32
+ --color-brand: #15803d;
33
+ --color-brand-hover: #166534;
34
+ --color-brand-light: #e8f6ee;
35
+ --color-brand-border: #a7d9b9;
36
36
 
37
37
  /* --- accent (rose) --- */
38
38
  --color-accent: #e11d48;
@@ -95,9 +95,46 @@
95
95
  'Plus Jakarta Sans', ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;
96
96
  --font-mono: ui-monospace, SFMono-Regular, Menlo, Consolas, 'Liberation Mono', monospace;
97
97
 
98
- /* --- motion --- */
98
+ /* --- motion ---
99
+ * paper enters by rising and fading — depth is its language, so overlays
100
+ * settle onto the page rather than snapping or scaling in. One token per
101
+ * overlay kind: `enter` for dialog panels + backdrops, `reveal` for anchored
102
+ * menus and popovers. matte and sprout define these same two names their own
103
+ * way, so an overlay's markup never changes between styles — only the values.
104
+ */
99
105
  --ease-brand: cubic-bezier(0.4, 0, 0.2, 1);
100
106
  --ease-brand-out: cubic-bezier(0, 0, 0.2, 1);
107
+ --animate-enter: nqm-rise 200ms var(--ease-brand-out);
108
+ --animate-reveal: nqm-drop 180ms var(--ease-brand-out);
109
+ }
110
+
111
+ /* Enter-only, matching the Drawer: the native <dialog> and the {#if}-mounted
112
+ * popover each play these once on mount and leave instantly. */
113
+ @keyframes nqm-fade {
114
+ from {
115
+ opacity: 0;
116
+ }
117
+ }
118
+ @keyframes nqm-rise {
119
+ from {
120
+ opacity: 0;
121
+ /* dialogs rise and grow a hair — a gentle zoom onto the page */
122
+ transform: translateY(8px) scale(0.98);
123
+ /* one layer, matching the panel's shadow-2xl, so box-shadow interpolates
124
+ * to the resting shadow instead of snapping (a mismatched layer count is
125
+ * discrete). The shadow deepens as the panel settles. */
126
+ box-shadow: 0 8px 20px -12px rgb(0 0 0 / 0.18);
127
+ }
128
+ }
129
+ @keyframes nqm-drop {
130
+ from {
131
+ opacity: 0;
132
+ transform: translateY(-6px);
133
+ /* two layers, matching the menus' shadow-lg — same reason as above */
134
+ box-shadow:
135
+ 0 2px 6px -3px rgb(0 0 0 / 0.08),
136
+ 0 1px 3px -2px rgb(0 0 0 / 0.06);
137
+ }
101
138
  }
102
139
 
103
140
  /*
@@ -126,10 +163,10 @@
126
163
  --color-text-muted: #9ca3af;
127
164
  --color-text-inverse: #0b0f19;
128
165
 
129
- --color-brand: #818cf8;
130
- --color-brand-hover: #a5b4fc;
131
- --color-brand-light: #1e1b4b;
132
- --color-brand-border: #3730a3;
166
+ --color-brand: #46a06a;
167
+ --color-brand-hover: #5cb47e;
168
+ --color-brand-light: #12281b;
169
+ --color-brand-border: #255034;
133
170
 
134
171
  --color-accent: #fb7185;
135
172
  --color-accent-hover: #fda4af;
@@ -182,10 +219,10 @@
182
219
  --color-text-muted: #9ca3af;
183
220
  --color-text-inverse: #0b0f19;
184
221
 
185
- --color-brand: #818cf8;
186
- --color-brand-hover: #a5b4fc;
187
- --color-brand-light: #1e1b4b;
188
- --color-brand-border: #3730a3;
222
+ --color-brand: #46a06a;
223
+ --color-brand-hover: #5cb47e;
224
+ --color-brand-light: #12281b;
225
+ --color-brand-border: #255034;
189
226
 
190
227
  --color-accent: #fb7185;
191
228
  --color-accent-hover: #fda4af;
@@ -234,6 +271,20 @@
234
271
  * a wash — the text underneath keeps its own colour and its own contrast.
235
272
  */
236
273
  @layer base {
274
+ /* The one overlay surface a class can't reach: every <dialog> backdrop
275
+ * fades with its panel. */
276
+ dialog::backdrop {
277
+ animation: nqm-fade 180ms var(--ease-brand-out);
278
+ }
279
+
280
+ @media (prefers-reduced-motion: reduce) {
281
+ .animate-enter,
282
+ .animate-reveal,
283
+ dialog::backdrop {
284
+ animation: none;
285
+ }
286
+ }
287
+
237
288
  ::selection {
238
289
  background-color: color-mix(in oklab, var(--color-brand) 24%, transparent);
239
290
  }
@@ -129,6 +129,24 @@
129
129
  */
130
130
  --ease-brand: cubic-bezier(0.34, 1.4, 0.64, 1);
131
131
  --ease-brand-out: cubic-bezier(0.22, 1, 0.36, 1);
132
+ /* Overlay entrances — sprout pops in with a little overshoot, on the same
133
+ * `enter`/`reveal` token names matte and paper use. Declared here so the
134
+ * three styles stay in token-name parity; sprout's overlay components adopt
135
+ * them in a later pass. */
136
+ --animate-enter: nqm-pop 240ms var(--ease-brand);
137
+ --animate-reveal: nqm-pop 220ms var(--ease-brand);
138
+ }
139
+
140
+ @keyframes nqm-fade {
141
+ from {
142
+ opacity: 0;
143
+ }
144
+ }
145
+ @keyframes nqm-pop {
146
+ from {
147
+ opacity: 0;
148
+ transform: scale(0.96);
149
+ }
132
150
  }
133
151
 
134
152
  /*
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nqmcreative/ui",
3
- "version": "0.4.2",
3
+ "version": "0.4.4",
4
4
  "description": "Svelte 5 + Tailwind CSS v4 component library — three styles (matte, paper, sprout), 70 components each, one shared behaviour core. No runtime dependencies.",
5
5
  "scripts": {
6
6
  "dev": "vite dev",