@vegastack/design 0.3.2 → 0.4.1

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,350 @@
1
+ "use client";
2
+ import {
3
+ cn
4
+ } from "./chunk-DS7SBEDS.js";
5
+
6
+ // src/icons/create-animated-icon.tsx
7
+ import * as React from "react";
8
+ import {
9
+ AnimatePresence,
10
+ MotionConfigContext,
11
+ motion,
12
+ useAnimation
13
+ } from "motion/react";
14
+ import { jsx, jsxs } from "react/jsx-runtime";
15
+ var REDUCED_MOTION_QUERY = "(prefers-reduced-motion: reduce)";
16
+ function subscribeToReducedMotion(onStoreChange) {
17
+ if (typeof window === "undefined" || typeof window.matchMedia !== "function")
18
+ return () => {
19
+ };
20
+ const query = window.matchMedia(REDUCED_MOTION_QUERY);
21
+ query.addEventListener("change", onStoreChange);
22
+ return () => query.removeEventListener("change", onStoreChange);
23
+ }
24
+ function readReducedMotion() {
25
+ if (typeof window === "undefined" || typeof window.matchMedia !== "function")
26
+ return false;
27
+ return window.matchMedia(REDUCED_MOTION_QUERY).matches;
28
+ }
29
+ function readReducedMotionOnServer() {
30
+ return false;
31
+ }
32
+ function useLiveReducedMotion() {
33
+ const preference = React.useSyncExternalStore(
34
+ subscribeToReducedMotion,
35
+ readReducedMotion,
36
+ readReducedMotionOnServer
37
+ );
38
+ const { reducedMotion } = React.useContext(MotionConfigContext);
39
+ if (reducedMotion === "always") return true;
40
+ return preference;
41
+ }
42
+ var TAGS = {
43
+ circle: "circle",
44
+ defs: "defs",
45
+ ellipse: "ellipse",
46
+ g: "g",
47
+ line: "line",
48
+ path: "path",
49
+ pattern: "pattern",
50
+ polygon: "polygon",
51
+ polyline: "polyline",
52
+ rect: "rect",
53
+ text: "text",
54
+ "motion.circle": motion.circle,
55
+ "motion.ellipse": motion.ellipse,
56
+ "motion.g": motion.g,
57
+ "motion.line": motion.line,
58
+ "motion.path": motion.path,
59
+ "motion.pattern": motion.pattern,
60
+ "motion.polygon": motion.polygon,
61
+ "motion.polyline": motion.polyline,
62
+ "motion.rect": motion.rect,
63
+ "motion.text": motion.text
64
+ };
65
+ var ANIMATED_ICON_RESERVED_KEYS = /* @__PURE__ */ new Set([
66
+ "animate",
67
+ "children",
68
+ "className",
69
+ "custom",
70
+ "exit",
71
+ "group",
72
+ "inherit",
73
+ "initial",
74
+ "style",
75
+ "tag",
76
+ "text",
77
+ "transition",
78
+ "variants"
79
+ ]);
80
+ var ROOT_DEFAULTS = {
81
+ viewBox: "0 0 24 24",
82
+ fill: "none",
83
+ stroke: "currentColor",
84
+ strokeWidth: "2",
85
+ strokeLinecap: "round",
86
+ strokeLinejoin: "round"
87
+ };
88
+ function isPresence(node) {
89
+ return typeof node === "object" && node.tag === "presence";
90
+ }
91
+ function isMotionTag(tag) {
92
+ return tag.startsWith("motion.");
93
+ }
94
+ function asNode(child) {
95
+ return typeof child === "string" ? { tag: "path", d: child } : child;
96
+ }
97
+ function specUsesControls(spec) {
98
+ if (spec.start || spec.stop) return true;
99
+ if (spec.svg?.tag === "motion.svg" && !spec.svg.inherit) return true;
100
+ const walk = (nodes) => nodes.some((child) => {
101
+ if (isPresence(child)) return false;
102
+ const node = asNode(child);
103
+ if (isMotionTag(node.tag) && !node.inherit && !node.animate) return true;
104
+ return node.children ? walk(node.children) : false;
105
+ });
106
+ return walk(spec.elements);
107
+ }
108
+ function hasPresenceNode(spec) {
109
+ return spec.elements.some(isPresence);
110
+ }
111
+ function svgAttributes(node) {
112
+ const attributes = {};
113
+ for (const [key, value] of Object.entries(node)) {
114
+ if (ANIMATED_ICON_RESERVED_KEYS.has(key)) continue;
115
+ if (value !== void 0) attributes[key] = value;
116
+ }
117
+ return attributes;
118
+ }
119
+ function renderNode(child, key, controls) {
120
+ const node = asNode(child);
121
+ const Tag = TAGS[node.tag];
122
+ const motionProps = isMotionTag(node.tag) ? {
123
+ animate: node.animate ?? (node.inherit ? void 0 : controls[node.group ?? "default"]),
124
+ initial: node.initial,
125
+ exit: node.exit,
126
+ variants: node.variants,
127
+ transition: node.transition,
128
+ custom: node.custom
129
+ } : void 0;
130
+ return /* @__PURE__ */ jsxs(
131
+ Tag,
132
+ {
133
+ ...svgAttributes(node),
134
+ className: node.className,
135
+ style: node.style,
136
+ ...motionProps,
137
+ children: [
138
+ node.text,
139
+ node.children?.map(
140
+ (grandchild, index) => renderNode(grandchild, index, controls)
141
+ )
142
+ ]
143
+ },
144
+ key
145
+ );
146
+ }
147
+ function createAnimatedIcon(spec) {
148
+ const groupNames = spec.groups ?? ["default"];
149
+ const usesControls = specUsesControls(spec);
150
+ const usesPresence = hasPresenceNode(spec);
151
+ const root = spec.svg ?? {};
152
+ const rootTag = root.tag ?? "svg";
153
+ const RootTag = rootTag === "motion.svg" ? motion.svg : "svg";
154
+ function AnimatedIconComponent({
155
+ className,
156
+ size = "var(--icon-default)",
157
+ ref,
158
+ onPointerEnter,
159
+ onPointerLeave,
160
+ onPointerDown,
161
+ onFocus,
162
+ onBlur,
163
+ ...props
164
+ }) {
165
+ const controlList = groupNames.map(() => useAnimation());
166
+ const shouldReduceMotion = useLiveReducedMotion();
167
+ const [isActive, setIsActive] = React.useState(false);
168
+ const reduceRef = React.useRef(shouldReduceMotion);
169
+ reduceRef.current = shouldReduceMotion;
170
+ const isControlledRef = React.useRef(false);
171
+ const timersRef = React.useRef([]);
172
+ const controls = React.useMemo(
173
+ () => Object.fromEntries(
174
+ groupNames.map((name, index) => [name, controlList[index]])
175
+ ),
176
+ // Each entry is referentially stable for the life of the component.
177
+ // eslint-disable-next-line react-hooks/exhaustive-deps
178
+ controlList
179
+ );
180
+ const clearTimers = React.useCallback(() => {
181
+ for (const timer of timersRef.current) clearTimeout(timer);
182
+ timersRef.current = [];
183
+ }, []);
184
+ React.useEffect(() => clearTimers, [clearTimers]);
185
+ const run = React.useCallback(
186
+ (control2, definition, transition) => {
187
+ if (reduceRef.current) {
188
+ control2.stop();
189
+ return Promise.resolve();
190
+ }
191
+ return Promise.resolve(control2.start(definition, transition)).then(
192
+ () => void 0,
193
+ () => void 0
194
+ );
195
+ },
196
+ []
197
+ );
198
+ const reset = React.useCallback(
199
+ (control2, definition, transition) => {
200
+ if (reduceRef.current) {
201
+ control2.stop();
202
+ control2.set(definition);
203
+ return Promise.resolve();
204
+ }
205
+ return Promise.resolve(control2.start(definition, transition)).then(
206
+ () => void 0,
207
+ () => void 0
208
+ );
209
+ },
210
+ []
211
+ );
212
+ const set = React.useCallback(
213
+ (control2, definition) => {
214
+ control2.stop();
215
+ control2.set(definition);
216
+ },
217
+ []
218
+ );
219
+ const after = React.useCallback(
220
+ (delayMs, task) => {
221
+ timersRef.current.push(setTimeout(task, delayMs));
222
+ },
223
+ []
224
+ );
225
+ const flagsRef = React.useRef({});
226
+ const control = React.useCallback(
227
+ (group) => {
228
+ const index = group ? groupNames.indexOf(group) : 0;
229
+ return controlList[index === -1 ? 0 : index];
230
+ },
231
+ // eslint-disable-next-line react-hooks/exhaustive-deps -- stable entries
232
+ controlList
233
+ );
234
+ const choreography = React.useMemo(
235
+ () => ({
236
+ control,
237
+ shouldReduceMotion,
238
+ run,
239
+ reset,
240
+ set,
241
+ after,
242
+ flags: flagsRef.current
243
+ }),
244
+ [control, shouldReduceMotion, run, reset, set, after]
245
+ );
246
+ const primaryControl = control();
247
+ const startAnimation = React.useCallback(() => {
248
+ clearTimers();
249
+ if (usesPresence) setIsActive(!reduceRef.current);
250
+ if (spec.start) {
251
+ void spec.start(choreography);
252
+ return;
253
+ }
254
+ if (usesControls) void run(primaryControl, "animate");
255
+ }, [choreography, clearTimers, run, primaryControl]);
256
+ const stopAnimation = React.useCallback(() => {
257
+ clearTimers();
258
+ if (usesPresence) setIsActive(false);
259
+ if (spec.stop) {
260
+ void spec.stop(choreography);
261
+ return;
262
+ }
263
+ if (usesControls) void reset(primaryControl, "normal");
264
+ }, [choreography, clearTimers, reset, primaryControl]);
265
+ React.useImperativeHandle(ref, () => {
266
+ isControlledRef.current = true;
267
+ return { startAnimation, stopAnimation };
268
+ });
269
+ React.useEffect(() => {
270
+ if (shouldReduceMotion) stopAnimation();
271
+ }, [shouldReduceMotion, stopAnimation]);
272
+ const handlePointerEnter = (event) => {
273
+ onPointerEnter?.(event);
274
+ if (!isControlledRef.current && event.pointerType !== "touch")
275
+ startAnimation();
276
+ };
277
+ const handlePointerLeave = (event) => {
278
+ onPointerLeave?.(event);
279
+ if (!isControlledRef.current && event.pointerType !== "touch")
280
+ stopAnimation();
281
+ };
282
+ const handlePointerDown = (event) => {
283
+ onPointerDown?.(event);
284
+ if (!isControlledRef.current && event.pointerType === "touch")
285
+ startAnimation();
286
+ };
287
+ const handleFocus = (event) => {
288
+ onFocus?.(event);
289
+ if (!isControlledRef.current) startAnimation();
290
+ };
291
+ const handleBlur = (event) => {
292
+ onBlur?.(event);
293
+ if (!isControlledRef.current) stopAnimation();
294
+ };
295
+ const rootMotionProps = rootTag === "motion.svg" ? {
296
+ animate: root.animate ?? (root.inherit ? void 0 : controls[root.group ?? "default"]),
297
+ initial: root.initial,
298
+ variants: root.variants,
299
+ transition: root.transition,
300
+ custom: root.custom
301
+ } : void 0;
302
+ return /* @__PURE__ */ jsx(
303
+ "span",
304
+ {
305
+ className: cn(
306
+ "inline-flex shrink-0 items-center justify-center",
307
+ className
308
+ ),
309
+ onPointerEnter: handlePointerEnter,
310
+ onPointerLeave: handlePointerLeave,
311
+ onPointerDown: handlePointerDown,
312
+ onFocus: handleFocus,
313
+ onBlur: handleBlur,
314
+ ...props,
315
+ children: /* @__PURE__ */ jsx(
316
+ RootTag,
317
+ {
318
+ xmlns: "http://www.w3.org/2000/svg",
319
+ width: size,
320
+ height: size,
321
+ viewBox: root.viewBox ?? ROOT_DEFAULTS.viewBox,
322
+ fill: root.fill ?? ROOT_DEFAULTS.fill,
323
+ stroke: root.stroke ?? ROOT_DEFAULTS.stroke,
324
+ strokeWidth: root.strokeWidth ?? ROOT_DEFAULTS.strokeWidth,
325
+ strokeLinecap: root.strokeLinecap ?? ROOT_DEFAULTS.strokeLinecap,
326
+ strokeLinejoin: root.strokeLinejoin ?? ROOT_DEFAULTS.strokeLinejoin,
327
+ overflow: root.overflow,
328
+ className: root.className,
329
+ style: root.style,
330
+ ...rootMotionProps,
331
+ children: spec.elements.map((node, index) => {
332
+ if (!isPresence(node)) return renderNode(node, index, controls);
333
+ return /* @__PURE__ */ jsx(AnimatePresence, { initial: false, mode: "wait", children: isActive ? /* @__PURE__ */ jsx(React.Fragment, { children: node.active.map(
334
+ (child, childIndex) => renderNode(child, childIndex, controls)
335
+ ) }, `${spec.name}-active`) : /* @__PURE__ */ jsx(React.Fragment, { children: node.rest.map(
336
+ (child, childIndex) => renderNode(child, childIndex, controls)
337
+ ) }, `${spec.name}-rest`) }, index);
338
+ })
339
+ }
340
+ )
341
+ }
342
+ );
343
+ }
344
+ AnimatedIconComponent.displayName = spec.name;
345
+ return AnimatedIconComponent;
346
+ }
347
+ export {
348
+ ANIMATED_ICON_RESERVED_KEYS,
349
+ createAnimatedIcon
350
+ };
@@ -83,6 +83,32 @@ var twMerge = (0, import_tailwind_merge.extendTailwindMerge)({
83
83
  function cn(...inputs) {
84
84
  return twMerge((0, import_clsx.clsx)(inputs));
85
85
  }
86
+ var fieldControl = [
87
+ "rounded-md border border-input bg-transparent dark:bg-input/(--alpha-input)",
88
+ "placeholder:text-muted-foreground-faint",
89
+ "not-disabled:not-data-disabled:hover:border-foreground/(--alpha-border-subtle)",
90
+ "focus:border-ring/(--alpha-tint-border)",
91
+ "not-focus:aria-invalid:border-destructive-border/(--alpha-tint-border)",
92
+ "not-focus:data-invalid:border-destructive-border/(--alpha-tint-border)",
93
+ "disabled:cursor-not-allowed disabled:bg-surface-1 disabled:opacity-(--opacity-dim)",
94
+ "data-disabled:cursor-not-allowed data-disabled:bg-surface-1 data-disabled:opacity-(--opacity-dim)"
95
+ ].join(" ");
96
+ var fieldControlGroup = [
97
+ "rounded-md border border-input bg-transparent dark:bg-input/(--alpha-input)",
98
+ "not-has-disabled:not-data-disabled:hover:border-foreground/(--alpha-border-subtle)",
99
+ "focus-within:border-ring/(--alpha-tint-border)",
100
+ "data-focused:border-ring/(--alpha-tint-border)",
101
+ // Both the SELF and the DESCENDANT form of the invalid tint (2026-09-09). `has-aria-invalid:` is
102
+ // a `:has()` over descendants, so a group that carries `aria-invalid` on ITSELF — which is where
103
+ // `<NumberField aria-invalid />` lands the attribute, on the `[data-field-group]` element —
104
+ // matched nothing and measured the neutral `--input` border. Inside a `Field` the state arrives
105
+ // as `data-invalid` on the group and was always correct; this is the standalone path.
106
+ "not-focus-within:aria-invalid:border-destructive-border/(--alpha-tint-border)",
107
+ "not-focus-within:has-aria-invalid:border-destructive-border/(--alpha-tint-border)",
108
+ "not-focus-within:data-invalid:border-destructive-border/(--alpha-tint-border)",
109
+ "has-disabled:cursor-not-allowed has-disabled:bg-surface-1 has-disabled:opacity-(--opacity-dim)",
110
+ "data-disabled:cursor-not-allowed data-disabled:bg-surface-1 data-disabled:opacity-(--opacity-dim)"
111
+ ].join(" ");
86
112
 
87
113
  // src/icons/brand-icon.tsx
88
114
  var import_jsx_runtime2 = require("react/jsx-runtime");
@@ -110,15 +110,18 @@ interface AnimatedIconHandle {
110
110
  }
111
111
  /**
112
112
  * Shape of a mirrored `lucide-animated` icon component: a React 19 function
113
- * component that accepts a runtime CSS length, spreads `div` props, and exposes an
114
- * {@link AnimatedIconHandle}. This is what `shadcn add @vegastack/<icon>`
113
+ * component that accepts a runtime CSS length, spreads `span` props, and exposes
114
+ * an {@link AnimatedIconHandle}. This is what `shadcn add @vegastack/<icon>`
115
115
  * copies in (e.g. `ActivityIcon`).
116
+ *
117
+ * The host is an `inline-flex` `<span>`: an icon sits inside a line of text, and
118
+ * a block-level box there is a layout bug.
116
119
  */
117
120
  type AnimatedIconComponent = React.ComponentType<{
118
121
  size?: number | string;
119
122
  ref?: React.Ref<AnimatedIconHandle>;
120
- } & Omit<React.HTMLAttributes<HTMLDivElement>, "ref">>;
121
- interface AnimatedIconProps extends Omit<React.HTMLAttributes<HTMLDivElement>, "ref"> {
123
+ } & Omit<React.HTMLAttributes<HTMLSpanElement>, "ref">>;
124
+ interface AnimatedIconProps extends Omit<React.HTMLAttributes<HTMLSpanElement>, "ref"> {
122
125
  /** A mirrored lucide-animated icon component, e.g. `import { ActivityIcon } from '@/components/ui/activity'`. */
123
126
  as: AnimatedIconComponent;
124
127
  /**
@@ -141,10 +144,14 @@ interface AnimatedIconProps extends Omit<React.HTMLAttributes<HTMLDivElement>, "
141
144
  * animation (pointer/focus/touch auto-trigger where provided upstream; or call
142
145
  * `ref.current.startAnimation()`).
143
146
  *
144
- * Deliberately imports no `motion` — the mirrored icon component carries that
145
- * dependency, so the base package stays lightweight. Every generated mirror reads
146
- * Motion's `useReducedMotion()` preference intrinsically and settles immediately at
147
- * its static resting state when the user requests reduced motion.
147
+ * Deliberately imports no `motion` — the mirrored icon carries that dependency
148
+ * through `@vegastack/design/create-animated-icon`, so this entry stays
149
+ * lightweight for consumers who only use `Icon`/`BrandIcon`. Every mirrored icon
150
+ * is gated on `prefers-reduced-motion` intrinsically (in the factory, once, as a
151
+ * live subscription) and settles immediately at its static resting state when
152
+ * the user requests reduced motion. No <MotionConfig> is required;
153
+ * <MotionConfig reducedMotion="always"> can add reduction, but nothing can take
154
+ * the OS preference away.
148
155
  *
149
156
  * @example
150
157
  * 'use client';
@@ -110,15 +110,18 @@ interface AnimatedIconHandle {
110
110
  }
111
111
  /**
112
112
  * Shape of a mirrored `lucide-animated` icon component: a React 19 function
113
- * component that accepts a runtime CSS length, spreads `div` props, and exposes an
114
- * {@link AnimatedIconHandle}. This is what `shadcn add @vegastack/<icon>`
113
+ * component that accepts a runtime CSS length, spreads `span` props, and exposes
114
+ * an {@link AnimatedIconHandle}. This is what `shadcn add @vegastack/<icon>`
115
115
  * copies in (e.g. `ActivityIcon`).
116
+ *
117
+ * The host is an `inline-flex` `<span>`: an icon sits inside a line of text, and
118
+ * a block-level box there is a layout bug.
116
119
  */
117
120
  type AnimatedIconComponent = React.ComponentType<{
118
121
  size?: number | string;
119
122
  ref?: React.Ref<AnimatedIconHandle>;
120
- } & Omit<React.HTMLAttributes<HTMLDivElement>, "ref">>;
121
- interface AnimatedIconProps extends Omit<React.HTMLAttributes<HTMLDivElement>, "ref"> {
123
+ } & Omit<React.HTMLAttributes<HTMLSpanElement>, "ref">>;
124
+ interface AnimatedIconProps extends Omit<React.HTMLAttributes<HTMLSpanElement>, "ref"> {
122
125
  /** A mirrored lucide-animated icon component, e.g. `import { ActivityIcon } from '@/components/ui/activity'`. */
123
126
  as: AnimatedIconComponent;
124
127
  /**
@@ -141,10 +144,14 @@ interface AnimatedIconProps extends Omit<React.HTMLAttributes<HTMLDivElement>, "
141
144
  * animation (pointer/focus/touch auto-trigger where provided upstream; or call
142
145
  * `ref.current.startAnimation()`).
143
146
  *
144
- * Deliberately imports no `motion` — the mirrored icon component carries that
145
- * dependency, so the base package stays lightweight. Every generated mirror reads
146
- * Motion's `useReducedMotion()` preference intrinsically and settles immediately at
147
- * its static resting state when the user requests reduced motion.
147
+ * Deliberately imports no `motion` — the mirrored icon carries that dependency
148
+ * through `@vegastack/design/create-animated-icon`, so this entry stays
149
+ * lightweight for consumers who only use `Icon`/`BrandIcon`. Every mirrored icon
150
+ * is gated on `prefers-reduced-motion` intrinsically (in the factory, once, as a
151
+ * live subscription) and settles immediately at its static resting state when
152
+ * the user requests reduced motion. No <MotionConfig> is required;
153
+ * <MotionConfig reducedMotion="always"> can add reduction, but nothing can take
154
+ * the OS preference away.
148
155
  *
149
156
  * @example
150
157
  * 'use client';
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  cn
3
- } from "../chunk-FBU37ITM.js";
3
+ } from "../chunk-DS7SBEDS.js";
4
4
 
5
5
  // src/icons/icon.tsx
6
6
  import "react";
package/dist/index.cjs CHANGED
@@ -22,11 +22,65 @@ var src_exports = {};
22
22
  __export(src_exports, {
23
23
  FLOATING: () => FLOATING,
24
24
  TIMINGS: () => TIMINGS,
25
- cn: () => cn
25
+ cn: () => cn,
26
+ fieldControl: () => fieldControl,
27
+ fieldControlGroup: () => fieldControlGroup,
28
+ fillInteractive: () => fillInteractive,
29
+ mergeRefs: () => mergeRefs,
30
+ prose: () => prose,
31
+ proseClassName: () => proseClassName,
32
+ selectedChipVariants: () => selectedChipVariants,
33
+ surfaceInteractive: () => surfaceInteractive,
34
+ surfaceInteractiveGroup: () => surfaceInteractiveGroup
26
35
  });
27
36
  module.exports = __toCommonJS(src_exports);
28
37
  var import_clsx = require("clsx");
29
38
  var import_tailwind_merge = require("tailwind-merge");
39
+
40
+ // src/prose.ts
41
+ var prose = {
42
+ /** The root's own ink and size — every rule below is relative to this. */
43
+ root: "text-base text-foreground",
44
+ // Headings. `scroll-m-20` keeps an anchored heading clear of a sticky header; the weight ladder
45
+ // caps at 500, which the `text-h*` roles already carry.
46
+ h1: "[&_h1]:mt-6 [&_h1]:mb-3 [&_h1]:scroll-m-20 [&_h1]:text-h1 [&_h1]:text-foreground [&_h1]:first:mt-0",
47
+ h2: "[&_h2]:mt-6 [&_h2]:mb-3 [&_h2]:scroll-m-20 [&_h2]:text-h2 [&_h2]:text-foreground [&_h2]:first:mt-0",
48
+ h3: "[&_h3]:mt-5 [&_h3]:mb-2 [&_h3]:scroll-m-20 [&_h3]:text-h3 [&_h3]:text-foreground [&_h3]:first:mt-0",
49
+ h4: "[&_h4]:mt-4 [&_h4]:mb-2 [&_h4]:scroll-m-20 [&_h4]:text-h4 [&_h4]:text-foreground [&_h4]:first:mt-0",
50
+ // h5/h6 leave the display tier and become labels — the type scale has no sixth heading size.
51
+ h5: "[&_h5]:mt-4 [&_h5]:mb-2 [&_h5]:text-label [&_h5]:text-foreground [&_h5]:first:mt-0",
52
+ h6: "[&_h6]:mt-4 [&_h6]:mb-2 [&_h6]:text-label [&_h6]:text-muted-foreground [&_h6]:first:mt-0",
53
+ p: "[&_p]:my-3 [&_p]:leading-relaxed [&_p]:text-foreground [&_p]:first:mt-0 [&_p]:last:mb-0",
54
+ // Marks. Links are `info` ink per design.md §Colours; `s` is Tiptap's strike element and `del`
55
+ // is GFM's — the same mark from two producers.
56
+ a: "[&_a]:font-medium [&_a]:text-info-text [&_a]:underline [&_a]:underline-offset-4 [&_a]:hover:text-info-text/(--alpha-link-hover)",
57
+ strong: "[&_strong]:font-medium [&_strong]:text-foreground",
58
+ em: "[&_em]:italic",
59
+ del: "[&_del]:text-muted-foreground [&_del]:line-through [&_s]:text-muted-foreground [&_s]:line-through",
60
+ // Lists. A GFM task list carries `contains-task-list` and its checkbox IS the marker, so the
61
+ // disc goes — expressed as a second, more specific descendant rule rather than a class on the
62
+ // element, which would lose the cascade (see the header).
63
+ ul: "[&_ul]:my-3 [&_ul]:ms-6 [&_ul]:list-disc [&_ul]:text-foreground [&_ul]:marker:text-muted-foreground [&_ul.contains-task-list]:list-none",
64
+ ol: "[&_ol]:my-3 [&_ol]:ms-6 [&_ol]:list-decimal [&_ol]:text-foreground [&_ol]:marker:text-muted-foreground",
65
+ li: "[&_li]:mt-1.5 [&_li]:leading-relaxed",
66
+ blockquote: "[&_blockquote]:my-3 [&_blockquote]:border-s-2 [&_blockquote]:border-border [&_blockquote]:ps-4 [&_blockquote]:text-muted-foreground [&_blockquote]:italic",
67
+ // Inline code is a chip. Code inside a `pre` is not: it inherits the panel's ground, so the chip
68
+ // is undone at higher specificity (`[&_pre_code]` is (0,1,2) against the chip's (0,1,1)).
69
+ code: "[&_code]:rounded-sm [&_code]:bg-muted [&_code]:px-1.5 [&_code]:py-0.5 [&_code]:font-mono [&_code]:text-base [&_code]:text-foreground",
70
+ // A bare `<pre>` wears `CodeBlock`'s surface — the same sunken panel, one vocabulary (B4-09).
71
+ // `CodeBlock`'s own `pre` is excluded: it already sits inside a `figure` carrying the border,
72
+ // radius and ground, and re-applying them here would draw a second panel inside the first.
73
+ pre: "[&_pre:not([data-slot='code-block-pre'])]:my-3 [&_pre:not([data-slot='code-block-pre'])]:overflow-x-auto [&_pre:not([data-slot='code-block-pre'])]:rounded-lg [&_pre:not([data-slot='code-block-pre'])]:border [&_pre:not([data-slot='code-block-pre'])]:border-border [&_pre:not([data-slot='code-block-pre'])]:bg-muted [&_pre:not([data-slot='code-block-pre'])]:p-4 [&_pre:not([data-slot='code-block-pre'])]:text-foreground",
74
+ preCode: "[&_pre_code]:bg-transparent [&_pre_code]:p-0 [&_pre_code]:font-mono [&_pre_code]:text-code [&_pre_code]:text-foreground",
75
+ hr: "[&_hr]:my-6 [&_hr]:border-border",
76
+ // GFM tables. The scroll container is structural and belongs to the consumer (MarkdownView wraps
77
+ // the table in an `overflow-x-auto` div); these are the cell and rule tokens.
78
+ table: "[&_table]:w-full [&_table]:border-collapse [&_table]:text-base [&_table]:text-foreground [&_thead]:border-b [&_thead]:border-border [&_tr]:border-b [&_tr]:border-border [&_tr]:last:border-0 [&_th]:px-3 [&_th]:py-2 [&_th]:text-start [&_th]:font-medium [&_th]:text-foreground [&_td]:px-3 [&_td]:py-2 [&_td]:text-muted-foreground",
79
+ img: "[&_img]:my-3 [&_img]:max-w-full [&_img]:rounded-lg [&_img]:border [&_img]:border-border"
80
+ };
81
+ var proseClassName = Object.values(prose).join(" ");
82
+
83
+ // src/index.ts
30
84
  var twMerge = (0, import_tailwind_merge.extendTailwindMerge)({
31
85
  extend: {
32
86
  classGroups: {
@@ -55,6 +109,60 @@ var twMerge = (0, import_tailwind_merge.extendTailwindMerge)({
55
109
  function cn(...inputs) {
56
110
  return twMerge((0, import_clsx.clsx)(inputs));
57
111
  }
112
+ var surfaceInteractive = "hover:bg-surface-2 active:bg-surface-3";
113
+ var surfaceInteractiveGroup = "group-hover/wash:bg-surface-2 group-active/wash:bg-surface-3";
114
+ var fillInteractive = {
115
+ foreground: "hover:bg-foreground/(--alpha-hover) active:bg-foreground/(--alpha-pressed)",
116
+ primary: "hover:bg-primary/(--alpha-hover) active:bg-primary/(--alpha-pressed)",
117
+ destructive: "hover:bg-destructive/(--alpha-hover) active:bg-destructive/(--alpha-pressed)",
118
+ success: "hover:bg-success/(--alpha-hover) active:bg-success/(--alpha-pressed)",
119
+ warning: "hover:bg-warning/(--alpha-hover) active:bg-warning/(--alpha-pressed)",
120
+ info: "hover:bg-info/(--alpha-hover) active:bg-info/(--alpha-pressed)",
121
+ brand: "hover:bg-brand/(--alpha-hover) active:bg-brand/(--alpha-pressed)"
122
+ };
123
+ var fieldControl = [
124
+ "rounded-md border border-input bg-transparent dark:bg-input/(--alpha-input)",
125
+ "placeholder:text-muted-foreground-faint",
126
+ "not-disabled:not-data-disabled:hover:border-foreground/(--alpha-border-subtle)",
127
+ "focus:border-ring/(--alpha-tint-border)",
128
+ "not-focus:aria-invalid:border-destructive-border/(--alpha-tint-border)",
129
+ "not-focus:data-invalid:border-destructive-border/(--alpha-tint-border)",
130
+ "disabled:cursor-not-allowed disabled:bg-surface-1 disabled:opacity-(--opacity-dim)",
131
+ "data-disabled:cursor-not-allowed data-disabled:bg-surface-1 data-disabled:opacity-(--opacity-dim)"
132
+ ].join(" ");
133
+ var fieldControlGroup = [
134
+ "rounded-md border border-input bg-transparent dark:bg-input/(--alpha-input)",
135
+ "not-has-disabled:not-data-disabled:hover:border-foreground/(--alpha-border-subtle)",
136
+ "focus-within:border-ring/(--alpha-tint-border)",
137
+ "data-focused:border-ring/(--alpha-tint-border)",
138
+ // Both the SELF and the DESCENDANT form of the invalid tint (2026-09-09). `has-aria-invalid:` is
139
+ // a `:has()` over descendants, so a group that carries `aria-invalid` on ITSELF — which is where
140
+ // `<NumberField aria-invalid />` lands the attribute, on the `[data-field-group]` element —
141
+ // matched nothing and measured the neutral `--input` border. Inside a `Field` the state arrives
142
+ // as `data-invalid` on the group and was always correct; this is the standalone path.
143
+ "not-focus-within:aria-invalid:border-destructive-border/(--alpha-tint-border)",
144
+ "not-focus-within:has-aria-invalid:border-destructive-border/(--alpha-tint-border)",
145
+ "not-focus-within:data-invalid:border-destructive-border/(--alpha-tint-border)",
146
+ "has-disabled:cursor-not-allowed has-disabled:bg-surface-1 has-disabled:opacity-(--opacity-dim)",
147
+ "data-disabled:cursor-not-allowed data-disabled:bg-surface-1 data-disabled:opacity-(--opacity-dim)"
148
+ ].join(" ");
149
+ var selectedChipVariants = {
150
+ /**
151
+ * The muted track the chips sit in — the ladder's well rung with a drawn inset boundary. The
152
+ * pseudo-element keeps the 28 / 32px track boxes unchanged; a real root border would add 2px and
153
+ * silently break the shared control scale.
154
+ */
155
+ track: "relative bg-surface-1 after:pointer-events-none after:absolute after:inset-0 after:rounded-[inherit] after:border after:border-border after:content-['']",
156
+ /**
157
+ * Chrome shared by every chip: a transparent hairline reserved at rest (so selecting adds no
158
+ * layout shift) and the muted→ink text step.
159
+ */
160
+ item: "border border-transparent hover:text-foreground",
161
+ /** Selected keyed on Base UI's `data-pressed` — Toggle, ToggleGroup, Segmented. */
162
+ pressed: "not-data-pressed:hover:bg-foreground/(--alpha-hover) not-data-pressed:active:bg-foreground/(--alpha-pressed) data-pressed:border-border data-pressed:bg-foreground/(--alpha-ink-tint) data-pressed:text-foreground data-pressed:hover:bg-foreground/(--alpha-ink-tint-strong) data-pressed:active:bg-foreground/(--alpha-ink-tint)",
163
+ /** Selected keyed on Base UI's `data-active` — Tabs. */
164
+ active: "not-data-[active]:hover:bg-foreground/(--alpha-hover) not-data-[active]:active:bg-foreground/(--alpha-pressed) data-[active]:border-border data-[active]:bg-foreground/(--alpha-ink-tint) data-[active]:text-foreground data-[active]:hover:bg-foreground/(--alpha-ink-tint-strong) data-[active]:active:bg-foreground/(--alpha-ink-tint)"
165
+ };
58
166
  var TIMINGS = {
59
167
  /** How long transient success feedback holds before reverting (CopyButton "Copied ✓"). */
60
168
  feedbackRevertMs: 1500,
@@ -63,16 +171,51 @@ var TIMINGS = {
63
171
  /** Hover delay before a rich preview (HoverCard) opens — guards accidental opens. */
64
172
  hoverOpenDelayMs: 700,
65
173
  /** Hover delay before a rich preview closes — lets the pointer travel into the card. */
66
- hoverCloseDelayMs: 300
174
+ hoverCloseDelayMs: 300,
175
+ /**
176
+ * Hover delay before a tooltip opens. A tooltip is a cheap label, not a preview, so it is
177
+ * deliberately much faster than {@link TIMINGS.hoverOpenDelayMs} — long enough that sweeping
178
+ * across a toolbar does not flash five tips, short enough that a deliberate hover feels
179
+ * instant. Base UI's `Tooltip.Provider` grouping then opens adjacent tips with no delay at
180
+ * all. Set once, in the app-root provider.
181
+ */
182
+ tooltipOpenDelayMs: 300,
183
+ /**
184
+ * Delay before a tooltip closes. Zero: a tooltip has nothing to travel into (it is not
185
+ * hoverable content, unlike a HoverCard), so holding it after the pointer leaves only
186
+ * obscures what the user moved on to.
187
+ */
188
+ tooltipCloseDelayMs: 0
67
189
  };
68
190
  var FLOATING = {
69
191
  sideOffsetAttached: 4,
70
192
  sideOffsetDetached: 8,
71
193
  collisionPadding: 8
72
194
  };
195
+ function mergeRefs(...refs) {
196
+ return (node) => {
197
+ for (const ref of refs) {
198
+ if (ref == null) continue;
199
+ if (typeof ref === "function") {
200
+ ref(node);
201
+ } else {
202
+ ref.current = node;
203
+ }
204
+ }
205
+ };
206
+ }
73
207
  // Annotate the CommonJS export names for ESM import in node:
74
208
  0 && (module.exports = {
75
209
  FLOATING,
76
210
  TIMINGS,
77
- cn
211
+ cn,
212
+ fieldControl,
213
+ fieldControlGroup,
214
+ fillInteractive,
215
+ mergeRefs,
216
+ prose,
217
+ proseClassName,
218
+ selectedChipVariants,
219
+ surfaceInteractive,
220
+ surfaceInteractiveGroup
78
221
  });