@mirror-physics/fractal-ui 0.2.0-next.8 → 0.2.0-next.82

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/dist/index.js CHANGED
@@ -1,208 +1,418 @@
1
- // src/components/Button/Button.tsx
2
- import * as React from "react";
3
-
4
- // src/utils/cn.ts
5
- import { clsx } from "clsx";
6
- function cn(...inputs) {
7
- return clsx(inputs);
8
- }
1
+ import {
2
+ Button,
3
+ ColumnEditor,
4
+ cn
5
+ } from "./chunk-UNMOINWV.js";
9
6
 
10
- // src/components/Button/Button.tsx
11
- import { jsxs } from "react/jsx-runtime";
12
- var colorPalette = {
13
- blue: {
14
- 0: "var(--blue-0)",
15
- 50: "var(--blue-50)",
16
- 100: "var(--blue-100)",
17
- 200: "var(--blue-200)",
18
- 500: "var(--blue-500)",
19
- 600: "var(--blue-600)",
20
- 700: "var(--blue-700)"
7
+ // src/components/CodeBlock/CodeBlock.tsx
8
+ import { useEffect, useMemo, useRef, useState } from "react";
9
+ import CodeMirror from "@uiw/react-codemirror";
10
+ import { HighlightStyle, foldGutter, foldService, syntaxHighlighting } from "@codemirror/language";
11
+ import { json } from "@codemirror/lang-json";
12
+ import {
13
+ Decoration,
14
+ EditorView,
15
+ MatchDecorator,
16
+ ViewPlugin,
17
+ lineNumbers as lineNumberGutter
18
+ } from "@codemirror/view";
19
+ import { tags } from "@lezer/highlight";
20
+ import { Check, Copy } from "@mirror-physics/fractal-icons";
21
+ import { jsx, jsxs } from "react/jsx-runtime";
22
+ var codeTheme = EditorView.theme({
23
+ "&": {
24
+ backgroundColor: "transparent",
25
+ color: "var(--fg-primary)",
26
+ fontSize: "0.75rem"
21
27
  },
22
- pink: {
23
- 0: "var(--pink-0)",
24
- 50: "var(--pink-50)",
25
- 100: "var(--pink-100)",
26
- 200: "var(--pink-200)",
27
- 500: "var(--pink-500)",
28
- 600: "var(--pink-600)",
29
- 700: "var(--pink-700)"
28
+ "&.cm-focused": { outline: "none" },
29
+ ".cm-scroller": {
30
+ fontFamily: "var(--font-mono)",
31
+ lineHeight: "1.55",
32
+ overscrollBehavior: "none"
30
33
  },
31
- orange: {
32
- 0: "var(--orange-0)",
33
- 50: "var(--orange-50)",
34
- 100: "var(--orange-100)",
35
- 200: "var(--orange-200)",
36
- 500: "var(--orange-500)",
37
- 600: "var(--orange-600)",
38
- 700: "var(--orange-700)"
34
+ ".cm-content": { padding: "10px 0" },
35
+ ".cm-line": {
36
+ padding: "0 12px 0 calc(12px + var(--fractal-code-wrap-indent, 0ch))",
37
+ textIndent: "calc(0px - var(--fractal-code-wrap-indent, 0ch))"
39
38
  },
40
- green: {
41
- 0: "var(--green-0)",
42
- 50: "var(--green-50)",
43
- 100: "var(--green-100)",
44
- 200: "var(--green-200)",
45
- 500: "var(--green-500)",
46
- 600: "var(--green-600)",
47
- 700: "var(--green-700)"
48
- }
49
- };
50
- var sizeConfig = {
51
- xs: { height: "28px", padding: "4px 8px", iconOnly: "28px", fontSize: "0.75rem" },
52
- sm: { height: "32px", padding: "6px 12px", iconOnly: "32px", fontSize: "0.75rem" },
53
- md: { height: "40px", padding: "8px 16px", iconOnly: "40px", fontSize: "0.875rem" },
54
- lg: { height: "44px", padding: "10px 20px", iconOnly: "44px", fontSize: "1rem" },
55
- xl: { height: "56px", padding: "14px 28px", iconOnly: "56px", fontSize: "1rem" }
56
- };
57
- function getButtonStyles(buttonType, isHovered, isActive, buttonSize, disabled, color, hasIconOnly) {
58
- const palette = colorPalette[color];
59
- const size = sizeConfig[buttonSize];
60
- const base = {
61
- height: size.height,
62
- padding: hasIconOnly ? "0" : size.padding,
63
- borderRadius: hasIconOnly ? "var(--radius-lg, 8px)" : "var(--radius-full, 9999px)",
64
- cursor: disabled ? "not-allowed" : "pointer",
65
- fontSize: size.fontSize,
66
- transition: "background-color 0.15s ease, border-color 0.15s ease, opacity 0.15s ease"
67
- };
68
- if (hasIconOnly) {
69
- base.width = size.iconOnly;
70
- base.justifyContent = "center";
39
+ ".cm-cursor, .cm-dropCursor": { borderLeftColor: "var(--fg-primary)" },
40
+ ".cm-gutters": {
41
+ backgroundColor: "var(--bg-canvas)",
42
+ borderRight: "1px solid var(--border-subtle)",
43
+ color: "var(--fg-tertiary)"
44
+ },
45
+ ".cm-lineNumbers .cm-gutterElement": { padding: "0 10px 0 4px" },
46
+ ".cm-foldGutter": { width: "14px" },
47
+ ".cm-foldGutter .cm-gutterElement": {
48
+ position: "relative",
49
+ display: "block",
50
+ width: "14px",
51
+ padding: "0"
52
+ },
53
+ ".cm-foldGutter .cm-gutterElement > span": {
54
+ position: "absolute",
55
+ top: "2px",
56
+ left: "0",
57
+ display: "inline-flex",
58
+ width: "14px",
59
+ height: "14px",
60
+ alignItems: "center",
61
+ justifyContent: "center",
62
+ padding: "0",
63
+ color: "var(--fg-tertiary)",
64
+ lineHeight: "14px",
65
+ transition: "color 120ms ease"
66
+ },
67
+ '.cm-foldGutter .cm-gutterElement > span[title="Fold line"]': { transform: "translateY(-3px)" },
68
+ ".cm-foldGutter .cm-gutterElement > span:hover": { color: "var(--fg-secondary)" },
69
+ ".fractal-code-block__escape": { color: "var(--fg-secondary)" },
70
+ ".cm-activeLine, .cm-activeLineGutter": { backgroundColor: "var(--bg-surface-1)" },
71
+ ".cm-selectionBackground, &.cm-focused .cm-selectionBackground, ::selection": {
72
+ backgroundColor: "var(--accent-surface-stroke) !important"
73
+ },
74
+ ".cm-searchMatch": {
75
+ backgroundColor: "var(--warning-surface)",
76
+ outline: "1px solid var(--warning-surface-stroke)"
77
+ },
78
+ ".cm-searchMatch.cm-searchMatch-selected": { backgroundColor: "var(--warning-subtle)" },
79
+ ".cm-panels": { backgroundColor: "var(--bg-surface-1)", color: "var(--fg-primary)" },
80
+ ".cm-tooltip": {
81
+ borderColor: "var(--border-subtle)",
82
+ backgroundColor: "var(--bg-canvas)",
83
+ color: "var(--fg-primary)"
71
84
  }
72
- if (disabled) {
73
- if (buttonType === "ghost") {
74
- return { ...base, backgroundColor: "transparent", border: "none", color: "var(--fg-disabled)" };
85
+ });
86
+ var fractalHighlightStyle = HighlightStyle.define([
87
+ { tag: tags.comment, color: "var(--fg-tertiary)", fontStyle: "italic" },
88
+ { tag: [tags.keyword, tags.controlKeyword, tags.operatorKeyword], color: "var(--purple-default)" },
89
+ { tag: [tags.string, tags.special(tags.string)], color: "var(--purple-default)" },
90
+ { tag: [tags.number, tags.bool, tags.null], color: "var(--orange-default)" },
91
+ { tag: [tags.function(tags.variableName), tags.labelName], color: "var(--accent-default)" },
92
+ { tag: [tags.typeName, tags.className, tags.namespace], color: "var(--pink-default)" },
93
+ { tag: [tags.propertyName, tags.attributeName], color: "var(--success-default)" },
94
+ { tag: [tags.operator, tags.punctuation], color: "var(--fg-secondary)" },
95
+ { tag: tags.bracket, color: "var(--accent-default)" },
96
+ { tag: [tags.heading, tags.strong], color: "var(--fg-primary)", fontWeight: "600" },
97
+ { tag: tags.emphasis, fontStyle: "italic" },
98
+ { tag: tags.link, color: "var(--accent-default)", textDecoration: "underline" },
99
+ { tag: tags.invalid, color: "var(--error-default)", textDecoration: "underline wavy" }
100
+ ]);
101
+ var jsonSupport = json();
102
+ var JSON_LANGUAGE_NAMES = /* @__PURE__ */ new Set(["json", "jsonl", "ndjson", "geojson"]);
103
+ var JSON_LINES_LANGUAGE_NAMES = /* @__PURE__ */ new Set(["jsonl", "ndjson"]);
104
+ var JSON_LINES_PRETTY_PRINT_THRESHOLD = 160;
105
+ var JSON_PROPERTY_PREFIX = /^(\s*"(?:\\.|[^"\\])*"\s*:\s*)/;
106
+ function countVisualColumns(value) {
107
+ let columns = 0;
108
+ for (const character of value) columns += character === " " ? 2 : 1;
109
+ return columns;
110
+ }
111
+ function getWrappedLineIndent(line) {
112
+ const leadingWhitespace = line.match(/^\s*/)?.[0] ?? "";
113
+ if (JSON_PROPERTY_PREFIX.test(line)) return Math.min(countVisualColumns(leadingWhitespace), 24);
114
+ return Math.min(countVisualColumns(leadingWhitespace) + 2, 12);
115
+ }
116
+ function buildWrappedLineIndentation(view) {
117
+ const decorations = [];
118
+ const decoratedLines = /* @__PURE__ */ new Set();
119
+ for (const visibleRange of view.visibleRanges) {
120
+ let position = visibleRange.from;
121
+ while (position <= visibleRange.to) {
122
+ const line = view.state.doc.lineAt(position);
123
+ if (!decoratedLines.has(line.from)) {
124
+ const indent = getWrappedLineIndent(line.text);
125
+ decorations.push(Decoration.line({
126
+ attributes: { style: `--fractal-code-wrap-indent: ${indent}ch` }
127
+ }).range(line.from));
128
+ decoratedLines.add(line.from);
129
+ }
130
+ if (line.to >= visibleRange.to) break;
131
+ position = line.to + 1;
75
132
  }
76
- return { ...base, backgroundColor: "transparent", border: "1px solid var(--fg-disabled)", color: "var(--fg-disabled)" };
77
133
  }
78
- switch (buttonType) {
79
- // Primary with text: dark pill. Icon-only: color palette (Synapse style).
80
- case "primary":
81
- if (hasIconOnly) {
82
- return {
83
- ...base,
84
- backgroundColor: isActive ? palette[700] : isHovered ? palette[500] : palette[600],
85
- border: "none",
86
- color: "var(--fg-inverse)"
87
- };
88
- }
89
- return {
90
- ...base,
91
- backgroundColor: "var(--fg-primary)",
92
- border: "1px solid var(--fg-primary)",
93
- color: "var(--fg-inverse)",
94
- ...isHovered || isActive ? { opacity: 0.85 } : {}
95
- };
96
- // Secondary: neutral. Rest=canvas, hover=surface-1 (darken), pressed=surface-2. Subtle border, dark text.
97
- case "secondary":
98
- return {
99
- ...base,
100
- backgroundColor: isActive ? "var(--bg-surface-2)" : isHovered ? "var(--bg-surface-1)" : "var(--bg-canvas)",
101
- border: "1px solid var(--border-subtle)",
102
- color: "var(--fg-primary)"
103
- };
104
- // Ghost: no border. Rest=transparent, hover/press=surface-1.
105
- case "ghost":
106
- return {
107
- ...base,
108
- backgroundColor: isHovered || isActive ? "var(--bg-surface-1)" : "transparent",
109
- border: "none",
110
- color: "var(--fg-primary)"
111
- };
112
- // Danger: low-intensity destructive action. Text stays steady; hover deepens the light red surface.
113
- case "danger":
114
- return {
115
- ...base,
116
- backgroundColor: isHovered || isActive ? "var(--danger-button-surface-hover)" : "var(--danger-button-surface)",
117
- border: "none",
118
- color: "var(--danger-button-fg)"
119
- };
120
- default:
121
- return base;
134
+ return Decoration.set(decorations, true);
135
+ }
136
+ var wrappedLineIndentation = ViewPlugin.fromClass(class {
137
+ constructor(view) {
138
+ this.decorations = buildWrappedLineIndentation(view);
139
+ }
140
+ update(update) {
141
+ this.decorations = buildWrappedLineIndentation(update.view);
122
142
  }
143
+ }, {
144
+ decorations: (plugin) => plugin.decorations
145
+ });
146
+ var jsonEscapeMatcher = new MatchDecorator({
147
+ regexp: /\\(?:["\\/bfnrt]|u[0-9a-fA-F]{4})/g,
148
+ decoration: Decoration.mark({ class: "fractal-code-block__escape" })
149
+ });
150
+ var jsonEscapeHighlighting = ViewPlugin.fromClass(class {
151
+ constructor(view) {
152
+ this.decorations = jsonEscapeMatcher.createDeco(view);
153
+ }
154
+ update(update) {
155
+ this.decorations = jsonEscapeMatcher.updateDeco(update, this.decorations);
156
+ }
157
+ }, {
158
+ decorations: (plugin) => plugin.decorations
159
+ });
160
+ function formatJsonLinesForDisplay(value) {
161
+ return value.split(/\r?\n/).map((line) => {
162
+ const firstContentIndex = line.search(/\S/);
163
+ if (firstContentIndex < 0 || line.length - firstContentIndex <= JSON_LINES_PRETTY_PRINT_THRESHOLD) return line;
164
+ const indentation = line.slice(0, firstContentIndex);
165
+ try {
166
+ const parsed = JSON.parse(line.slice(firstContentIndex));
167
+ if (parsed === null || typeof parsed !== "object") return line;
168
+ return JSON.stringify(parsed, null, 2).split("\n").map((formattedLine) => `${indentation}${formattedLine}`).join("\n");
169
+ } catch {
170
+ return line;
171
+ }
172
+ }).join("\n");
123
173
  }
124
- var Button = React.forwardRef(
125
- ({ className, buttonType = "primary", buttonSize = "md", color = "blue", icon, hasIconOnly, iconPosition = "left", style, disabled, children, ...props }, ref) => {
126
- const [isHovered, setIsHovered] = React.useState(false);
127
- const [isActive, setIsActive] = React.useState(false);
128
- const typeStyles = getButtonStyles(buttonType, isHovered, isActive, buttonSize, !!disabled, color, hasIconOnly);
129
- const hasIconAndText = icon && children && !hasIconOnly;
130
- const finalStyles = hasIconAndText ? { ...typeStyles, gap: "var(--space-2, 8px)", ...style } : { ...typeStyles, ...style };
131
- return /* @__PURE__ */ jsxs(
132
- "button",
133
- {
134
- ref,
135
- type: "button",
136
- className: cn("fractal-button", `fractal-button--${buttonType}`, className),
137
- style: finalStyles,
138
- disabled,
139
- onMouseEnter: (e) => {
140
- setIsHovered(true);
141
- props.onMouseEnter?.(e);
142
- },
143
- onMouseLeave: (e) => {
144
- setIsHovered(false);
145
- setIsActive(false);
146
- props.onMouseLeave?.(e);
147
- },
148
- onMouseDown: (e) => {
149
- setIsActive(true);
150
- props.onMouseDown?.(e);
151
- },
152
- onMouseUp: (e) => {
153
- setIsActive(false);
154
- props.onMouseUp?.(e);
155
- },
156
- ...props,
157
- children: [
158
- iconPosition !== "right" && icon,
159
- !hasIconOnly && children,
160
- iconPosition === "right" && icon
161
- ]
174
+ function findJsonContainerEnd(document2, start) {
175
+ const openingCharacter = document2[start];
176
+ if (openingCharacter !== "{" && openingCharacter !== "[") return null;
177
+ const closingCharacters = [];
178
+ let isInsideString = false;
179
+ let isEscaped = false;
180
+ for (let index = start; index < document2.length; index += 1) {
181
+ const character = document2[index];
182
+ if (isInsideString) {
183
+ if (isEscaped) {
184
+ isEscaped = false;
185
+ } else if (character === "\\") {
186
+ isEscaped = true;
187
+ } else if (character === '"') {
188
+ isInsideString = false;
162
189
  }
190
+ continue;
191
+ }
192
+ if (character === '"') {
193
+ isInsideString = true;
194
+ } else if (character === "{") {
195
+ closingCharacters.push("}");
196
+ } else if (character === "[") {
197
+ closingCharacters.push("]");
198
+ } else if (character === "}" || character === "]") {
199
+ if (closingCharacters.pop() !== character) return null;
200
+ if (closingCharacters.length === 0) return index;
201
+ }
202
+ }
203
+ return null;
204
+ }
205
+ var jsonLinesFoldSupport = foldService.of((state, lineStart, lineEnd) => {
206
+ const line = state.doc.sliceString(lineStart, lineEnd);
207
+ const firstContentIndex = line.search(/\S/);
208
+ if (firstContentIndex < 0) return null;
209
+ const containerStart = lineStart + firstContentIndex;
210
+ const openingCharacter = line[firstContentIndex];
211
+ if (openingCharacter !== "{" && openingCharacter !== "[") return null;
212
+ const document2 = state.doc.toString();
213
+ const containerEnd = findJsonContainerEnd(document2, containerStart);
214
+ if (containerEnd === null || containerEnd - containerStart < 2) return null;
215
+ try {
216
+ const parsed = JSON.parse(document2.slice(containerStart, containerEnd + 1));
217
+ if (parsed === null || typeof parsed !== "object") return null;
218
+ } catch {
219
+ return null;
220
+ }
221
+ return {
222
+ from: containerStart + 1,
223
+ to: containerEnd
224
+ };
225
+ });
226
+ function resolveLanguage(language, fileName) {
227
+ const normalized = language?.trim().toLowerCase();
228
+ const extension = fileName?.split(/[?#]/, 1)[0].split(".").pop()?.toLowerCase();
229
+ if (normalized && JSON_LANGUAGE_NAMES.has(normalized) || extension && JSON_LANGUAGE_NAMES.has(extension)) {
230
+ const isJsonLines = Boolean(
231
+ normalized && JSON_LINES_LANGUAGE_NAMES.has(normalized) || extension && JSON_LINES_LANGUAGE_NAMES.has(extension)
163
232
  );
233
+ return { label: isJsonLines ? "JSONL" : "JSON", support: jsonSupport, isJsonLines };
164
234
  }
165
- );
166
- Button.displayName = "Button";
235
+ if (normalized && !["text", "txt", "plaintext", "plain"].includes(normalized)) {
236
+ return { label: language?.trim(), support: null, isJsonLines: false };
237
+ }
238
+ return { support: null, isJsonLines: false };
239
+ }
240
+ function CodeBlock({
241
+ value,
242
+ language,
243
+ fileName,
244
+ label,
245
+ editable = false,
246
+ onChange,
247
+ lineNumbers = true,
248
+ wrap = true,
249
+ copyable = true,
250
+ ariaLabel,
251
+ height,
252
+ minHeight,
253
+ maxHeight = "28rem",
254
+ placeholder,
255
+ autoFocus,
256
+ className,
257
+ ...props
258
+ }) {
259
+ const resolvedLanguage = useMemo(() => resolveLanguage(language, fileName), [fileName, language]);
260
+ const displayValue = useMemo(
261
+ () => !editable && resolvedLanguage.isJsonLines ? formatJsonLinesForDisplay(value) : value,
262
+ [editable, resolvedLanguage.isJsonLines, value]
263
+ );
264
+ const [copyStatus, setCopyStatus] = useState("idle");
265
+ const copyResetTimer = useRef(null);
266
+ useEffect(() => () => {
267
+ if (copyResetTimer.current) clearTimeout(copyResetTimer.current);
268
+ }, []);
269
+ const extensions = useMemo(() => [
270
+ codeTheme,
271
+ syntaxHighlighting(fractalHighlightStyle),
272
+ ...resolvedLanguage.support ? [resolvedLanguage.support] : [],
273
+ ...resolvedLanguage.support ? [jsonEscapeHighlighting] : [],
274
+ ...resolvedLanguage.isJsonLines ? [jsonLinesFoldSupport] : [],
275
+ ...lineNumbers ? [foldGutter(), lineNumberGutter()] : [],
276
+ ...wrap ? [EditorView.lineWrapping, wrappedLineIndentation] : []
277
+ ], [lineNumbers, resolvedLanguage.isJsonLines, resolvedLanguage.support, wrap]);
278
+ const languageLabel = resolvedLanguage.label;
279
+ const toolbarLabel = label ?? fileName ?? languageLabel;
280
+ const surfaceLabel = ariaLabel ?? (fileName ? `${fileName} ${editable ? "editor" : "viewer"}` : `Code ${editable ? "editor" : "viewer"}`);
281
+ const copyCode = async () => {
282
+ try {
283
+ await navigator.clipboard.writeText(value);
284
+ setCopyStatus("copied");
285
+ } catch {
286
+ setCopyStatus("error");
287
+ }
288
+ if (copyResetTimer.current) clearTimeout(copyResetTimer.current);
289
+ copyResetTimer.current = setTimeout(() => setCopyStatus("idle"), 1800);
290
+ };
291
+ const showToolbar = toolbarLabel !== void 0 || copyable || editable;
292
+ return /* @__PURE__ */ jsxs(
293
+ "div",
294
+ {
295
+ className: cn(
296
+ "fractal-code-block",
297
+ editable && "fractal-code-block--editable",
298
+ !lineNumbers && "fractal-code-block--no-line-numbers",
299
+ className
300
+ ),
301
+ ...props,
302
+ children: [
303
+ showToolbar && /* @__PURE__ */ jsxs("div", { className: "fractal-code-block__toolbar", children: [
304
+ /* @__PURE__ */ jsxs("div", { className: "fractal-code-block__identity", children: [
305
+ toolbarLabel !== void 0 && /* @__PURE__ */ jsx("span", { className: "fractal-code-block__label", title: typeof toolbarLabel === "string" ? toolbarLabel : void 0, children: toolbarLabel }),
306
+ fileName && languageLabel && /* @__PURE__ */ jsx("span", { className: "fractal-code-block__language", children: languageLabel }),
307
+ editable && /* @__PURE__ */ jsx("span", { className: "fractal-code-block__mode", children: "Editing" })
308
+ ] }),
309
+ copyable && /* @__PURE__ */ jsxs(
310
+ "button",
311
+ {
312
+ type: "button",
313
+ className: "fractal-code-block__copy",
314
+ "aria-label": copyStatus === "copied" ? "Code copied" : "Copy code",
315
+ onClick: () => void copyCode(),
316
+ children: [
317
+ copyStatus === "copied" ? /* @__PURE__ */ jsx(Check, { size: 14 }) : /* @__PURE__ */ jsx(Copy, { size: 14 }),
318
+ /* @__PURE__ */ jsx("span", { children: copyStatus === "copied" ? "Copied" : copyStatus === "error" ? "Copy failed" : "Copy" })
319
+ ]
320
+ }
321
+ )
322
+ ] }),
323
+ /* @__PURE__ */ jsx("div", { className: "fractal-code-block__surface", children: /* @__PURE__ */ jsx(
324
+ CodeMirror,
325
+ {
326
+ "aria-label": surfaceLabel,
327
+ value: displayValue,
328
+ editable,
329
+ readOnly: !editable,
330
+ onChange,
331
+ extensions,
332
+ theme: "none",
333
+ basicSetup: {
334
+ lineNumbers: false,
335
+ foldGutter: false,
336
+ drawSelection: editable,
337
+ highlightActiveLine: editable,
338
+ highlightActiveLineGutter: editable,
339
+ highlightSelectionMatches: editable,
340
+ autocompletion: editable,
341
+ history: editable
342
+ },
343
+ indentWithTab: editable,
344
+ height,
345
+ minHeight,
346
+ maxHeight,
347
+ placeholder,
348
+ autoFocus
349
+ }
350
+ ) }),
351
+ /* @__PURE__ */ jsx("span", { className: "fractal-code-block__status", "aria-live": "polite", children: copyStatus === "copied" ? "Code copied to clipboard." : copyStatus === "error" ? "Code could not be copied." : "" })
352
+ ]
353
+ }
354
+ );
355
+ }
356
+
357
+ // src/components/Breadcrumbs/Breadcrumbs.tsx
358
+ import { ChevronRight } from "@mirror-physics/fractal-icons";
359
+ import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
360
+ function Breadcrumbs({
361
+ items,
362
+ ariaLabel = "Breadcrumbs",
363
+ separator = /* @__PURE__ */ jsx2(ChevronRight, { "aria-hidden": "true", size: 14 }),
364
+ className,
365
+ ...props
366
+ }) {
367
+ if (items.length === 0) return null;
368
+ return /* @__PURE__ */ jsx2("nav", { "aria-label": ariaLabel, className: cn("fractal-breadcrumbs", className), ...props, children: /* @__PURE__ */ jsx2("ol", { className: "fractal-breadcrumbs__list", children: items.map((item, index) => {
369
+ const isCurrent = index === items.length - 1;
370
+ const content = isCurrent ? /* @__PURE__ */ jsx2("span", { "aria-current": "page", className: "fractal-breadcrumbs__current", children: item.label }) : /* @__PURE__ */ jsx2("a", { className: "fractal-breadcrumbs__link", href: item.href ?? "#", onClick: item.onClick, children: item.label });
371
+ return /* @__PURE__ */ jsxs2("li", { className: "fractal-breadcrumbs__item", children: [
372
+ index > 0 && /* @__PURE__ */ jsx2("span", { "aria-hidden": "true", className: "fractal-breadcrumbs__separator", children: separator }),
373
+ content
374
+ ] }, index);
375
+ }) }) });
376
+ }
167
377
 
168
378
  // src/components/Card/Card.tsx
169
- import * as React2 from "react";
379
+ import * as React from "react";
170
380
  import { Folder as FolderIcon } from "@mirror-physics/fractal-icons";
171
- import { jsx, jsxs as jsxs2 } from "react/jsx-runtime";
172
- var Card = React2.forwardRef(
173
- ({ className, ...props }, ref) => /* @__PURE__ */ jsx("div", { ref, className: cn("fractal-card", className), ...props })
381
+ import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
382
+ var Card = React.forwardRef(
383
+ ({ className, ...props }, ref) => /* @__PURE__ */ jsx3("div", { ref, "data-fractal-border-surface": "surface-1", className: cn("fractal-card", className), ...props })
174
384
  );
175
385
  Card.displayName = "Card";
176
- var CardHeader = React2.forwardRef(
177
- ({ className, ...props }, ref) => /* @__PURE__ */ jsx("div", { ref, className: cn("fractal-card-header", className), ...props })
386
+ var CardHeader = React.forwardRef(
387
+ ({ className, ...props }, ref) => /* @__PURE__ */ jsx3("div", { ref, className: cn("fractal-card-header", className), ...props })
178
388
  );
179
389
  CardHeader.displayName = "CardHeader";
180
- var CardTitle = React2.forwardRef(
181
- ({ className, ...props }, ref) => /* @__PURE__ */ jsx("h3", { ref, className: cn("fractal-card-title", className), ...props })
390
+ var CardTitle = React.forwardRef(
391
+ ({ className, ...props }, ref) => /* @__PURE__ */ jsx3("h3", { ref, className: cn("fractal-card-title", className), ...props })
182
392
  );
183
393
  CardTitle.displayName = "CardTitle";
184
- var CardDescription = React2.forwardRef(
185
- ({ className, ...props }, ref) => /* @__PURE__ */ jsx("p", { ref, className: cn("fractal-card-description", className), ...props })
394
+ var CardDescription = React.forwardRef(
395
+ ({ className, ...props }, ref) => /* @__PURE__ */ jsx3("p", { ref, className: cn("fractal-card-description", className), ...props })
186
396
  );
187
397
  CardDescription.displayName = "CardDescription";
188
- var CardContent = React2.forwardRef(
189
- ({ className, ...props }, ref) => /* @__PURE__ */ jsx("div", { ref, className: cn("fractal-card-content", className), ...props })
398
+ var CardContent = React.forwardRef(
399
+ ({ className, ...props }, ref) => /* @__PURE__ */ jsx3("div", { ref, className: cn("fractal-card-content", className), ...props })
190
400
  );
191
401
  CardContent.displayName = "CardContent";
192
- var CardFooter = React2.forwardRef(
193
- ({ className, ...props }, ref) => /* @__PURE__ */ jsx("div", { ref, className: cn("fractal-card-footer", className), ...props })
402
+ var CardFooter = React.forwardRef(
403
+ ({ className, ...props }, ref) => /* @__PURE__ */ jsx3("div", { ref, className: cn("fractal-card-footer", className), ...props })
194
404
  );
195
405
  CardFooter.displayName = "CardFooter";
196
- var FeatureCard = React2.forwardRef(
197
- ({ icon, title, description, className, ...props }, ref) => /* @__PURE__ */ jsxs2("div", { ref, className: cn("fractal-feature-card", className), ...props, children: [
198
- icon && /* @__PURE__ */ jsx("div", { className: "fractal-feature-card__icon", children: icon }),
199
- /* @__PURE__ */ jsx("h3", { className: "fractal-feature-card__title", children: title }),
200
- /* @__PURE__ */ jsx("p", { className: "fractal-feature-card__description", children: description })
406
+ var FeatureCard = React.forwardRef(
407
+ ({ icon, title, description, className, ...props }, ref) => /* @__PURE__ */ jsxs3("div", { ref, className: cn("fractal-feature-card", className), ...props, children: [
408
+ icon && /* @__PURE__ */ jsx3("div", { className: "fractal-feature-card__icon", children: icon }),
409
+ /* @__PURE__ */ jsx3("h3", { className: "fractal-feature-card__title", children: title }),
410
+ /* @__PURE__ */ jsx3("p", { className: "fractal-feature-card__description", children: description })
201
411
  ] })
202
412
  );
203
413
  FeatureCard.displayName = "FeatureCard";
204
- var FolderCard = React2.forwardRef(
205
- ({ name, sessionCount, selected, className, onClick, ...props }, ref) => /* @__PURE__ */ jsxs2(
414
+ var FolderCard = React.forwardRef(
415
+ ({ name, sessionCount, selected, className, onClick, ...props }, ref) => /* @__PURE__ */ jsxs3(
206
416
  "div",
207
417
  {
208
418
  ref,
@@ -218,11 +428,11 @@ var FolderCard = React2.forwardRef(
218
428
  className: cn("fractal-folder-card", selected && "fractal-folder-card--selected", className),
219
429
  ...props,
220
430
  children: [
221
- /* @__PURE__ */ jsxs2("div", { className: "fractal-folder-card__header", children: [
222
- /* @__PURE__ */ jsx(FolderIcon, { size: 16, className: "fractal-folder-card__icon", "aria-hidden": true }),
223
- /* @__PURE__ */ jsx("span", { className: "fractal-folder-card__name", children: name })
431
+ /* @__PURE__ */ jsxs3("div", { className: "fractal-folder-card__header", children: [
432
+ /* @__PURE__ */ jsx3(FolderIcon, { size: 16, className: "fractal-folder-card__icon", "aria-hidden": true }),
433
+ /* @__PURE__ */ jsx3("span", { className: "fractal-folder-card__name", children: name })
224
434
  ] }),
225
- sessionCount !== void 0 && /* @__PURE__ */ jsx("div", { className: "fractal-folder-card__footer", children: /* @__PURE__ */ jsxs2("span", { className: "fractal-folder-card__sessions", children: [
435
+ sessionCount !== void 0 && /* @__PURE__ */ jsx3("div", { className: "fractal-folder-card__footer", children: /* @__PURE__ */ jsxs3("span", { className: "fractal-folder-card__sessions", children: [
226
436
  sessionCount,
227
437
  " ",
228
438
  sessionCount === 1 ? "session" : "sessions"
@@ -234,10 +444,10 @@ var FolderCard = React2.forwardRef(
234
444
  FolderCard.displayName = "FolderCard";
235
445
 
236
446
  // src/components/PromptCard/PromptCard.tsx
237
- import * as React3 from "react";
238
- import { jsx as jsx2, jsxs as jsxs3 } from "react/jsx-runtime";
447
+ import * as React2 from "react";
448
+ import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
239
449
  var titleToPrompt = (title) => typeof title === "string" || typeof title === "number" ? String(title) : "";
240
- var PromptCard = React3.forwardRef(
450
+ var PromptCard = React2.forwardRef(
241
451
  ({
242
452
  title,
243
453
  icon,
@@ -256,7 +466,7 @@ var PromptCard = React3.forwardRef(
256
466
  onPromptSelect?.(prompt ?? titleToPrompt(title));
257
467
  }
258
468
  };
259
- return /* @__PURE__ */ jsxs3(
469
+ return /* @__PURE__ */ jsxs4(
260
470
  "button",
261
471
  {
262
472
  ref,
@@ -271,16 +481,16 @@ var PromptCard = React3.forwardRef(
271
481
  onClick: handleClick,
272
482
  ...props,
273
483
  children: [
274
- icon && /* @__PURE__ */ jsx2("span", { className: "fractal-prompt-card__icon", "aria-hidden": "true", children: icon }),
275
- /* @__PURE__ */ jsx2("span", { className: "fractal-prompt-card__title", children: title })
484
+ icon && /* @__PURE__ */ jsx4("span", { className: "fractal-prompt-card__icon", "aria-hidden": "true", children: icon }),
485
+ /* @__PURE__ */ jsx4("span", { className: "fractal-prompt-card__title", children: title })
276
486
  ]
277
487
  }
278
488
  );
279
489
  }
280
490
  );
281
491
  PromptCard.displayName = "PromptCard";
282
- var PromptGrid = React3.forwardRef(
283
- ({ compact, padded, className, ...props }, ref) => /* @__PURE__ */ jsx2(
492
+ var PromptGrid = React2.forwardRef(
493
+ ({ compact, padded, className, ...props }, ref) => /* @__PURE__ */ jsx4(
284
494
  "div",
285
495
  {
286
496
  ref,
@@ -297,29 +507,356 @@ var PromptGrid = React3.forwardRef(
297
507
  PromptGrid.displayName = "PromptGrid";
298
508
 
299
509
  // src/components/Input/Input.tsx
510
+ import * as React3 from "react";
511
+ import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
512
+ var Input = React3.forwardRef(
513
+ ({ className, type, controlSize = "lg", fieldLabel, id, placeholder, ...props }, ref) => {
514
+ const generatedId = React3.useId();
515
+ const fieldId = id ?? generatedId;
516
+ const floatingLabel = type === "search" ? void 0 : fieldLabel;
517
+ const input = /* @__PURE__ */ jsx5(
518
+ "input",
519
+ {
520
+ type,
521
+ ref,
522
+ id: floatingLabel ? fieldId : id,
523
+ placeholder: floatingLabel ? placeholder || " " : placeholder,
524
+ "data-control-size": controlSize,
525
+ className: cn("fractal-control", "fractal-input", className),
526
+ ...props,
527
+ "aria-label": props["aria-label"] ?? (type === "search" && !props["aria-labelledby"] ? fieldLabel ?? "Search" : void 0)
528
+ }
529
+ );
530
+ if (!floatingLabel) return input;
531
+ return /* @__PURE__ */ jsxs5(
532
+ "div",
533
+ {
534
+ className: "fractal-floating-field",
535
+ "data-control-size": controlSize,
536
+ "data-always-float": ["date", "datetime-local", "time", "month", "week", "color", "file", "range"].includes(type ?? "") || void 0,
537
+ children: [
538
+ input,
539
+ /* @__PURE__ */ jsx5("label", { className: "fractal-floating-field__label", htmlFor: fieldId, children: fieldLabel })
540
+ ]
541
+ }
542
+ );
543
+ }
544
+ );
545
+ Input.displayName = "Input";
546
+
547
+ // src/components/TableSearch/TableSearch.tsx
300
548
  import * as React4 from "react";
301
- import { jsx as jsx3 } from "react/jsx-runtime";
302
- var Input = React4.forwardRef(
303
- ({ className, type, ...props }, ref) => /* @__PURE__ */ jsx3(
304
- "input",
549
+ import { MagnifyingGlass, MagnifyingGlassLines } from "@mirror-physics/fractal-icons";
550
+ import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
551
+ var TableSearch = React4.forwardRef(
552
+ ({
553
+ searchType = "text",
554
+ controlSize = "lg",
555
+ className,
556
+ inputClassName,
557
+ placeholder,
558
+ title,
559
+ ...props
560
+ }, ref) => {
561
+ const isGlob = searchType === "glob";
562
+ const SearchIcon = isGlob ? MagnifyingGlassLines : MagnifyingGlass;
563
+ return /* @__PURE__ */ jsxs6(
564
+ "div",
565
+ {
566
+ className: cn("fractal-table-search", className),
567
+ "data-search-type": searchType,
568
+ "data-control-size": controlSize,
569
+ children: [
570
+ /* @__PURE__ */ jsx6(SearchIcon, { className: "fractal-table-search__icon", size: 17, "aria-hidden": "true" }),
571
+ /* @__PURE__ */ jsx6(
572
+ Input,
573
+ {
574
+ ...props,
575
+ controlSize,
576
+ ref,
577
+ type: "search",
578
+ className: cn("fractal-table-search__input", inputClassName),
579
+ placeholder: placeholder ?? (isGlob ? "Glob search \u2014 use *, ?, or [a-z]" : "Search"),
580
+ title: title ?? (isGlob ? "Glob pattern: *, **, ?, [abc], [a-z], and [!abc]" : void 0)
581
+ }
582
+ )
583
+ ]
584
+ }
585
+ );
586
+ }
587
+ );
588
+ TableSearch.displayName = "TableSearch";
589
+
590
+ // src/components/NumberInput/NumberInput.tsx
591
+ import * as React5 from "react";
592
+ import { Minus, Plus } from "@mirror-physics/fractal-icons";
593
+ import { jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
594
+ function NumberInput({
595
+ unit,
596
+ controlSize = "lg",
597
+ className,
598
+ value,
599
+ defaultValue,
600
+ disabled,
601
+ readOnly,
602
+ min,
603
+ max,
604
+ ...props
605
+ }) {
606
+ const inputRef = React5.useRef(null);
607
+ const numericValue = Number(value ?? defaultValue);
608
+ const characterCount = Math.max(String(value ?? defaultValue ?? "").length, 1);
609
+ const controlLabel = typeof props["aria-label"] === "string" ? props["aria-label"] : props.fieldLabel ?? "value";
610
+ const decrementDisabled = disabled || readOnly || Number.isFinite(numericValue) && min !== void 0 && numericValue <= Number(min);
611
+ const incrementDisabled = disabled || readOnly || Number.isFinite(numericValue) && max !== void 0 && numericValue >= Number(max);
612
+ const changeByStep = (direction) => {
613
+ const input = inputRef.current;
614
+ if (!input) return;
615
+ if (direction === -1) input.stepDown();
616
+ else input.stepUp();
617
+ input.dispatchEvent(new Event("input", { bubbles: true }));
618
+ input.focus();
619
+ };
620
+ return /* @__PURE__ */ jsxs7("div", { "data-control-size": controlSize, className: cn("fractal-number-input", unit && "fractal-number-input--with-unit", className), children: [
621
+ /* @__PURE__ */ jsx7(
622
+ Input,
623
+ {
624
+ ref: inputRef,
625
+ controlSize,
626
+ type: "number",
627
+ value,
628
+ defaultValue,
629
+ disabled,
630
+ readOnly,
631
+ min,
632
+ max,
633
+ ...props
634
+ }
635
+ ),
636
+ unit && /* @__PURE__ */ jsx7(
637
+ "span",
638
+ {
639
+ className: "fractal-number-input-unit",
640
+ style: { left: `calc(var(--control-padding-inline) + ${characterCount}ch)` },
641
+ "aria-hidden": "true",
642
+ children: unit
643
+ }
644
+ ),
645
+ /* @__PURE__ */ jsxs7("div", { className: "fractal-number-input-controls", children: [
646
+ /* @__PURE__ */ jsx7(
647
+ "button",
648
+ {
649
+ type: "button",
650
+ className: "fractal-number-input-step",
651
+ "aria-label": `Decrease ${controlLabel}`,
652
+ disabled: decrementDisabled,
653
+ onClick: () => changeByStep(-1),
654
+ children: /* @__PURE__ */ jsx7(Minus, { "aria-hidden": "true", size: 16 })
655
+ }
656
+ ),
657
+ /* @__PURE__ */ jsx7(
658
+ "button",
659
+ {
660
+ type: "button",
661
+ className: "fractal-number-input-step",
662
+ "aria-label": `Increase ${controlLabel}`,
663
+ disabled: incrementDisabled,
664
+ onClick: () => changeByStep(1),
665
+ children: /* @__PURE__ */ jsx7(Plus, { "aria-hidden": "true", size: 16 })
666
+ }
667
+ )
668
+ ] })
669
+ ] });
670
+ }
671
+
672
+ // src/components/Slider/Slider.tsx
673
+ import { jsx as jsx8, jsxs as jsxs8 } from "react/jsx-runtime";
674
+ function Slider(props) {
675
+ const {
676
+ min = 0,
677
+ max = 100,
678
+ step = 1,
679
+ disabled = false,
680
+ className,
681
+ showValues = true,
682
+ formatValue = (value) => value,
683
+ knobs = 1
684
+ } = props;
685
+ const span = Math.max(max - min, 1);
686
+ const rawValues = knobs === 2 ? props.value : [props.value];
687
+ const firstValue = clamp(rawValues[0], min, max);
688
+ const secondValue = knobs === 2 ? clamp(rawValues[1], firstValue, max) : firstValue;
689
+ const selectionStart = knobs === 2 ? firstValue : min;
690
+ const selectionEnd = secondValue;
691
+ const selectionStartPercent = (selectionStart - min) / span * 100;
692
+ const selectionEndPercent = (selectionEnd - min) / span * 100;
693
+ const changeSingleValue = (value) => {
694
+ ;
695
+ props.onValueChange(clamp(value, min, max));
696
+ };
697
+ const changeRangeValue = (nextLower, nextUpper) => {
698
+ ;
699
+ props.onValueChange([
700
+ clamp(nextLower, min, nextUpper),
701
+ clamp(nextUpper, nextLower, max)
702
+ ]);
703
+ };
704
+ const getKeyboardValue = (event, value) => {
705
+ const pageStep = step * 10;
706
+ const adjustments = {
707
+ ArrowDown: -step,
708
+ ArrowLeft: -step,
709
+ ArrowRight: step,
710
+ ArrowUp: step,
711
+ PageDown: -pageStep,
712
+ PageUp: pageStep
713
+ };
714
+ if (event.key === "Home") return min;
715
+ if (event.key === "End") return max;
716
+ if (!(event.key in adjustments)) return null;
717
+ return value + adjustments[event.key];
718
+ };
719
+ return /* @__PURE__ */ jsxs8(
720
+ "div",
305
721
  {
306
- type,
307
- ref,
308
- className: cn("fractal-input", className),
309
- ...props
722
+ className: cn("fractal-slider", disabled && "fractal-slider--disabled", className),
723
+ "data-disabled": disabled || void 0,
724
+ "data-knobs": knobs,
725
+ children: [
726
+ showValues && /* @__PURE__ */ jsxs8("div", { className: "fractal-slider-values", "aria-hidden": "true", children: [
727
+ /* @__PURE__ */ jsx8("output", { children: formatValue(firstValue) }),
728
+ knobs === 2 && /* @__PURE__ */ jsx8("output", { children: formatValue(secondValue) })
729
+ ] }),
730
+ /* @__PURE__ */ jsxs8(
731
+ "div",
732
+ {
733
+ className: "fractal-slider-control",
734
+ style: {
735
+ "--fractal-slider-selection-start": `${selectionStartPercent}%`,
736
+ "--fractal-slider-selection-end": `${selectionEndPercent}%`
737
+ },
738
+ children: [
739
+ /* @__PURE__ */ jsx8("span", { className: "fractal-slider-track", "aria-hidden": "true" }),
740
+ /* @__PURE__ */ jsx8("span", { className: "fractal-slider-selection", "aria-hidden": "true" }),
741
+ /* @__PURE__ */ jsx8(
742
+ "input",
743
+ {
744
+ "aria-label": knobs === 2 ? props.lowerAriaLabel ?? "Lower value" : props.ariaLabel ?? "Slider value",
745
+ disabled,
746
+ max,
747
+ min,
748
+ onChange: (event) => {
749
+ const nextValue = event.currentTarget.valueAsNumber;
750
+ if (knobs === 2) changeRangeValue(Math.min(nextValue, secondValue), secondValue);
751
+ else changeSingleValue(nextValue);
752
+ },
753
+ onKeyDown: (event) => {
754
+ const nextValue = getKeyboardValue(event, firstValue);
755
+ if (nextValue === null) return;
756
+ event.preventDefault();
757
+ if (knobs === 2) changeRangeValue(Math.min(nextValue, secondValue), secondValue);
758
+ else changeSingleValue(nextValue);
759
+ },
760
+ step,
761
+ type: "range",
762
+ value: firstValue
763
+ }
764
+ ),
765
+ knobs === 2 && /* @__PURE__ */ jsx8(
766
+ "input",
767
+ {
768
+ "aria-label": props.upperAriaLabel ?? "Upper value",
769
+ disabled,
770
+ max,
771
+ min,
772
+ onChange: (event) => {
773
+ const nextValue = event.currentTarget.valueAsNumber;
774
+ changeRangeValue(firstValue, Math.max(nextValue, firstValue));
775
+ },
776
+ onKeyDown: (event) => {
777
+ const nextValue = getKeyboardValue(event, secondValue);
778
+ if (nextValue === null) return;
779
+ event.preventDefault();
780
+ changeRangeValue(firstValue, Math.max(nextValue, firstValue));
781
+ },
782
+ step,
783
+ type: "range",
784
+ value: secondValue
785
+ }
786
+ )
787
+ ]
788
+ }
789
+ )
790
+ ]
310
791
  }
311
- )
792
+ );
793
+ }
794
+ function clamp(value, minimum, maximum) {
795
+ return Math.min(maximum, Math.max(minimum, value));
796
+ }
797
+
798
+ // src/components/PasswordInput/PasswordInput.tsx
799
+ import * as React6 from "react";
800
+ import { Eye, EyeSlash } from "@mirror-physics/fractal-icons";
801
+ import { jsx as jsx9, jsxs as jsxs9 } from "react/jsx-runtime";
802
+ var PasswordInput = React6.forwardRef(
803
+ ({
804
+ className,
805
+ controlSize = "lg",
806
+ disabled,
807
+ showPasswordLabel = "Show password",
808
+ hidePasswordLabel = "Hide password",
809
+ ...props
810
+ }, ref) => {
811
+ const [passwordVisible, setPasswordVisible] = React6.useState(false);
812
+ const toggleLabel = passwordVisible ? hidePasswordLabel : showPasswordLabel;
813
+ return /* @__PURE__ */ jsxs9(
814
+ "div",
815
+ {
816
+ className: cn("fractal-password-input", className),
817
+ "data-control-size": controlSize,
818
+ "data-password-visible": passwordVisible ? "" : void 0,
819
+ children: [
820
+ /* @__PURE__ */ jsx9(
821
+ Input,
822
+ {
823
+ ...props,
824
+ controlSize,
825
+ ref,
826
+ className: "fractal-password-input-field",
827
+ disabled,
828
+ type: passwordVisible ? "text" : "password"
829
+ }
830
+ ),
831
+ /* @__PURE__ */ jsx9(
832
+ "button",
833
+ {
834
+ type: "button",
835
+ className: "fractal-password-input-toggle",
836
+ "aria-label": toggleLabel,
837
+ "aria-pressed": passwordVisible,
838
+ disabled,
839
+ title: toggleLabel,
840
+ onClick: () => setPasswordVisible((visible) => !visible),
841
+ children: passwordVisible ? /* @__PURE__ */ jsx9(EyeSlash, { "aria-hidden": "true", size: 18 }) : /* @__PURE__ */ jsx9(Eye, { "aria-hidden": "true", size: 18 })
842
+ }
843
+ )
844
+ ]
845
+ }
846
+ );
847
+ }
312
848
  );
313
- Input.displayName = "Input";
849
+ PasswordInput.displayName = "PasswordInput";
314
850
 
315
851
  // src/components/Label/Label.tsx
316
- import * as React5 from "react";
317
- import { jsx as jsx4 } from "react/jsx-runtime";
318
- var Label = React5.forwardRef(
319
- ({ className, ...props }, ref) => /* @__PURE__ */ jsx4(
852
+ import * as React7 from "react";
853
+ import { jsx as jsx10 } from "react/jsx-runtime";
854
+ var Label = React7.forwardRef(
855
+ ({ className, controlSize = "lg", ...props }, ref) => /* @__PURE__ */ jsx10(
320
856
  "label",
321
857
  {
322
858
  ref,
859
+ "data-control-size": controlSize,
323
860
  className: cn("fractal-label", className),
324
861
  ...props
325
862
  }
@@ -328,44 +865,147 @@ var Label = React5.forwardRef(
328
865
  Label.displayName = "Label";
329
866
 
330
867
  // src/components/Alert/Alert.tsx
331
- import * as React6 from "react";
332
- import { jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
868
+ import * as React8 from "react";
869
+ import { CircleCheck, CircleClose, CircleInfo, Warning } from "@mirror-physics/fractal-icons";
870
+ import { jsx as jsx11, jsxs as jsxs10 } from "react/jsx-runtime";
333
871
  var defaultIcons = {
334
- default: "\u2139",
335
- info: "\u2139",
336
- success: "\u2713",
337
- warning: "\u26A0",
338
- error: "\u2715"
872
+ default: CircleInfo,
873
+ info: CircleInfo,
874
+ success: CircleCheck,
875
+ warning: Warning,
876
+ error: CircleClose
877
+ };
878
+ var Alert = React8.forwardRef(
879
+ ({ className, variant = "default", icon, children, ...props }, ref) => {
880
+ const DefaultIcon = defaultIcons[variant];
881
+ return /* @__PURE__ */ jsxs10(
882
+ "div",
883
+ {
884
+ ref,
885
+ role: "alert",
886
+ className: cn("fractal-alert", `fractal-alert--${variant}`, className),
887
+ ...props,
888
+ children: [
889
+ /* @__PURE__ */ jsx11("span", { className: "fractal-alert-icon", "aria-hidden": true, children: icon ?? /* @__PURE__ */ jsx11(DefaultIcon, { size: 18 }) }),
890
+ /* @__PURE__ */ jsx11("div", { className: "fractal-alert-content", children })
891
+ ]
892
+ }
893
+ );
894
+ }
895
+ );
896
+ Alert.displayName = "Alert";
897
+ var AlertTitle = React8.forwardRef(
898
+ ({ className, ...props }, ref) => /* @__PURE__ */ jsx11("h5", { ref, className: cn("fractal-alert-title", className), ...props })
899
+ );
900
+ AlertTitle.displayName = "AlertTitle";
901
+ var AlertDescription = React8.forwardRef(
902
+ ({ className, ...props }, ref) => /* @__PURE__ */ jsx11("div", { ref, className: cn("fractal-alert-description", className), ...props })
903
+ );
904
+ AlertDescription.displayName = "AlertDescription";
905
+
906
+ // src/components/Toast/Toast.tsx
907
+ import * as React9 from "react";
908
+ import { createPortal } from "react-dom";
909
+ import { CircleCheck as CircleCheck2, CircleClose as CircleClose2, CircleInfo as CircleInfo2, Close, Warning as Warning2 } from "@mirror-physics/fractal-icons";
910
+ import { jsx as jsx12, jsxs as jsxs11 } from "react/jsx-runtime";
911
+ var defaultIcons2 = {
912
+ default: CircleInfo2,
913
+ info: CircleInfo2,
914
+ success: CircleCheck2,
915
+ warning: Warning2,
916
+ error: CircleClose2
339
917
  };
340
- var Alert = React6.forwardRef(
341
- ({ className, variant = "default", icon, children, ...props }, ref) => /* @__PURE__ */ jsxs4(
918
+ var Toast = React9.forwardRef(function Toast2({
919
+ action,
920
+ children,
921
+ className,
922
+ dismissLabel = "Dismiss notification",
923
+ duration,
924
+ icon,
925
+ onBlurCapture,
926
+ onDismiss,
927
+ onFocusCapture,
928
+ onPointerEnter,
929
+ onPointerLeave,
930
+ role,
931
+ variant = "default",
932
+ ...props
933
+ }, ref) {
934
+ const [paused, setPaused] = React9.useState(false);
935
+ const dismissRef = React9.useRef(onDismiss);
936
+ const DefaultIcon = defaultIcons2[variant];
937
+ const resolvedDuration = duration === void 0 ? variant === "error" ? null : 5e3 : duration;
938
+ React9.useEffect(() => {
939
+ dismissRef.current = onDismiss;
940
+ }, [onDismiss]);
941
+ React9.useEffect(() => {
942
+ if (!dismissRef.current || paused || resolvedDuration === null || resolvedDuration <= 0) return;
943
+ const timeout = window.setTimeout(() => dismissRef.current?.(), resolvedDuration);
944
+ return () => window.clearTimeout(timeout);
945
+ }, [paused, resolvedDuration]);
946
+ const announcementRole = role ?? (variant === "error" ? "alert" : "status");
947
+ return /* @__PURE__ */ jsxs11(
342
948
  "div",
343
949
  {
344
950
  ref,
345
- role: "alert",
346
- className: cn("fractal-alert", `fractal-alert--${variant}`, className),
951
+ "aria-atomic": "true",
952
+ "aria-live": announcementRole === "alert" ? "assertive" : "polite",
953
+ className: cn("fractal-toast", `fractal-toast--${variant}`, className),
954
+ role: announcementRole,
955
+ onBlurCapture: (event) => {
956
+ onBlurCapture?.(event);
957
+ if (!event.currentTarget.contains(event.relatedTarget)) setPaused(false);
958
+ },
959
+ onFocusCapture: (event) => {
960
+ onFocusCapture?.(event);
961
+ setPaused(true);
962
+ },
963
+ onPointerEnter: (event) => {
964
+ onPointerEnter?.(event);
965
+ setPaused(true);
966
+ },
967
+ onPointerLeave: (event) => {
968
+ onPointerLeave?.(event);
969
+ setPaused(false);
970
+ },
347
971
  ...props,
348
972
  children: [
349
- /* @__PURE__ */ jsx5("span", { className: "fractal-alert-icon", "aria-hidden": true, children: icon ?? defaultIcons[variant] }),
350
- /* @__PURE__ */ jsx5("div", { className: "fractal-alert-content", children })
973
+ /* @__PURE__ */ jsx12("span", { "aria-hidden": "true", className: "fractal-toast__icon", children: icon ?? /* @__PURE__ */ jsx12(DefaultIcon, { size: 18 }) }),
974
+ /* @__PURE__ */ jsx12("div", { className: "fractal-toast__content", children }),
975
+ action && /* @__PURE__ */ jsx12("div", { className: "fractal-toast__action", children: action }),
976
+ /* @__PURE__ */ jsx12(
977
+ "button",
978
+ {
979
+ "aria-label": dismissLabel,
980
+ className: "fractal-toast__dismiss",
981
+ onClick: onDismiss,
982
+ type: "button",
983
+ children: /* @__PURE__ */ jsx12(Close, { "aria-hidden": "true", size: 14 })
984
+ }
985
+ )
351
986
  ]
352
987
  }
353
- )
354
- );
355
- Alert.displayName = "Alert";
356
- var AlertTitle = React6.forwardRef(
357
- ({ className, ...props }, ref) => /* @__PURE__ */ jsx5("h5", { ref, className: cn("fractal-alert-title", className), ...props })
358
- );
359
- AlertTitle.displayName = "AlertTitle";
360
- var AlertDescription = React6.forwardRef(
361
- ({ className, ...props }, ref) => /* @__PURE__ */ jsx5("div", { ref, className: cn("fractal-alert-description", className), ...props })
362
- );
363
- AlertDescription.displayName = "AlertDescription";
988
+ );
989
+ });
990
+ var ToastViewport = React9.forwardRef(function ToastViewport2({ children, className, portal = true, position = "top-center", ...props }, ref) {
991
+ const viewport = /* @__PURE__ */ jsx12(
992
+ "div",
993
+ {
994
+ ref,
995
+ className: cn("fractal-toast-viewport", `fractal-toast-viewport--${position}`, className),
996
+ "data-position": position,
997
+ ...props,
998
+ children
999
+ }
1000
+ );
1001
+ if (!portal || typeof document === "undefined") return viewport;
1002
+ return createPortal(viewport, document.body);
1003
+ });
364
1004
 
365
1005
  // src/components/Toggle/Toggle.tsx
366
- import { jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
1006
+ import { jsx as jsx13, jsxs as jsxs12 } from "react/jsx-runtime";
367
1007
  function Toggle({ checked, onChange, label, className }) {
368
- return /* @__PURE__ */ jsxs5(
1008
+ return /* @__PURE__ */ jsxs12(
369
1009
  "button",
370
1010
  {
371
1011
  type: "button",
@@ -374,18 +1014,19 @@ function Toggle({ checked, onChange, label, className }) {
374
1014
  className: cn("fractal-toggle", className),
375
1015
  onClick: () => onChange(!checked),
376
1016
  children: [
377
- /* @__PURE__ */ jsx6("span", { className: cn("fractal-toggle-track", checked && "fractal-toggle-track--checked"), children: /* @__PURE__ */ jsx6("span", { className: cn("fractal-toggle-thumb", checked && "fractal-toggle-thumb--checked") }) }),
378
- label && /* @__PURE__ */ jsx6("span", { className: "fractal-toggle-label", children: label })
1017
+ /* @__PURE__ */ jsx13("span", { className: cn("fractal-toggle-track", checked && "fractal-toggle-track--checked"), children: /* @__PURE__ */ jsx13("span", { className: cn("fractal-toggle-thumb", checked && "fractal-toggle-thumb--checked") }) }),
1018
+ label && /* @__PURE__ */ jsx13("span", { className: "fractal-toggle-label", children: label })
379
1019
  ]
380
1020
  }
381
1021
  );
382
1022
  }
383
1023
 
384
1024
  // src/components/Modal/Modal.tsx
385
- import { createPortal } from "react-dom";
1025
+ import * as React10 from "react";
1026
+ import { createPortal as createPortal2 } from "react-dom";
386
1027
 
387
1028
  // src/hooks/useEscapeDismiss.ts
388
- import { useEffect, useRef } from "react";
1029
+ import { useEffect as useEffect3, useRef as useRef4 } from "react";
389
1030
  var nextId = 0;
390
1031
  var stack = [];
391
1032
  function insertSorted(entry) {
@@ -418,9 +1059,9 @@ function ensureGlobalListener() {
418
1059
  );
419
1060
  }
420
1061
  function useEscapeDismiss(priority, handler, enabled = true) {
421
- const handlerRef = useRef(handler);
1062
+ const handlerRef = useRef4(handler);
422
1063
  handlerRef.current = handler;
423
- useEffect(() => {
1064
+ useEffect3(() => {
424
1065
  if (!enabled) return;
425
1066
  ensureGlobalListener();
426
1067
  const id = nextId++;
@@ -434,27 +1075,99 @@ function useEscapeDismiss(priority, handler, enabled = true) {
434
1075
  }, [priority, enabled]);
435
1076
  }
436
1077
 
1078
+ // src/components/UILoader/UILoader.tsx
1079
+ import { useId as useId2 } from "react";
1080
+ import { jsx as jsx14, jsxs as jsxs13 } from "react/jsx-runtime";
1081
+ var LEFT = "M0 53C0 51 1.6 49.5 3.6 49.5h16.5c1 0 1.9.4 2.5 1L42.1 69.6c.7.7 1.1 1.6 1.1 2.5v16.2c0 2-1.6 3.5-3.6 3.5H23.1c-1 0-1.9-.4-2.5-1L1.1 71.7C.4 71 0 70.1 0 69.2V53Z";
1082
+ var CENTER = "M0 3.5C0 1.6 1.6 0 3.6 0h16.5c1 0 1.9.4 2.5 1L92.5 69.6c.7.7 1 1.6 1 2.5v16.2c0 2-1.6 3.5-3.6 3.5H73.4c-1 0-1.9-.4-2.5-1L1.1 22.2C.4 21.6 0 20.7 0 19.7V3.5Z";
1083
+ var RIGHT = "M50.4 3.5C50.4 1.6 52 0 54 0h16.5c1 0 1.9.4 2.5 1l69.8 68.6c.7.7 1.1 1.6 1.1 2.5v16.2c0 2-1.6 3.5-3.6 3.5h-16.5c-1 0-1.9-.4-2.5-1L51.4 22.2c-.7-.6-1-1.5-1-2.5V3.5Z";
1084
+ function UILoader({ size = 48, tone = "neutral", className, style, label = "Loading", ariaHidden = false }) {
1085
+ const rawId = useId2().replace(/[^a-zA-Z0-9_-]/g, "");
1086
+ const clipId = `fractal-ui-loader-clip-${rawId}`;
1087
+ const gradientId = `fractal-ui-loader-gradient-${rawId}`;
1088
+ const height = typeof size === "number" ? `${size}px` : size;
1089
+ return /* @__PURE__ */ jsx14(
1090
+ "span",
1091
+ {
1092
+ "aria-hidden": ariaHidden || void 0,
1093
+ "aria-label": ariaHidden ? void 0 : label,
1094
+ className: cn("fractal-ui-loader", `fractal-ui-loader--${tone}`, className),
1095
+ role: ariaHidden ? void 0 : "status",
1096
+ style,
1097
+ children: /* @__PURE__ */ jsxs13("svg", { "aria-hidden": "true", fill: "none", viewBox: "0 0 144 92", style: { height, width: "auto" }, children: [
1098
+ /* @__PURE__ */ jsxs13("defs", { children: [
1099
+ /* @__PURE__ */ jsxs13("linearGradient", { id: gradientId, x1: "59", x2: "94.3", y1: "0", y2: "89.1", gradientUnits: "userSpaceOnUse", children: [
1100
+ /* @__PURE__ */ jsx14("stop", { className: "fractal-ui-loader__stop-start", offset: "0" }),
1101
+ /* @__PURE__ */ jsx14("stop", { className: "fractal-ui-loader__stop-end", offset: "1" })
1102
+ ] }),
1103
+ /* @__PURE__ */ jsxs13("linearGradient", { id: `${gradientId}-shine`, x1: "0", x2: "1", y1: "0", y2: "0", children: [
1104
+ /* @__PURE__ */ jsx14("stop", { offset: "0.4", stopColor: "white", stopOpacity: "0" }),
1105
+ /* @__PURE__ */ jsx14("stop", { offset: "0.5", stopColor: "white", stopOpacity: "0.62" }),
1106
+ /* @__PURE__ */ jsx14("stop", { offset: "0.6", stopColor: "white", stopOpacity: "0" })
1107
+ ] }),
1108
+ /* @__PURE__ */ jsxs13("clipPath", { id: clipId, children: [
1109
+ /* @__PURE__ */ jsx14("path", { d: LEFT }),
1110
+ /* @__PURE__ */ jsx14("path", { d: CENTER }),
1111
+ /* @__PURE__ */ jsx14("path", { d: RIGHT })
1112
+ ] })
1113
+ ] }),
1114
+ /* @__PURE__ */ jsx14("path", { d: CENTER, fill: `url(#${gradientId})` }),
1115
+ /* @__PURE__ */ jsx14("path", { d: RIGHT, fill: `url(#${gradientId})` }),
1116
+ /* @__PURE__ */ jsx14("path", { d: LEFT, fill: `url(#${gradientId})` }),
1117
+ /* @__PURE__ */ jsx14("g", { clipPath: `url(#${clipId})`, children: /* @__PURE__ */ jsx14("g", { className: "fractal-ui-loader__shine-axis", transform: "translate(72 46) rotate(68.4)", children: /* @__PURE__ */ jsx14("rect", { className: "fractal-ui-loader__shine", x: "-160", y: "-100", width: "320", height: "200", fill: `url(#${gradientId}-shine)` }) }) })
1118
+ ] })
1119
+ }
1120
+ );
1121
+ }
1122
+
437
1123
  // src/components/Modal/Modal.tsx
438
- import { jsx as jsx7, jsxs as jsxs6 } from "react/jsx-runtime";
439
- var CloseIcon = () => /* @__PURE__ */ jsx7("svg", { width: "16", height: "16", viewBox: "0 0 16 16", fill: "none", xmlns: "http://www.w3.org/2000/svg", children: /* @__PURE__ */ jsx7("path", { d: "M12 4L4 12M4 4L12 12", stroke: "currentColor", strokeWidth: "1.5", strokeLinecap: "round", strokeLinejoin: "round" }) });
440
- function Modal({ open, onClose, title, children, danger, warn, footer, noClose, size = "sm", className }) {
441
- useEscapeDismiss(80, onClose, open && !noClose);
1124
+ import { jsx as jsx15, jsxs as jsxs14 } from "react/jsx-runtime";
1125
+ var CloseIcon = () => /* @__PURE__ */ jsx15("svg", { width: "16", height: "16", viewBox: "0 0 16 16", fill: "none", xmlns: "http://www.w3.org/2000/svg", children: /* @__PURE__ */ jsx15("path", { d: "M12 4L4 12M4 4L12 12", stroke: "currentColor", strokeWidth: "1.5", strokeLinecap: "round", strokeLinejoin: "round" }) });
1126
+ function Modal({ open, onClose, title, children, danger, warn, footer, noClose, closeDisabled = false, size = "sm", loading = false, loadingLabel = "Loading", scrollable = false, className }) {
1127
+ const [bodyScrolled, setBodyScrolled] = React10.useState(false);
1128
+ useEscapeDismiss(80, onClose, open && !noClose && !closeDisabled);
1129
+ React10.useEffect(() => {
1130
+ if (open) setBodyScrolled(false);
1131
+ }, [open]);
442
1132
  if (!open) return null;
443
1133
  const sizeClass = `fractal-modal-panel--${size}`;
444
1134
  const accentClass = danger ? "fractal-modal-panel--danger" : warn ? "fractal-modal-panel--warn" : "";
445
- return createPortal(
446
- /* @__PURE__ */ jsx7("div", { className: "fractal-modal-overlay", onClick: noClose ? void 0 : onClose, children: /* @__PURE__ */ jsxs6(
1135
+ return createPortal2(
1136
+ /* @__PURE__ */ jsx15("div", { className: "fractal-modal-overlay", onClick: noClose || closeDisabled ? void 0 : onClose, children: /* @__PURE__ */ jsxs14(
447
1137
  "div",
448
1138
  {
449
- className: cn("fractal-modal-panel", sizeClass, accentClass, className),
1139
+ className: cn("fractal-modal-panel", sizeClass, accentClass, scrollable && "fractal-modal-panel--scrollable", className),
450
1140
  onClick: (e) => e.stopPropagation(),
451
1141
  children: [
452
- /* @__PURE__ */ jsxs6("div", { className: "fractal-modal-title-row", children: [
453
- /* @__PURE__ */ jsx7("h3", { className: cn("fractal-modal-title", danger && "fractal-modal-title--danger", warn && "fractal-modal-title--warn"), children: title }),
454
- !noClose && /* @__PURE__ */ jsx7("button", { type: "button", onClick: onClose, title: "Close", className: "fractal-modal-close", children: /* @__PURE__ */ jsx7(CloseIcon, {}) })
455
- ] }),
456
- /* @__PURE__ */ jsx7("div", { className: cn("fractal-modal-body", size === "lg" && "fractal-modal-body--lg"), children }),
457
- footer && /* @__PURE__ */ jsx7("div", { className: "fractal-modal-footer", children: footer })
1142
+ /* @__PURE__ */ jsxs14(
1143
+ "div",
1144
+ {
1145
+ className: cn(
1146
+ "fractal-modal-title-row",
1147
+ scrollable && "fractal-modal-title-row--scrollable",
1148
+ scrollable && bodyScrolled && "fractal-modal-title-row--scrolled"
1149
+ ),
1150
+ children: [
1151
+ /* @__PURE__ */ jsx15("h3", { className: cn("fractal-modal-title", danger && "fractal-modal-title--danger", warn && "fractal-modal-title--warn"), children: title }),
1152
+ !noClose && /* @__PURE__ */ jsx15("button", { type: "button", onClick: onClose, title: "Close", disabled: closeDisabled, className: "fractal-modal-close", children: /* @__PURE__ */ jsx15(CloseIcon, {}) })
1153
+ ]
1154
+ }
1155
+ ),
1156
+ /* @__PURE__ */ jsx15(
1157
+ "div",
1158
+ {
1159
+ "aria-busy": loading || void 0,
1160
+ className: cn(
1161
+ "fractal-modal-body",
1162
+ size === "lg" && "fractal-modal-body--lg",
1163
+ loading && "fractal-modal-body--loading",
1164
+ scrollable && "fractal-modal-body--scrollable"
1165
+ ),
1166
+ onScroll: scrollable ? (event) => setBodyScrolled(event.currentTarget.scrollTop > 0) : void 0,
1167
+ children: loading ? /* @__PURE__ */ jsx15(UILoader, { label: loadingLabel }) : children
1168
+ }
1169
+ ),
1170
+ footer && /* @__PURE__ */ jsx15("div", { className: cn("fractal-modal-footer", scrollable && "fractal-modal-footer--divided"), children: footer })
458
1171
  ]
459
1172
  }
460
1173
  ) }),
@@ -462,94 +1175,441 @@ function Modal({ open, onClose, title, children, danger, warn, footer, noClose,
462
1175
  );
463
1176
  }
464
1177
 
1178
+ // src/components/TableLayout/TableOverflowMenu.tsx
1179
+ import { useId as useId3, useLayoutEffect, useRef as useRef5, useState as useState5 } from "react";
1180
+ import { createPortal as createPortal3 } from "react-dom";
1181
+ import { Ellipsis } from "@mirror-physics/fractal-icons";
1182
+ import { jsx as jsx16, jsxs as jsxs15 } from "react/jsx-runtime";
1183
+ function TableOverflowMenu({ items, label, disabled, onSelect }) {
1184
+ const id = useId3();
1185
+ const trigger = useRef5(null);
1186
+ const panel = useRef5(null);
1187
+ const initialLast = useRef5(false);
1188
+ const [open, setOpen] = useState5(false);
1189
+ const [position, setPosition] = useState5({ top: 0, left: 0 });
1190
+ const buttons = () => [...panel.current?.querySelectorAll("button:not(:disabled)") ?? []];
1191
+ const close = (restore = true) => {
1192
+ setOpen(false);
1193
+ if (restore) trigger.current?.focus({ preventScroll: true });
1194
+ };
1195
+ useEscapeDismiss(100, () => close(), open);
1196
+ useLayoutEffect(() => {
1197
+ if (!open || !trigger.current || !panel.current) return;
1198
+ const place = () => {
1199
+ const anchor = trigger.current.getBoundingClientRect();
1200
+ const menu = panel.current.getBoundingClientRect();
1201
+ const above = window.innerHeight - anchor.bottom < menu.height + 8 && anchor.top > window.innerHeight - anchor.bottom;
1202
+ setPosition({
1203
+ top: Math.max(8, Math.min(above ? anchor.top - menu.height - 4 : anchor.bottom + 4, window.innerHeight - menu.height - 8)),
1204
+ left: Math.max(8, Math.min(anchor.right - menu.width, window.innerWidth - menu.width - 8))
1205
+ });
1206
+ };
1207
+ place();
1208
+ const enabled = buttons();
1209
+ (initialLast.current ? enabled[enabled.length - 1] : enabled[0])?.focus({ preventScroll: true });
1210
+ if (enabled.length === 0) panel.current.focus({ preventScroll: true });
1211
+ const outside = (event) => {
1212
+ if (!panel.current?.contains(event.target) && !trigger.current?.contains(event.target)) setOpen(false);
1213
+ };
1214
+ const scroll = (event) => {
1215
+ if (!panel.current?.contains(event.target)) setOpen(false);
1216
+ };
1217
+ const observer = new ResizeObserver(place);
1218
+ observer.observe(panel.current);
1219
+ document.addEventListener("pointerdown", outside);
1220
+ window.addEventListener("scroll", scroll, true);
1221
+ window.addEventListener("resize", place);
1222
+ return () => {
1223
+ observer.disconnect();
1224
+ document.removeEventListener("pointerdown", outside);
1225
+ window.removeEventListener("scroll", scroll, true);
1226
+ window.removeEventListener("resize", place);
1227
+ };
1228
+ }, [open]);
1229
+ useLayoutEffect(() => {
1230
+ if (disabled || items.length === 0) setOpen(false);
1231
+ }, [disabled, items.length]);
1232
+ if (items.length === 0) return null;
1233
+ return /* @__PURE__ */ jsxs15("div", { className: "fractal-table-overflow-trigger", children: [
1234
+ /* @__PURE__ */ jsx16(
1235
+ Button,
1236
+ {
1237
+ ref: trigger,
1238
+ buttonType: "ghost",
1239
+ controlSize: "sm",
1240
+ hasIconOnly: true,
1241
+ icon: /* @__PURE__ */ jsx16(Ellipsis, { "aria-hidden": true }),
1242
+ "aria-label": label,
1243
+ "aria-haspopup": "menu",
1244
+ "aria-expanded": open,
1245
+ "aria-controls": open ? id : void 0,
1246
+ disabled,
1247
+ onClick: () => {
1248
+ initialLast.current = false;
1249
+ setOpen(!open);
1250
+ },
1251
+ onKeyDown: (event) => {
1252
+ if (event.key === "ArrowDown" || event.key === "ArrowUp") {
1253
+ event.preventDefault();
1254
+ initialLast.current = event.key === "ArrowUp";
1255
+ setOpen(true);
1256
+ }
1257
+ }
1258
+ }
1259
+ ),
1260
+ open && createPortal3(/* @__PURE__ */ jsx16(
1261
+ "div",
1262
+ {
1263
+ ref: panel,
1264
+ id,
1265
+ role: "menu",
1266
+ tabIndex: -1,
1267
+ "aria-label": label,
1268
+ className: "fractal-table-overflow-menu",
1269
+ style: position,
1270
+ onClick: (event) => event.stopPropagation(),
1271
+ onKeyDown: (event) => {
1272
+ event.stopPropagation();
1273
+ const enabled = buttons(), index = enabled.indexOf(document.activeElement);
1274
+ if (event.key === "Escape") {
1275
+ event.preventDefault();
1276
+ close();
1277
+ } else if (event.key === "Tab") {
1278
+ close();
1279
+ } else if (["ArrowDown", "ArrowUp", "Home", "End"].includes(event.key)) {
1280
+ event.preventDefault();
1281
+ const next = event.key === "Home" ? 0 : event.key === "End" ? enabled.length - 1 : (index + (event.key === "ArrowDown" ? 1 : -1) + enabled.length) % enabled.length;
1282
+ enabled[next]?.focus();
1283
+ } else if (event.key.length === 1 && /\S/.test(event.key)) {
1284
+ const ordered = [...enabled.slice(index + 1), ...enabled.slice(0, index + 1)];
1285
+ ordered.find((button) => button.textContent?.trim().toLocaleLowerCase().startsWith(event.key.toLocaleLowerCase()))?.focus();
1286
+ }
1287
+ },
1288
+ children: items.map((item) => /* @__PURE__ */ jsxs15(
1289
+ "button",
1290
+ {
1291
+ type: "button",
1292
+ role: "menuitem",
1293
+ tabIndex: -1,
1294
+ disabled: item.disabled,
1295
+ onClick: () => {
1296
+ onSelect(item.value);
1297
+ close();
1298
+ },
1299
+ children: [
1300
+ item.icon && /* @__PURE__ */ jsx16("span", { "aria-hidden": "true", children: item.icon }),
1301
+ item.label
1302
+ ]
1303
+ },
1304
+ item.value
1305
+ ))
1306
+ }
1307
+ ), document.body)
1308
+ ] });
1309
+ }
1310
+
1311
+ // src/components/TableLayout/TableLayout.tsx
1312
+ import { useLayoutEffect as useLayoutEffect2, useRef as useRef6, useState as useState6 } from "react";
1313
+ import { jsx as jsx17 } from "react/jsx-runtime";
1314
+ var length = (value) => typeof value === "number" ? `${value}px` : value;
1315
+ function columnStyle(column) {
1316
+ return {
1317
+ width: column.width,
1318
+ "--table-column-width": !column.width || column.width.includes("%") ? void 0 : `calc(${column.width} - 1px)`,
1319
+ "--table-column-min": length(column.minWidth),
1320
+ "--table-column-max": length(column.maxWidth)
1321
+ };
1322
+ }
1323
+ function useOverflow() {
1324
+ const ref = useRef6(null);
1325
+ const [overflow, setOverflow] = useState6(false);
1326
+ useLayoutEffect2(() => {
1327
+ const el = ref.current;
1328
+ if (!el) return;
1329
+ const measure = () => setOverflow(el.scrollWidth > el.clientWidth + 1 || el.scrollHeight > el.clientHeight + 1);
1330
+ const resize = new ResizeObserver(measure);
1331
+ const observe = () => {
1332
+ resize.disconnect();
1333
+ resize.observe(el);
1334
+ for (const child of el.children) resize.observe(child);
1335
+ measure();
1336
+ };
1337
+ const mutation = new MutationObserver(observe);
1338
+ mutation.observe(el, { childList: true, subtree: true, characterData: true });
1339
+ observe();
1340
+ return () => {
1341
+ resize.disconnect();
1342
+ mutation.disconnect();
1343
+ };
1344
+ }, []);
1345
+ return { ref, overflow };
1346
+ }
1347
+ function TableViewport({ children, maxHeight, label }) {
1348
+ const { ref, overflow } = useOverflow();
1349
+ return /* @__PURE__ */ jsx17("div", { ref, className: "fractal-table-viewport", style: { maxHeight }, tabIndex: overflow ? 0 : void 0, role: overflow ? "region" : void 0, "aria-label": overflow ? `${label}, scrollable table` : void 0, children });
1350
+ }
1351
+ function TableCellContent({ children, label }) {
1352
+ const { ref, overflow } = useOverflow();
1353
+ useLayoutEffect2(() => {
1354
+ const cell = ref.current;
1355
+ if (!cell) return;
1356
+ const wheel = (event) => {
1357
+ if (event.defaultPrevented || event.ctrlKey || !event.cancelable || Math.abs(event.deltaX) <= Math.abs(event.deltaY)) return;
1358
+ const range = cell.scrollWidth - cell.clientWidth;
1359
+ if (range <= 1) return;
1360
+ const rtl = getComputedStyle(cell).direction === "rtl";
1361
+ const left = rtl ? -range : 0, right = rtl ? 0 : range;
1362
+ if (event.deltaX > 0 ? cell.scrollLeft < right - 1 : cell.scrollLeft > left + 1) return;
1363
+ const table = cell.closest(".fractal-table-viewport");
1364
+ if (!table) return;
1365
+ const tableRange = table.scrollWidth - table.clientWidth;
1366
+ const tableRtl = getComputedStyle(table).direction === "rtl";
1367
+ const tableLeft = tableRtl ? -tableRange : 0, tableRight = tableRtl ? 0 : tableRange;
1368
+ if (event.deltaX > 0 ? table.scrollLeft >= tableRight - 1 : table.scrollLeft <= tableLeft + 1) return;
1369
+ const unit = event.deltaMode === 1 ? parseFloat(getComputedStyle(cell).lineHeight) || 16 : event.deltaMode === 2 ? table.clientWidth : 1;
1370
+ event.preventDefault();
1371
+ table.scrollLeft += event.deltaX * unit;
1372
+ };
1373
+ cell.addEventListener("wheel", wheel, { passive: false });
1374
+ return () => cell.removeEventListener("wheel", wheel);
1375
+ }, [ref]);
1376
+ return /* @__PURE__ */ jsx17(
1377
+ "div",
1378
+ {
1379
+ ref,
1380
+ className: "fractal-table-cell-scroll",
1381
+ "data-overflow": overflow || void 0,
1382
+ tabIndex: overflow ? 0 : void 0,
1383
+ role: overflow ? "region" : void 0,
1384
+ "aria-label": overflow ? `${label}, scroll horizontally for full content` : void 0,
1385
+ onKeyDown: (event) => {
1386
+ if (!overflow || event.target !== event.currentTarget) return;
1387
+ if (["ArrowLeft", "ArrowRight", "Home", "End"].includes(event.key)) {
1388
+ event.preventDefault();
1389
+ event.stopPropagation();
1390
+ const el = event.currentTarget;
1391
+ if (event.key === "ArrowLeft" || event.key === "ArrowRight") el.scrollBy({ left: event.key === "ArrowLeft" ? -40 : 40 });
1392
+ else el.scrollLeft = event.key === "Home" ? 0 : el.scrollWidth * (getComputedStyle(el).direction === "rtl" ? -1 : 1);
1393
+ } else if (event.key === " " || event.key === "Enter") event.stopPropagation();
1394
+ },
1395
+ children: /* @__PURE__ */ jsx17("div", { className: "fractal-table-cell-content", children })
1396
+ }
1397
+ );
1398
+ }
1399
+
465
1400
  // src/components/DataTable/DataTable.tsx
466
- import { jsx as jsx8, jsxs as jsxs7 } from "react/jsx-runtime";
467
- function DataTable({ columns, data, emptyMessage, header, size = "md", className }) {
468
- return /* @__PURE__ */ jsx8("div", { className: cn("fractal-datatable", `fractal-datatable--${size}`, className), children: /* @__PURE__ */ jsxs7("div", { className: "fractal-datatable-surface", children: [
469
- header && /* @__PURE__ */ jsx8("div", { className: "fractal-datatable-header", children: header }),
470
- data.length === 0 ? /* @__PURE__ */ jsx8("div", { className: "fractal-datatable-empty", children: emptyMessage || "No data" }) : /* @__PURE__ */ jsxs7("table", { className: "fractal-datatable-table", children: [
471
- /* @__PURE__ */ jsx8("thead", { children: /* @__PURE__ */ jsx8("tr", { className: "fractal-datatable-thead-row", children: columns.map((col) => /* @__PURE__ */ jsx8("th", { className: "fractal-datatable-th", style: { width: col.width }, children: col.header }, col.key)) }) }),
472
- /* @__PURE__ */ jsx8("tbody", { children: data.map((row, rowIndex) => /* @__PURE__ */ jsx8(
1401
+ import { jsx as jsx18, jsxs as jsxs16 } from "react/jsx-runtime";
1402
+ function DataTable({ columns, data, emptyMessage, header, size = "md", className, overflowMenu, layout = "fill", stickyHeader = false, stickyFirstColumn = false, maxHeight, "aria-label": ariaLabel = "Data table" }) {
1403
+ return /* @__PURE__ */ jsx18("div", { className: cn("fractal-table-layout", "fractal-datatable", `fractal-datatable--${size}`, className), "data-overflow-menu": Boolean(overflowMenu), "data-table-layout": layout, "data-sticky-first": stickyFirstColumn, "data-sticky-header": stickyHeader, children: /* @__PURE__ */ jsxs16("div", { className: "fractal-datatable-surface", children: [
1404
+ header && /* @__PURE__ */ jsx18("div", { className: "fractal-datatable-header", children: header }),
1405
+ data.length === 0 ? /* @__PURE__ */ jsx18("div", { className: "fractal-datatable-empty", children: emptyMessage || "No data" }) : /* @__PURE__ */ jsx18(TableViewport, { maxHeight, label: ariaLabel, children: /* @__PURE__ */ jsxs16("table", { "aria-label": ariaLabel, className: "fractal-datatable-table", children: [
1406
+ /* @__PURE__ */ jsx18("thead", { children: /* @__PURE__ */ jsxs16("tr", { className: "fractal-datatable-thead-row", children: [
1407
+ columns.map((col, columnIndex) => /* @__PURE__ */ jsx18("th", { scope: "col", className: "fractal-datatable-th", style: columnStyle(col), "data-pinned-column": columnIndex === 0 ? "data" : void 0, children: /* @__PURE__ */ jsx18(TableCellContent, { label: `${col.header} column`, children: col.header }) }, col.key)),
1408
+ overflowMenu && /* @__PURE__ */ jsx18("th", { scope: "col", "aria-label": "Row actions", className: "fractal-datatable-th fractal-table-overflow-cell", "data-pinned-column": "overflow" })
1409
+ ] }) }),
1410
+ /* @__PURE__ */ jsx18("tbody", { children: data.map((row, rowIndex) => /* @__PURE__ */ jsxs16(
473
1411
  "tr",
474
1412
  {
475
1413
  className: "fractal-datatable-row",
476
- children: columns.map((col) => /* @__PURE__ */ jsx8("td", { className: "fractal-datatable-td", style: { width: col.width }, children: col.render(row, rowIndex) }, col.key))
1414
+ children: [
1415
+ columns.map((col, columnIndex) => /* @__PURE__ */ jsx18("td", { className: "fractal-datatable-td", style: columnStyle(col), "data-pinned-column": columnIndex === 0 ? "data" : void 0, children: /* @__PURE__ */ jsx18(TableCellContent, { label: `${col.header || "Value"}, row ${rowIndex + 1}`, children: col.render(row, rowIndex) }) }, col.key)),
1416
+ overflowMenu && /* @__PURE__ */ jsx18("td", { className: "fractal-datatable-td fractal-table-overflow-cell", "data-pinned-column": "overflow", children: /* @__PURE__ */ jsx18(TableOverflowMenu, { items: overflowMenu.items(row, rowIndex), label: overflowMenu.getLabel?.(row, rowIndex) ?? `Actions for row ${rowIndex + 1}`, disabled: overflowMenu.isDisabled?.(row, rowIndex), onSelect: (value) => overflowMenu.onSelect(value, row, rowIndex) }) })
1417
+ ]
477
1418
  },
478
1419
  rowIndex
479
1420
  )) })
480
- ] })
1421
+ ] }) })
481
1422
  ] }) });
482
1423
  }
483
1424
 
484
1425
  // src/components/Dropdown/Dropdown.tsx
485
- import * as React7 from "react";
486
- import { ChevronDown, ChevronUp } from "@mirror-physics/fractal-icons";
487
- import { Fragment as Fragment2, jsx as jsx9, jsxs as jsxs8 } from "react/jsx-runtime";
488
- var MARGIN = 4;
1426
+ import * as React11 from "react";
1427
+ import { Check as Check2, MagnifyingGlass as MagnifyingGlass2, ChevronDown, ChevronLeft, ChevronRight as ChevronRight2, ChevronUp } from "@mirror-physics/fractal-icons";
1428
+
1429
+ // src/components/Dropdown/position.ts
1430
+ function positionDropdown(trigger, panel, bounds, direction, align) {
1431
+ const gap = 4;
1432
+ const width = Math.min(panel.width, Math.max(0, bounds.right - bounds.left));
1433
+ let maxHeight = Math.max(0, bounds.bottom - bounds.top);
1434
+ let left;
1435
+ let top;
1436
+ if (direction === "left" || direction === "right") {
1437
+ const after = bounds.right - trigger.right - gap;
1438
+ const before = trigger.left - bounds.left - gap;
1439
+ let placeRight = direction === "right";
1440
+ if (width > (placeRight ? after : before) && (placeRight ? before > after : after > before)) {
1441
+ placeRight = !placeRight;
1442
+ }
1443
+ left = placeRight ? trigger.right + gap : trigger.left - width - gap;
1444
+ top = trigger.top;
1445
+ } else {
1446
+ const below = Math.max(0, bounds.bottom - trigger.bottom - gap);
1447
+ const above = Math.max(0, trigger.top - bounds.top - gap);
1448
+ let placeAbove = direction === "up";
1449
+ if (panel.height > (placeAbove ? above : below) && (placeAbove ? below > above : above > below)) {
1450
+ placeAbove = !placeAbove;
1451
+ }
1452
+ maxHeight = Math.min(maxHeight, placeAbove ? above : below);
1453
+ top = placeAbove ? trigger.top - Math.min(panel.height, maxHeight) - gap : trigger.bottom + gap;
1454
+ left = align === "right" ? trigger.right - width : trigger.left;
1455
+ }
1456
+ const height = Math.min(panel.height, maxHeight);
1457
+ return {
1458
+ left: Math.max(bounds.left, Math.min(left, bounds.right - width)),
1459
+ top: Math.max(bounds.top, Math.min(top, bounds.bottom - height)),
1460
+ maxHeight
1461
+ };
1462
+ }
1463
+
1464
+ // src/components/Dropdown/Dropdown.tsx
1465
+ import { Fragment as Fragment2, jsx as jsx19, jsxs as jsxs17 } from "react/jsx-runtime";
489
1466
  function Dropdown({
490
1467
  items,
491
- selectedValue,
1468
+ selectedValue = "",
492
1469
  onSelect,
493
- triggerLabel = "Options",
1470
+ multiple = false,
1471
+ selectedValues = [],
1472
+ onSelectionChange,
1473
+ searchable = false,
1474
+ searchPlaceholder = "Find an option",
1475
+ emptyMessage = "No matching options.",
1476
+ triggerLabel,
1477
+ fieldLabel,
494
1478
  direction = "down",
495
1479
  align = "left",
1480
+ maxHeight = 320,
1481
+ collisionPadding = 8,
1482
+ triggerOrientation = "horizontal",
496
1483
  triggerContent,
497
1484
  label,
1485
+ iconOnly = false,
498
1486
  noChevron = false,
499
- size = "md",
1487
+ size: sizeAlias = "lg",
1488
+ controlSize,
1489
+ triggerId,
500
1490
  triggerVariant = "outline",
501
1491
  className,
502
1492
  triggerClassName,
503
1493
  panelClassName,
504
- closeOnSelect = true
1494
+ closeOnSelect = true,
1495
+ disabled = false
505
1496
  }) {
506
- const [open, setOpen] = React7.useState(false);
507
- const triggerRef = React7.useRef(null);
508
- const panelRef = React7.useRef(null);
509
- const [panelStyle, setPanelStyle] = React7.useState({});
510
- const visibleItems = items.filter(
1497
+ const [query, setQuery] = React11.useState("");
1498
+ const searchRef = React11.useRef(null);
1499
+ const accessibleLabel = triggerLabel ?? fieldLabel ?? "Options";
1500
+ const size = controlSize ?? sizeAlias;
1501
+ const [open, setOpen] = React11.useState(false);
1502
+ React11.useEffect(() => {
1503
+ if (!open) setQuery("");
1504
+ }, [open]);
1505
+ const rootRef = React11.useRef(null);
1506
+ const menuId = React11.useId();
1507
+ const focusOnOpen = React11.useRef(false);
1508
+ const triggerRef = React11.useRef(null);
1509
+ const panelRef = React11.useRef(null);
1510
+ const [panelStyle, setPanelStyle] = React11.useState({});
1511
+ const renderableItems = items.filter(
511
1512
  (item) => item.icon != null || item.description != null && (typeof item.description === "string" ? item.description !== "" : true)
512
1513
  );
513
- if (visibleItems.length === 0) return null;
1514
+ const normalizedQuery = query.trim().toLocaleLowerCase();
1515
+ let groupTitle = "";
1516
+ const matching = new Set(renderableItems.filter((item) => {
1517
+ if (item.kind === "header") {
1518
+ groupTitle = String(item.description ?? "");
1519
+ return false;
1520
+ }
1521
+ return `${groupTitle} ${typeof item.description === "string" ? item.description : item.value}`.toLocaleLowerCase().includes(normalizedQuery);
1522
+ }).map((item) => item.value));
1523
+ const visibleItems = renderableItems.filter((item, index) => {
1524
+ if (item.kind !== "header") return matching.has(item.value);
1525
+ const nextHeader = renderableItems.findIndex((next, i) => i > index && next.kind === "header");
1526
+ return renderableItems.slice(index + 1, nextHeader < 0 ? void 0 : nextHeader).some((next) => matching.has(next.value));
1527
+ });
514
1528
  const selectableItems = visibleItems.filter((i) => i.kind !== "header");
515
1529
  const selected = items.find((i) => i.value === selectedValue && i.kind !== "header") ?? selectableItems[0] ?? visibleItems[0];
516
- const isUp = direction === "up";
517
- const ChevronIcon = isUp ? ChevronUp : ChevronDown;
518
- React7.useLayoutEffect(() => {
519
- if (!open || !triggerRef.current || !panelRef.current) return;
520
- const triggerRect = triggerRef.current.getBoundingClientRect();
521
- const panelRect = panelRef.current.getBoundingClientRect();
522
- const vw = window.innerWidth;
523
- const vh = window.innerHeight;
524
- const spaceBelow = vh - triggerRect.bottom - MARGIN;
525
- const spaceAbove = triggerRect.top - MARGIN;
526
- let placeAbove = isUp;
527
- if (placeAbove && panelRect.height > spaceAbove && spaceBelow > spaceAbove) {
528
- placeAbove = false;
529
- } else if (!placeAbove && panelRect.height > spaceBelow && spaceAbove > spaceBelow) {
530
- placeAbove = true;
531
- }
532
- const top = placeAbove ? -(panelRect.height + MARGIN) : triggerRect.height + MARGIN;
533
- let left;
534
- let right;
535
- if (align === "right") {
536
- right = 0;
537
- const panelLeft = triggerRect.right - panelRect.width;
538
- if (panelLeft < 0) {
539
- right = void 0;
540
- left = 0;
1530
+ const ChevronIcon = direction === "up" ? ChevronUp : direction === "left" ? ChevronLeft : direction === "right" ? ChevronRight2 : ChevronDown;
1531
+ useEscapeDismiss(100, () => {
1532
+ setOpen(false);
1533
+ triggerRef.current?.focus();
1534
+ }, open);
1535
+ React11.useLayoutEffect(() => {
1536
+ const root = rootRef.current;
1537
+ const trigger = triggerRef.current;
1538
+ const panel = panelRef.current;
1539
+ if (!open || !root || !trigger || !panel) return;
1540
+ const updatePosition = () => {
1541
+ const viewport = window.visualViewport;
1542
+ const bounds = {
1543
+ left: viewport?.offsetLeft ?? 0,
1544
+ top: viewport?.offsetTop ?? 0,
1545
+ right: (viewport?.offsetLeft ?? 0) + (viewport?.width ?? document.documentElement.clientWidth),
1546
+ bottom: (viewport?.offsetTop ?? 0) + (viewport?.height ?? document.documentElement.clientHeight)
1547
+ };
1548
+ for (let ancestor = root.parentElement; ancestor; ancestor = ancestor.parentElement) {
1549
+ const style = getComputedStyle(ancestor);
1550
+ const rect = ancestor.getBoundingClientRect();
1551
+ if (/(auto|scroll|hidden|clip)/.test(style.overflowX)) {
1552
+ bounds.left = Math.max(bounds.left, rect.left + ancestor.clientLeft);
1553
+ bounds.right = Math.min(bounds.right, rect.left + ancestor.clientLeft + ancestor.clientWidth);
1554
+ }
1555
+ if (/(auto|scroll|hidden|clip)/.test(style.overflowY)) {
1556
+ bounds.top = Math.max(bounds.top, rect.top + ancestor.clientTop);
1557
+ bounds.bottom = Math.min(bounds.bottom, rect.top + ancestor.clientTop + ancestor.clientHeight);
1558
+ }
541
1559
  }
542
- } else {
543
- left = 0;
544
- const panelRight = triggerRect.left + panelRect.width;
545
- if (panelRight > vw) {
546
- left = void 0;
547
- right = 0;
1560
+ const padding = Math.max(0, collisionPadding);
1561
+ bounds.left += padding;
1562
+ bounds.right = Math.max(bounds.left, bounds.right - padding);
1563
+ bounds.top += padding;
1564
+ bounds.bottom = Math.max(bounds.top, bounds.bottom - padding);
1565
+ const triggerRect = trigger.getBoundingClientRect();
1566
+ const rootRect = root.getBoundingClientRect();
1567
+ const availableWidth = bounds.right - bounds.left;
1568
+ const heightLimit = Math.max(0, maxHeight);
1569
+ panel.style.maxWidth = `${availableWidth}px`;
1570
+ panel.style.minWidth = `${Math.min(triggerRect.width, availableWidth)}px`;
1571
+ const panelRect = panel.getBoundingClientRect();
1572
+ const desiredHeight = Math.min(heightLimit, panel.scrollHeight + panel.offsetHeight - panel.clientHeight);
1573
+ const position = positionDropdown(triggerRect, { width: panelRect.width, height: desiredHeight }, bounds, direction, align);
1574
+ const nextStyle = {
1575
+ left: position.left - rootRect.left - root.clientLeft + root.scrollLeft,
1576
+ top: position.top - rootRect.top - root.clientTop + root.scrollTop,
1577
+ minWidth: Math.min(triggerRect.width, availableWidth),
1578
+ maxWidth: availableWidth,
1579
+ maxHeight: Math.min(heightLimit, position.maxHeight)
1580
+ };
1581
+ for (const key of Object.keys(nextStyle)) {
1582
+ panel.style[key] = `${nextStyle[key]}px`;
548
1583
  }
1584
+ setPanelStyle((previous) => Object.entries(nextStyle).every(([key, value]) => previous[key] === value) ? previous : nextStyle);
1585
+ };
1586
+ updatePosition();
1587
+ if (searchable && !panel.contains(document.activeElement)) searchRef.current?.focus({ preventScroll: true });
1588
+ if (focusOnOpen.current) {
1589
+ focusOnOpen.current = false;
1590
+ const option = panel.querySelector('[aria-selected="true"]') ?? panel.querySelector('[role="option"]');
1591
+ option?.focus({ preventScroll: true });
1592
+ option?.scrollIntoView({ block: "nearest" });
1593
+ }
1594
+ const resize = new ResizeObserver(updatePosition);
1595
+ resize.observe(root);
1596
+ resize.observe(panel);
1597
+ for (let ancestor = root.parentElement; ancestor; ancestor = ancestor.parentElement) {
1598
+ resize.observe(ancestor);
549
1599
  }
550
- setPanelStyle({ top, left, right });
551
- }, [open, isUp, align]);
552
- React7.useEffect(() => {
1600
+ window.addEventListener("resize", updatePosition);
1601
+ window.addEventListener("scroll", updatePosition, true);
1602
+ window.visualViewport?.addEventListener("resize", updatePosition);
1603
+ window.visualViewport?.addEventListener("scroll", updatePosition);
1604
+ return () => {
1605
+ resize.disconnect();
1606
+ window.removeEventListener("resize", updatePosition);
1607
+ window.removeEventListener("scroll", updatePosition, true);
1608
+ window.visualViewport?.removeEventListener("resize", updatePosition);
1609
+ window.visualViewport?.removeEventListener("scroll", updatePosition);
1610
+ };
1611
+ }, [open, direction, align, maxHeight, collisionPadding, items, label, query, searchable]);
1612
+ React11.useEffect(() => {
553
1613
  if (!open) return;
554
1614
  const handleClickOutside = (e) => {
555
1615
  const target = e.target;
@@ -559,75 +1619,120 @@ function Dropdown({
559
1619
  document.addEventListener("mousedown", handleClickOutside);
560
1620
  return () => document.removeEventListener("mousedown", handleClickOutside);
561
1621
  }, [open]);
562
- return /* @__PURE__ */ jsxs8("div", { className: cn("fractal-dropdown", className), children: [
563
- /* @__PURE__ */ jsxs8(
1622
+ if (renderableItems.length === 0) return null;
1623
+ return /* @__PURE__ */ jsxs17("div", { ref: rootRef, "data-control-size": size, "data-filled": multiple ? selectedValues.length > 0 : Boolean(selectedValue), "data-open": open, className: cn("fractal-dropdown", fieldLabel && "fractal-floating-field fractal-floating-field--dropdown", className), children: [
1624
+ /* @__PURE__ */ jsxs17(
564
1625
  "button",
565
1626
  {
566
1627
  ref: triggerRef,
1628
+ id: triggerId ?? `${menuId}-trigger`,
1629
+ "data-control-size": size,
567
1630
  type: "button",
568
- "aria-haspopup": "listbox",
1631
+ disabled,
1632
+ "aria-haspopup": searchable ? "dialog" : "listbox",
569
1633
  "aria-expanded": open,
570
- "aria-label": triggerLabel,
1634
+ "aria-controls": open ? searchable ? menuId : `${menuId}-options` : void 0,
1635
+ onKeyDown: (event) => {
1636
+ if (event.key === "ArrowDown" || event.key === "ArrowUp") {
1637
+ event.preventDefault();
1638
+ if (open) {
1639
+ const option = panelRef.current?.querySelector('[aria-selected="true"]') ?? panelRef.current?.querySelector('[role="option"]');
1640
+ option?.focus({ preventScroll: true });
1641
+ option?.scrollIntoView({ block: "nearest" });
1642
+ } else {
1643
+ focusOnOpen.current = true;
1644
+ setOpen(true);
1645
+ }
1646
+ }
1647
+ },
1648
+ "aria-label": accessibleLabel,
571
1649
  onClick: () => setOpen((o) => !o),
572
1650
  className: cn(
1651
+ "fractal-control",
573
1652
  "fractal-dropdown-trigger",
574
1653
  `fractal-dropdown-trigger--${size}`,
575
1654
  `fractal-dropdown-trigger--${triggerVariant}`,
1655
+ `fractal-dropdown-trigger--${triggerOrientation}`,
1656
+ iconOnly && "fractal-dropdown-trigger--icon-only",
576
1657
  triggerClassName
577
1658
  ),
578
1659
  children: [
579
- triggerContent != null ? triggerContent : /* @__PURE__ */ jsxs8(Fragment2, { children: [
580
- selected.icon != null && /* @__PURE__ */ jsx9("span", { className: "fractal-dropdown-icon fractal-dropdown-icon--trigger", children: selected.icon }),
581
- selected.description != null && (typeof selected.description !== "string" || selected.description !== "") && /* @__PURE__ */ jsx9("span", { className: "fractal-dropdown-selected-text", children: selected.description })
582
- ] }),
583
- !noChevron && /* @__PURE__ */ jsx9("span", { className: "fractal-dropdown-chevron", "aria-hidden": true, children: /* @__PURE__ */ jsx9(ChevronIcon, { size: size === "sm" ? 14 : 16 }) })
1660
+ /* @__PURE__ */ jsx19("span", { className: fieldLabel ? "fractal-dropdown-field-value" : void 0, style: fieldLabel ? void 0 : { display: "contents" }, children: triggerContent != null ? triggerContent : /* @__PURE__ */ jsxs17(Fragment2, { children: [
1661
+ selected?.icon != null && /* @__PURE__ */ jsx19("span", { className: "fractal-dropdown-icon fractal-dropdown-icon--trigger", children: selected?.icon }),
1662
+ selected?.description != null && (typeof selected?.description !== "string" || selected?.description !== "") && /* @__PURE__ */ jsx19("span", { className: "fractal-dropdown-selected-text", children: selected?.description })
1663
+ ] }) }),
1664
+ !noChevron && !iconOnly && /* @__PURE__ */ jsx19("span", { className: "fractal-dropdown-chevron", "aria-hidden": true, children: /* @__PURE__ */ jsx19(ChevronIcon, { size: size === "sm" ? 14 : 16 }) })
584
1665
  ]
585
1666
  }
586
1667
  ),
587
- open && /* @__PURE__ */ jsxs8(
1668
+ fieldLabel ? /* @__PURE__ */ jsx19("label", { className: "fractal-floating-field__label", htmlFor: triggerId ?? `${menuId}-trigger`, children: fieldLabel }) : null,
1669
+ open && /* @__PURE__ */ jsxs17(
588
1670
  "div",
589
1671
  {
590
1672
  ref: panelRef,
591
- role: "listbox",
592
- "aria-label": triggerLabel,
1673
+ id: menuId,
1674
+ onKeyDown: (event) => {
1675
+ if (event.target === searchRef.current && event.key !== "ArrowDown") return;
1676
+ if (!["ArrowDown", "ArrowUp", "Home", "End"].includes(event.key)) return;
1677
+ event.preventDefault();
1678
+ const options = Array.from(panelRef.current?.querySelectorAll('[role="option"]') ?? []);
1679
+ const current = options.indexOf(document.activeElement);
1680
+ const index = event.key === "Home" ? 0 : event.key === "End" ? options.length - 1 : (current + (event.key === "ArrowDown" ? 1 : -1) + options.length) % options.length;
1681
+ options[index]?.focus({ preventScroll: true });
1682
+ options[index]?.scrollIntoView({ block: "nearest" });
1683
+ },
1684
+ role: searchable ? "dialog" : void 0,
1685
+ "aria-label": accessibleLabel,
593
1686
  className: cn("fractal-dropdown-panel", `fractal-dropdown-panel--${size}`, panelClassName),
594
1687
  style: panelStyle,
595
1688
  children: [
596
- label != null && /* @__PURE__ */ jsx9("div", { className: "fractal-dropdown-label", children: label }),
597
- visibleItems.map((item, index) => {
598
- const topDivider = item.border === "top" ? /* @__PURE__ */ jsx9("div", { className: "fractal-dropdown-separator" }) : null;
599
- const bottomDivider = item.border === "bottom" ? /* @__PURE__ */ jsx9("div", { className: "fractal-dropdown-separator" }) : null;
600
- if (item.kind === "header") {
601
- return /* @__PURE__ */ jsxs8(React7.Fragment, { children: [
1689
+ searchable && /* @__PURE__ */ jsxs17("div", { className: "fractal-dropdown-search", children: [
1690
+ /* @__PURE__ */ jsx19(MagnifyingGlass2, { "aria-hidden": "true" }),
1691
+ /* @__PURE__ */ jsx19("input", { ref: searchRef, type: "search", "aria-label": searchPlaceholder, placeholder: searchPlaceholder, value: query, onChange: (event) => setQuery(event.target.value) })
1692
+ ] }),
1693
+ /* @__PURE__ */ jsxs17("div", { role: "listbox", id: `${menuId}-options`, "aria-label": accessibleLabel, "aria-multiselectable": multiple || void 0, children: [
1694
+ label != null && /* @__PURE__ */ jsx19("div", { className: "fractal-dropdown-label", children: label }),
1695
+ visibleItems.map((item, index) => {
1696
+ const topDivider = item.border === "top" ? /* @__PURE__ */ jsx19("div", { className: "fractal-dropdown-separator" }) : null;
1697
+ const bottomDivider = item.border === "bottom" ? /* @__PURE__ */ jsx19("div", { className: "fractal-dropdown-separator" }) : null;
1698
+ if (item.kind === "header") {
1699
+ return /* @__PURE__ */ jsxs17(React11.Fragment, { children: [
1700
+ topDivider,
1701
+ /* @__PURE__ */ jsx19("div", { role: "presentation", className: "fractal-dropdown-label", children: item.description }),
1702
+ bottomDivider
1703
+ ] }, `header-${index}`);
1704
+ }
1705
+ const isSelected = multiple ? selectedValues.includes(item.value) : item.value === selectedValue;
1706
+ return /* @__PURE__ */ jsxs17(React11.Fragment, { children: [
602
1707
  topDivider,
603
- /* @__PURE__ */ jsx9("div", { role: "presentation", className: "fractal-dropdown-label", children: item.description }),
1708
+ /* @__PURE__ */ jsxs17(
1709
+ "button",
1710
+ {
1711
+ type: "button",
1712
+ role: "option",
1713
+ "aria-selected": isSelected,
1714
+ onClick: () => {
1715
+ if (multiple) onSelectionChange?.(isSelected ? selectedValues.filter((value) => value !== item.value) : [...selectedValues, item.value]);
1716
+ else onSelect?.(item.value);
1717
+ if (!multiple && closeOnSelect) {
1718
+ setOpen(false);
1719
+ triggerRef.current?.focus();
1720
+ }
1721
+ },
1722
+ className: "fractal-dropdown-option",
1723
+ children: [
1724
+ multiple && /* @__PURE__ */ jsx19("span", { className: "fractal-dropdown-checkbox", "data-checked": isSelected, "aria-hidden": "true", children: isSelected && /* @__PURE__ */ jsx19(Check2, {}) }),
1725
+ item.icon != null && /* @__PURE__ */ jsx19("span", { className: "fractal-dropdown-icon", children: item.icon }),
1726
+ item.description != null && (typeof item.description !== "string" || item.description !== "") && /* @__PURE__ */ jsx19("span", { className: "fractal-dropdown-option-text", children: item.description }),
1727
+ item.trailing != null && /* @__PURE__ */ jsx19("span", { className: "fractal-dropdown-trailing", children: item.trailing })
1728
+ ]
1729
+ }
1730
+ ),
604
1731
  bottomDivider
605
- ] }, `header-${index}`);
606
- }
607
- const isSelected = item.value === selectedValue;
608
- return /* @__PURE__ */ jsxs8(React7.Fragment, { children: [
609
- topDivider,
610
- /* @__PURE__ */ jsxs8(
611
- "button",
612
- {
613
- type: "button",
614
- role: "option",
615
- "aria-selected": isSelected,
616
- onClick: () => {
617
- onSelect(item.value);
618
- if (closeOnSelect) setOpen(false);
619
- },
620
- className: "fractal-dropdown-option",
621
- children: [
622
- item.icon != null && /* @__PURE__ */ jsx9("span", { className: "fractal-dropdown-icon", children: item.icon }),
623
- item.description != null && (typeof item.description !== "string" || item.description !== "") && /* @__PURE__ */ jsx9("span", { className: "fractal-dropdown-option-text", children: item.description }),
624
- item.trailing != null && /* @__PURE__ */ jsx9("span", { className: "fractal-dropdown-trailing", children: item.trailing })
625
- ]
626
- }
627
- ),
628
- bottomDivider
629
- ] }, item.value);
630
- })
1732
+ ] }, item.value);
1733
+ })
1734
+ ] }),
1735
+ !selectableItems.length && /* @__PURE__ */ jsx19("p", { role: "status", className: "fractal-dropdown-label", children: emptyMessage })
631
1736
  ]
632
1737
  }
633
1738
  )
@@ -635,10 +1740,10 @@ function Dropdown({
635
1740
  }
636
1741
 
637
1742
  // src/components/Link/Link.tsx
638
- import * as React8 from "react";
639
- import { jsx as jsx10 } from "react/jsx-runtime";
640
- var Link = React8.forwardRef(
641
- ({ className, theme = "default", size = "md", ...props }, ref) => /* @__PURE__ */ jsx10(
1743
+ import * as React12 from "react";
1744
+ import { jsx as jsx20 } from "react/jsx-runtime";
1745
+ var Link = React12.forwardRef(
1746
+ ({ className, theme = "default", size = "md", ...props }, ref) => /* @__PURE__ */ jsx20(
642
1747
  "a",
643
1748
  {
644
1749
  ref,
@@ -650,50 +1755,142 @@ var Link = React8.forwardRef(
650
1755
  Link.displayName = "Link";
651
1756
 
652
1757
  // src/components/Chip/Chip.tsx
653
- import * as React9 from "react";
654
- import { jsx as jsx11 } from "react/jsx-runtime";
655
- var Chip = React9.forwardRef(
656
- ({ className, tone = "neutral", ...props }, ref) => /* @__PURE__ */ jsx11(
1758
+ import * as React13 from "react";
1759
+ import { jsx as jsx21 } from "react/jsx-runtime";
1760
+ var Chip = React13.forwardRef(
1761
+ ({ className, tone = "neutral", ...props }, ref) => /* @__PURE__ */ jsx21(
657
1762
  "span",
658
1763
  {
659
1764
  ref,
660
1765
  className: cn("fractal-chip", `fractal-chip--${tone}`, className),
661
1766
  ...props
662
1767
  }
663
- )
664
- );
665
- Chip.displayName = "Chip";
1768
+ )
1769
+ );
1770
+ Chip.displayName = "Chip";
1771
+
1772
+ // src/components/Checkbox/Checkbox.tsx
1773
+ import { Checkbox as CheckboxIcon, CheckboxChecked, Minus as Minus2 } from "@mirror-physics/fractal-icons";
1774
+ import { jsx as jsx22, jsxs as jsxs18 } from "react/jsx-runtime";
1775
+ function Checkbox({
1776
+ checked,
1777
+ indeterminate = false,
1778
+ onCheckedChange,
1779
+ className,
1780
+ disabled,
1781
+ onClick,
1782
+ onKeyDown,
1783
+ ...props
1784
+ }) {
1785
+ return /* @__PURE__ */ jsx22(
1786
+ "button",
1787
+ {
1788
+ ...props,
1789
+ className: cn(
1790
+ "fractal-checkbox",
1791
+ (checked || indeterminate) && "fractal-checkbox--checked",
1792
+ indeterminate && "fractal-checkbox--indeterminate",
1793
+ className
1794
+ ),
1795
+ type: "button",
1796
+ role: "checkbox",
1797
+ "aria-checked": indeterminate ? "mixed" : checked,
1798
+ disabled,
1799
+ onClick: (event) => {
1800
+ onClick?.(event);
1801
+ if (!event.defaultPrevented) onCheckedChange(indeterminate || !checked);
1802
+ },
1803
+ onKeyDown: (event) => {
1804
+ onKeyDown?.(event);
1805
+ if (event.key === " " || event.key === "Enter") event.stopPropagation();
1806
+ },
1807
+ children: indeterminate ? /* @__PURE__ */ jsxs18("span", { className: "fractal-checkbox__indeterminate-icon", "aria-hidden": "true", children: [
1808
+ /* @__PURE__ */ jsx22(CheckboxIcon, { size: 18 }),
1809
+ /* @__PURE__ */ jsx22(Minus2, { className: "fractal-checkbox__indeterminate-mark", size: 12 })
1810
+ ] }) : checked ? /* @__PURE__ */ jsx22(CheckboxChecked, { "aria-hidden": "true", size: 18 }) : /* @__PURE__ */ jsx22(CheckboxIcon, { "aria-hidden": "true", size: 18 })
1811
+ }
1812
+ );
1813
+ }
1814
+
1815
+ // src/components/Radio/Radio.tsx
1816
+ import { jsx as jsx23, jsxs as jsxs19 } from "react/jsx-runtime";
1817
+ function Radio({
1818
+ checked,
1819
+ className,
1820
+ description,
1821
+ disabled,
1822
+ label,
1823
+ onCheckedChange,
1824
+ ...props
1825
+ }) {
1826
+ return /* @__PURE__ */ jsxs19("label", { className: cn("fractal-radio", disabled && "fractal-radio--disabled", className), children: [
1827
+ /* @__PURE__ */ jsx23(
1828
+ "input",
1829
+ {
1830
+ ...props,
1831
+ checked,
1832
+ className: "fractal-radio__input",
1833
+ disabled,
1834
+ onChange: (event) => onCheckedChange?.(event.currentTarget.checked),
1835
+ type: "radio"
1836
+ }
1837
+ ),
1838
+ /* @__PURE__ */ jsx23("span", { "aria-hidden": "true", className: "fractal-radio__control", children: /* @__PURE__ */ jsx23("span", { className: "fractal-radio__dot" }) }),
1839
+ /* @__PURE__ */ jsxs19("span", { className: "fractal-radio__content", children: [
1840
+ /* @__PURE__ */ jsx23("span", { className: "fractal-radio__label", children: label }),
1841
+ description && /* @__PURE__ */ jsx23("span", { className: "fractal-radio__description", children: description })
1842
+ ] })
1843
+ ] });
1844
+ }
666
1845
 
667
1846
  // src/components/SortableTable/SortableTable.tsx
668
- import { useMemo, useState as useState3 } from "react";
1847
+ import { useMemo as useMemo2, useState as useState8 } from "react";
669
1848
  import { ArrowUp, ArrowDown, ArrowsUpDown } from "@mirror-physics/fractal-icons";
670
- import { jsx as jsx12, jsxs as jsxs9 } from "react/jsx-runtime";
1849
+ import { jsx as jsx24, jsxs as jsxs20 } from "react/jsx-runtime";
671
1850
  function SortableTable({
672
1851
  columns,
673
1852
  data,
1853
+ "aria-label": ariaLabel,
674
1854
  emptyMessage,
675
1855
  header,
676
1856
  size = "md",
1857
+ embedded = false,
1858
+ layout = "fill",
1859
+ stickyHeader = false,
1860
+ stickyFirstColumn = false,
1861
+ maxHeight,
1862
+ overflowMenu,
677
1863
  className,
1864
+ defaultSort,
678
1865
  defaultSortKey = void 0,
679
1866
  defaultSortDirection = null,
680
1867
  sortKey: controlledKey,
681
1868
  sortDirection: controlledDirection,
682
1869
  onSortChange,
1870
+ pagination,
683
1871
  highlightRow,
684
- onRowClick
1872
+ selection,
1873
+ isRowDisabled,
1874
+ isRowSelectionDisabled,
1875
+ onRowClick,
1876
+ onRowDoubleClick,
1877
+ rowAction
685
1878
  }) {
686
- const [internalKey, setInternalKey] = useState3(defaultSortKey ?? null);
687
- const [internalDirection, setInternalDirection] = useState3(defaultSortDirection);
1879
+ const [internalKey, setInternalKey] = useState8(defaultSortKey ?? null);
1880
+ const [internalDirection, setInternalDirection] = useState8(defaultSortDirection);
688
1881
  const isControlled = controlledKey !== void 0 && controlledDirection !== void 0;
689
1882
  const activeKey = isControlled ? controlledKey : internalKey;
690
1883
  const activeDirection = isControlled ? controlledDirection : internalDirection;
691
- const sorted = useMemo(() => {
692
- if (!activeKey || !activeDirection) return data;
693
- const col = columns.find((c) => c.key === activeKey);
1884
+ const baselineKey = defaultSort?.key ?? null;
1885
+ const baselineDirection = defaultSort?.direction ?? null;
1886
+ const effectiveKey = activeKey && activeDirection ? activeKey : baselineKey;
1887
+ const effectiveDirection = activeKey && activeDirection ? activeDirection : baselineDirection;
1888
+ const sorted = useMemo2(() => {
1889
+ if (!effectiveKey || !effectiveDirection) return data;
1890
+ const col = columns.find((c) => c.key === effectiveKey);
694
1891
  if (!col) return data;
695
1892
  const accessor = col.sortAccessor ?? ((row) => {
696
- const value = row[activeKey];
1893
+ const value = row[effectiveKey];
697
1894
  return typeof value === "string" || typeof value === "number" ? value : null;
698
1895
  });
699
1896
  const copy = [...data];
@@ -704,26 +1901,41 @@ function SortableTable({
704
1901
  if (av == null) return 1;
705
1902
  if (bv == null) return -1;
706
1903
  if (typeof av === "number" && typeof bv === "number") {
707
- return activeDirection === "asc" ? av - bv : bv - av;
1904
+ return effectiveDirection === "asc" ? av - bv : bv - av;
708
1905
  }
709
1906
  const as = String(av).toLowerCase();
710
1907
  const bs = String(bv).toLowerCase();
711
- if (as < bs) return activeDirection === "asc" ? -1 : 1;
712
- if (as > bs) return activeDirection === "asc" ? 1 : -1;
1908
+ if (as < bs) return effectiveDirection === "asc" ? -1 : 1;
1909
+ if (as > bs) return effectiveDirection === "asc" ? 1 : -1;
713
1910
  return 0;
714
1911
  });
715
1912
  return copy;
716
- }, [data, columns, activeKey, activeDirection]);
1913
+ }, [data, columns, effectiveKey, effectiveDirection]);
1914
+ const visibleRows = useMemo2(() => {
1915
+ if (!pagination) return sorted;
1916
+ const pageSize = Math.max(1, pagination.pageSize);
1917
+ const totalPages = Math.max(1, Math.ceil(sorted.length / pageSize));
1918
+ const page = Math.min(Math.max(1, pagination.page), totalPages);
1919
+ const startIndex = (page - 1) * pageSize;
1920
+ return sorted.slice(startIndex, startIndex + pageSize);
1921
+ }, [pagination, sorted]);
1922
+ const selectedRows = selection ? sorted.filter(selection.isSelected) : [];
1923
+ const selectableRows = selection ? sorted.filter((row) => !isRowDisabled?.(row) && !isRowSelectionDisabled?.(row)) : [];
1924
+ const selectedRowCount = selection ? selectableRows.filter(selection.isSelected).length : 0;
1925
+ const allRowsSelected = selectableRows.length > 0 && selectedRowCount === selectableRows.length;
1926
+ const someRowsSelected = selectedRows.length > 0 && !allRowsSelected;
717
1927
  const handleSortClick = (key) => {
718
1928
  let nextDirection;
719
- if (activeKey !== key) {
1929
+ if (!activeKey || !activeDirection) {
1930
+ nextDirection = baselineKey === key && baselineDirection ? baselineDirection : "asc";
1931
+ } else if (activeKey !== key) {
720
1932
  nextDirection = "asc";
1933
+ } else if (baselineKey === key && baselineDirection) {
1934
+ nextDirection = activeDirection === baselineDirection ? baselineDirection === "asc" ? "desc" : "asc" : null;
721
1935
  } else if (activeDirection === "asc") {
722
1936
  nextDirection = "desc";
723
- } else if (activeDirection === "desc") {
724
- nextDirection = null;
725
1937
  } else {
726
- nextDirection = "asc";
1938
+ nextDirection = null;
727
1939
  }
728
1940
  const nextKey = nextDirection === null ? null : key;
729
1941
  if (isControlled) {
@@ -734,101 +1946,200 @@ function SortableTable({
734
1946
  onSortChange?.(nextKey, nextDirection);
735
1947
  }
736
1948
  };
737
- return /* @__PURE__ */ jsx12("div", { className: cn("fractal-sortable-table", `fractal-sortable-table--${size}`, className), children: /* @__PURE__ */ jsxs9("div", { className: "fractal-sortable-table-surface", children: [
738
- header && /* @__PURE__ */ jsx12("div", { className: "fractal-sortable-table-header", children: header }),
739
- sorted.length === 0 ? /* @__PURE__ */ jsx12("div", { className: "fractal-sortable-table-empty", children: emptyMessage || "No data" }) : /* @__PURE__ */ jsxs9("table", { className: "fractal-sortable-table-table", children: [
740
- /* @__PURE__ */ jsx12("thead", { children: /* @__PURE__ */ jsx12("tr", { className: "fractal-sortable-table-thead-row", children: columns.map((col) => {
741
- const isActive = activeKey === col.key && activeDirection !== null;
742
- if (!col.sortable) {
743
- return /* @__PURE__ */ jsx12(
744
- "th",
745
- {
746
- className: "fractal-sortable-table-th",
747
- style: { width: col.width },
748
- children: col.header
749
- },
750
- col.key
751
- );
752
- }
753
- return /* @__PURE__ */ jsx12(
1949
+ return /* @__PURE__ */ jsx24("div", { className: cn("fractal-table-layout", "fractal-sortable-table", `fractal-sortable-table--${size}`, embedded && "fractal-sortable-table--embedded", className), "data-overflow-menu": Boolean(overflowMenu), "data-table-layout": layout, "data-sticky-first": stickyFirstColumn, "data-sticky-header": stickyHeader, "data-table-selection": Boolean(selection), children: /* @__PURE__ */ jsxs20("div", { className: "fractal-sortable-table-surface", children: [
1950
+ header && /* @__PURE__ */ jsx24("div", { className: "fractal-sortable-table-header", children: header }),
1951
+ visibleRows.length === 0 ? /* @__PURE__ */ jsx24("div", { className: "fractal-sortable-table-empty", children: emptyMessage || "No data" }) : /* @__PURE__ */ jsx24(TableViewport, { maxHeight, label: ariaLabel || "Data table", children: /* @__PURE__ */ jsxs20("table", { "aria-label": ariaLabel, className: "fractal-sortable-table-table", children: [
1952
+ /* @__PURE__ */ jsx24("thead", { children: /* @__PURE__ */ jsxs20("tr", { className: "fractal-sortable-table-thead-row", children: [
1953
+ selection && /* @__PURE__ */ jsx24(
754
1954
  "th",
755
1955
  {
756
- className: "fractal-sortable-table-th fractal-sortable-table-th--sortable",
757
- style: { width: col.width },
758
- children: /* @__PURE__ */ jsxs9(
759
- "button",
1956
+ className: "fractal-sortable-table-th fractal-sortable-table-selection-cell",
1957
+ "data-pinned-column": "selection",
1958
+ scope: "col",
1959
+ children: /* @__PURE__ */ jsx24(
1960
+ Checkbox,
760
1961
  {
761
- type: "button",
762
- className: cn(
763
- "fractal-sortable-table-sort-button",
764
- isActive && "fractal-sortable-table-sort-button--active"
765
- ),
766
- onClick: () => handleSortClick(col.key),
767
- "aria-sort": isActive ? activeDirection === "asc" ? "ascending" : "descending" : "none",
768
- children: [
769
- /* @__PURE__ */ jsx12("span", { children: col.header }),
770
- /* @__PURE__ */ jsx12("span", { className: "fractal-sortable-table-sort-icon", "aria-hidden": "true", children: isActive && activeDirection === "asc" ? /* @__PURE__ */ jsx12(ArrowUp, { size: 14 }) : isActive && activeDirection === "desc" ? /* @__PURE__ */ jsx12(ArrowDown, { size: 14 }) : /* @__PURE__ */ jsx12(ArrowsUpDown, { size: 14 }) })
771
- ]
1962
+ "aria-label": selection.selectAllLabel ?? "Select all rows",
1963
+ checked: allRowsSelected,
1964
+ disabled: selectableRows.length === 0 && selectedRows.length === 0,
1965
+ indeterminate: someRowsSelected,
1966
+ onCheckedChange: (checked) => {
1967
+ const rows = checked ? selectableRows : selectedRows;
1968
+ if (selection.onSelectAllChange) {
1969
+ selection.onSelectAllChange(rows, checked);
1970
+ return;
1971
+ }
1972
+ rows.forEach((row) => {
1973
+ if (selection.isSelected(row) !== checked) {
1974
+ selection.onSelectedChange(row, checked);
1975
+ }
1976
+ });
1977
+ }
772
1978
  }
773
1979
  )
774
- },
775
- col.key
776
- );
777
- }) }) }),
778
- /* @__PURE__ */ jsx12("tbody", { children: sorted.map((row, rowIndex) => {
1980
+ }
1981
+ ),
1982
+ columns.map((col, columnIndex) => {
1983
+ const isActive = activeKey === col.key && activeDirection !== null;
1984
+ if (!col.sortable) {
1985
+ return /* @__PURE__ */ jsx24(
1986
+ "th",
1987
+ {
1988
+ className: "fractal-sortable-table-th",
1989
+ scope: "col",
1990
+ style: columnStyle(col),
1991
+ "data-pinned-column": columnIndex === 0 ? "data" : void 0,
1992
+ children: /* @__PURE__ */ jsx24(TableCellContent, { label: `${col.header} column`, children: col.header })
1993
+ },
1994
+ col.key
1995
+ );
1996
+ }
1997
+ return /* @__PURE__ */ jsx24(
1998
+ "th",
1999
+ {
2000
+ className: "fractal-sortable-table-th fractal-sortable-table-th--sortable",
2001
+ scope: "col",
2002
+ style: columnStyle(col),
2003
+ "data-pinned-column": columnIndex === 0 ? "data" : void 0,
2004
+ children: /* @__PURE__ */ jsx24(TableCellContent, { label: `${col.header} column`, children: /* @__PURE__ */ jsxs20(
2005
+ "button",
2006
+ {
2007
+ type: "button",
2008
+ className: cn(
2009
+ "fractal-sortable-table-sort-button",
2010
+ isActive && "fractal-sortable-table-sort-button--active"
2011
+ ),
2012
+ onClick: () => handleSortClick(col.key),
2013
+ "aria-sort": isActive ? activeDirection === "asc" ? "ascending" : "descending" : "none",
2014
+ children: [
2015
+ /* @__PURE__ */ jsx24("span", { children: col.header }),
2016
+ /* @__PURE__ */ jsx24("span", { className: "fractal-sortable-table-sort-icon", "aria-hidden": "true", children: isActive && activeDirection === "asc" ? /* @__PURE__ */ jsx24(ArrowUp, { size: 14 }) : isActive && activeDirection === "desc" ? /* @__PURE__ */ jsx24(ArrowDown, { size: 14 }) : /* @__PURE__ */ jsx24(ArrowsUpDown, { size: 14 }) })
2017
+ ]
2018
+ }
2019
+ ) })
2020
+ },
2021
+ col.key
2022
+ );
2023
+ }),
2024
+ overflowMenu && /* @__PURE__ */ jsx24("th", { scope: "col", "aria-label": "Row actions", className: "fractal-sortable-table-th fractal-table-overflow-cell", "data-pinned-column": "overflow" })
2025
+ ] }) }),
2026
+ /* @__PURE__ */ jsx24("tbody", { children: visibleRows.map((row, rowIndex) => {
779
2027
  const highlighted = highlightRow?.(row, rowIndex) ?? false;
780
- return /* @__PURE__ */ jsx12(
2028
+ const disabled = isRowDisabled?.(row) ?? false;
2029
+ const selectionDisabled = disabled || (isRowSelectionDisabled?.(row) ?? false);
2030
+ const interactive = !disabled && Boolean(onRowClick || onRowDoubleClick);
2031
+ const action = !disabled && rowAction ? {
2032
+ label: rowAction.label(row, rowIndex),
2033
+ icon: rowAction.icon(row, rowIndex)
2034
+ } : null;
2035
+ return /* @__PURE__ */ jsxs20(
781
2036
  "tr",
782
2037
  {
783
2038
  className: cn(
784
2039
  "fractal-sortable-table-row",
785
2040
  highlighted && "fractal-sortable-table-row--highlighted",
786
- onRowClick && "fractal-sortable-table-row--interactive"
2041
+ interactive && "fractal-sortable-table-row--interactive",
2042
+ disabled && "fractal-sortable-table-row--disabled"
787
2043
  ),
788
- onClick: onRowClick ? () => onRowClick(row, rowIndex) : void 0,
789
- onKeyDown: onRowClick ? (event) => {
2044
+ "data-disabled": disabled || void 0,
2045
+ onClick: !disabled && onRowClick ? () => onRowClick(row, rowIndex) : void 0,
2046
+ onDoubleClick: !disabled && onRowDoubleClick ? () => onRowDoubleClick(row, rowIndex) : void 0,
2047
+ onKeyDown: interactive ? (event) => {
790
2048
  if (event.key === "Enter" || event.key === " ") {
791
2049
  event.preventDefault();
792
- onRowClick(row, rowIndex);
2050
+ const activateRow = onRowClick ?? onRowDoubleClick;
2051
+ activateRow?.(row, rowIndex);
793
2052
  }
794
2053
  } : void 0,
795
- role: onRowClick ? "button" : void 0,
796
- tabIndex: onRowClick ? 0 : void 0,
797
- children: columns.map((col) => /* @__PURE__ */ jsx12(
798
- "td",
799
- {
800
- className: "fractal-sortable-table-td",
801
- style: { width: col.width, textAlign: col.align ?? "left" },
802
- children: col.render(row, rowIndex)
803
- },
804
- col.key
805
- ))
2054
+ "aria-label": action?.label,
2055
+ "aria-disabled": disabled || void 0,
2056
+ role: interactive ? "button" : void 0,
2057
+ tabIndex: interactive ? 0 : void 0,
2058
+ children: [
2059
+ selection && /* @__PURE__ */ jsx24("td", { className: "fractal-sortable-table-td fractal-sortable-table-selection-cell", "data-pinned-column": "selection", children: /* @__PURE__ */ jsx24(
2060
+ Checkbox,
2061
+ {
2062
+ "aria-label": selection.getRowLabel(row),
2063
+ checked: !selectionDisabled && selection.isSelected(row),
2064
+ disabled: selectionDisabled,
2065
+ onClick: (event) => event.stopPropagation(),
2066
+ onCheckedChange: (checked) => {
2067
+ if (!selectionDisabled) selection.onSelectedChange(row, checked);
2068
+ }
2069
+ }
2070
+ ) }),
2071
+ columns.map((col, columnIndex) => /* @__PURE__ */ jsxs20(
2072
+ "td",
2073
+ {
2074
+ className: cn(
2075
+ "fractal-sortable-table-td",
2076
+ action && columnIndex === columns.length - 1 && "fractal-sortable-table-td--row-action"
2077
+ ),
2078
+ style: { ...columnStyle(col), textAlign: col.align ?? "left" },
2079
+ "data-column-align": col.align,
2080
+ "data-pinned-column": columnIndex === 0 ? "data" : void 0,
2081
+ children: [
2082
+ /* @__PURE__ */ jsx24(TableCellContent, { label: `${col.header || "Value"}, row ${rowIndex + 1}`, children: col.render(row, rowIndex) }),
2083
+ action && columnIndex === columns.length - 1 && /* @__PURE__ */ jsx24("span", { className: "fractal-sortable-table-row-action", "aria-hidden": "true", children: /* @__PURE__ */ jsx24("span", { className: "fractal-sortable-table-row-action__icon", children: action.icon }) })
2084
+ ]
2085
+ },
2086
+ col.key
2087
+ )),
2088
+ overflowMenu && /* @__PURE__ */ jsx24(
2089
+ "td",
2090
+ {
2091
+ className: "fractal-sortable-table-td fractal-table-overflow-cell",
2092
+ "data-pinned-column": "overflow",
2093
+ onClick: (event) => event.stopPropagation(),
2094
+ onDoubleClick: (event) => event.stopPropagation(),
2095
+ onKeyDown: (event) => event.stopPropagation(),
2096
+ children: /* @__PURE__ */ jsx24(TableOverflowMenu, { items: overflowMenu.items(row, rowIndex), label: overflowMenu.getLabel?.(row, rowIndex) ?? `Actions for row ${rowIndex + 1}`, disabled: disabled || overflowMenu.isDisabled?.(row, rowIndex), onSelect: (value) => overflowMenu.onSelect(value, row, rowIndex) })
2097
+ }
2098
+ )
2099
+ ]
806
2100
  },
807
2101
  rowIndex
808
2102
  );
809
2103
  }) })
810
- ] })
2104
+ ] }) })
811
2105
  ] }) });
812
2106
  }
813
2107
 
814
2108
  // src/components/Tabs/Tabs.tsx
815
- import { useId, useRef as useRef3, useState as useState4 } from "react";
816
- import { jsx as jsx13, jsxs as jsxs10 } from "react/jsx-runtime";
2109
+ import { useId as useId5, useLayoutEffect as useLayoutEffect4, useRef as useRef8, useState as useState9 } from "react";
2110
+ import { jsx as jsx25, jsxs as jsxs21 } from "react/jsx-runtime";
817
2111
  function Tabs({
818
2112
  items,
819
2113
  defaultValue,
820
2114
  value: controlledValue,
821
2115
  onValueChange,
822
2116
  className,
823
- ariaLabel
2117
+ ariaLabel,
2118
+ divider = false,
2119
+ variant = "line"
824
2120
  }) {
825
- const reactId = useId();
826
- const [internalValue, setInternalValue] = useState4(
2121
+ const reactId = useId5();
2122
+ const [internalValue, setInternalValue] = useState9(
827
2123
  defaultValue ?? items.find((i) => !i.disabled)?.id ?? items[0]?.id ?? ""
828
2124
  );
829
2125
  const isControlled = controlledValue !== void 0;
830
2126
  const activeId = isControlled ? controlledValue : internalValue;
831
- const tabRefs = useRef3({});
2127
+ const tabListRef = useRef8(null);
2128
+ const tabRefs = useRef8({});
2129
+ const [indicator, setIndicator] = useState9(null);
2130
+ useLayoutEffect4(() => {
2131
+ const updateIndicator = () => {
2132
+ const activeTab = tabRefs.current[activeId];
2133
+ if (!activeTab) return;
2134
+ setIndicator({ offset: activeTab.offsetLeft, width: activeTab.offsetWidth });
2135
+ };
2136
+ updateIndicator();
2137
+ const list = tabListRef.current;
2138
+ if (!list || typeof ResizeObserver === "undefined") return;
2139
+ const observer = new ResizeObserver(updateIndicator);
2140
+ observer.observe(list);
2141
+ return () => observer.disconnect();
2142
+ }, [activeId, items.length]);
832
2143
  const selectTab = (id) => {
833
2144
  if (isControlled) {
834
2145
  onValueChange?.(id);
@@ -838,6 +2149,10 @@ function Tabs({
838
2149
  }
839
2150
  };
840
2151
  const focusable = items.filter((i) => !i.disabled);
2152
+ const indicatorStyle = indicator ? {
2153
+ "--fractal-tabs-indicator-offset": `${indicator.offset}px`,
2154
+ "--fractal-tabs-indicator-width": `${indicator.width}px`
2155
+ } : void 0;
841
2156
  const onKeyDown = (event) => {
842
2157
  const currentIndex = focusable.findIndex((i) => i.id === activeId);
843
2158
  if (currentIndex === -1) return;
@@ -863,41 +2178,52 @@ function Tabs({
863
2178
  selectTab(nextId2);
864
2179
  requestAnimationFrame(() => tabRefs.current[nextId2]?.focus());
865
2180
  };
866
- return /* @__PURE__ */ jsxs10("div", { className: cn("fractal-tabs", className), children: [
867
- /* @__PURE__ */ jsx13("div", { role: "tablist", "aria-label": ariaLabel, className: "fractal-tabs-list", children: items.map((item) => {
868
- const isActive = item.id === activeId;
869
- const tabId = `${reactId}-tab-${item.id}`;
870
- const panelId = `${reactId}-panel-${item.id}`;
871
- return /* @__PURE__ */ jsx13(
872
- "button",
873
- {
874
- ref: (el) => {
875
- tabRefs.current[item.id] = el;
876
- },
877
- id: tabId,
878
- role: "tab",
879
- type: "button",
880
- "aria-selected": isActive,
881
- "aria-controls": panelId,
882
- tabIndex: isActive ? 0 : -1,
883
- disabled: item.disabled,
884
- className: cn(
885
- "fractal-tabs-trigger",
886
- isActive && "fractal-tabs-trigger--active",
887
- item.disabled && "fractal-tabs-trigger--disabled"
888
- ),
889
- onClick: () => !item.disabled && selectTab(item.id),
890
- onKeyDown,
891
- children: item.label
892
- },
893
- item.id
894
- );
895
- }) }),
2181
+ return /* @__PURE__ */ jsxs21("div", { className: cn("fractal-tabs", `fractal-tabs--${variant}`, divider && "fractal-tabs--divider", className), children: [
2182
+ /* @__PURE__ */ jsx25(
2183
+ "div",
2184
+ {
2185
+ ref: tabListRef,
2186
+ role: "tablist",
2187
+ "aria-label": ariaLabel,
2188
+ className: "fractal-tabs-list",
2189
+ "data-indicator-ready": indicator ? "true" : void 0,
2190
+ style: indicatorStyle,
2191
+ children: items.map((item) => {
2192
+ const isActive = item.id === activeId;
2193
+ const tabId = `${reactId}-tab-${item.id}`;
2194
+ const panelId = `${reactId}-panel-${item.id}`;
2195
+ return /* @__PURE__ */ jsx25(
2196
+ "button",
2197
+ {
2198
+ ref: (el) => {
2199
+ tabRefs.current[item.id] = el;
2200
+ },
2201
+ id: tabId,
2202
+ role: "tab",
2203
+ type: "button",
2204
+ "aria-selected": isActive,
2205
+ "aria-controls": panelId,
2206
+ tabIndex: isActive ? 0 : -1,
2207
+ disabled: item.disabled,
2208
+ className: cn(
2209
+ "fractal-tabs-trigger",
2210
+ isActive && "fractal-tabs-trigger--active",
2211
+ item.disabled && "fractal-tabs-trigger--disabled"
2212
+ ),
2213
+ onClick: () => !item.disabled && selectTab(item.id),
2214
+ onKeyDown,
2215
+ children: item.label
2216
+ },
2217
+ item.id
2218
+ );
2219
+ })
2220
+ }
2221
+ ),
896
2222
  items.map((item) => {
897
2223
  const tabId = `${reactId}-tab-${item.id}`;
898
2224
  const panelId = `${reactId}-panel-${item.id}`;
899
2225
  const isActive = item.id === activeId;
900
- return /* @__PURE__ */ jsx13(
2226
+ return /* @__PURE__ */ jsx25(
901
2227
  "div",
902
2228
  {
903
2229
  id: panelId,
@@ -913,10 +2239,112 @@ function Tabs({
913
2239
  ] });
914
2240
  }
915
2241
 
2242
+ // src/components/ContentSwitcher/ContentSwitcher.tsx
2243
+ import * as React14 from "react";
2244
+ import { jsx as jsx26, jsxs as jsxs22 } from "react/jsx-runtime";
2245
+ function ContentSwitcher({ items, value, onValueChange, ariaLabel, fullWidth = false, className }) {
2246
+ const switcherRef = React14.useRef(null);
2247
+ const buttonRefs = React14.useRef([]);
2248
+ const indicatorMotionReadyRef = React14.useRef(false);
2249
+ const activeIndex = items.findIndex((item) => item.value === value);
2250
+ React14.useLayoutEffect(() => {
2251
+ const switcher = switcherRef.current;
2252
+ const activeButton = buttonRefs.current[activeIndex];
2253
+ let motionFrame = 0;
2254
+ if (!switcher || !activeButton) {
2255
+ switcher?.removeAttribute("data-indicator-ready");
2256
+ switcher?.removeAttribute("data-indicator-animate");
2257
+ indicatorMotionReadyRef.current = false;
2258
+ return;
2259
+ }
2260
+ const updateIndicator = () => {
2261
+ const switcherRect = switcher.getBoundingClientRect();
2262
+ const activeButtonRect = activeButton.getBoundingClientRect();
2263
+ switcher.style.setProperty("--fractal-content-switcher-indicator-x", `${activeButtonRect.left - switcherRect.left - switcher.clientLeft}px`);
2264
+ switcher.style.setProperty("--fractal-content-switcher-indicator-y", `${activeButtonRect.top - switcherRect.top - switcher.clientTop}px`);
2265
+ switcher.style.setProperty("--fractal-content-switcher-indicator-width", `${activeButtonRect.width}px`);
2266
+ switcher.style.setProperty("--fractal-content-switcher-indicator-height", `${activeButtonRect.height}px`);
2267
+ switcher.setAttribute("data-indicator-ready", "true");
2268
+ if (!indicatorMotionReadyRef.current && motionFrame === 0) {
2269
+ motionFrame = window.requestAnimationFrame(() => {
2270
+ indicatorMotionReadyRef.current = true;
2271
+ switcher.setAttribute("data-indicator-animate", "true");
2272
+ motionFrame = 0;
2273
+ });
2274
+ }
2275
+ };
2276
+ updateIndicator();
2277
+ if (typeof ResizeObserver === "undefined") {
2278
+ return () => {
2279
+ if (motionFrame !== 0) window.cancelAnimationFrame(motionFrame);
2280
+ };
2281
+ }
2282
+ const resizeObserver = new ResizeObserver(updateIndicator);
2283
+ resizeObserver.observe(switcher);
2284
+ buttonRefs.current.forEach((button) => {
2285
+ if (button) resizeObserver.observe(button);
2286
+ });
2287
+ return () => {
2288
+ resizeObserver.disconnect();
2289
+ if (motionFrame !== 0) window.cancelAnimationFrame(motionFrame);
2290
+ };
2291
+ }, [activeIndex, items.length]);
2292
+ const moveFocus = (index, direction) => {
2293
+ let nextIndex = index;
2294
+ do {
2295
+ nextIndex = (nextIndex + direction + items.length) % items.length;
2296
+ } while (items[nextIndex]?.disabled && nextIndex !== index);
2297
+ if (!items[nextIndex]?.disabled) {
2298
+ buttonRefs.current[nextIndex]?.focus();
2299
+ onValueChange(items[nextIndex].value);
2300
+ }
2301
+ };
2302
+ return /* @__PURE__ */ jsxs22(
2303
+ "div",
2304
+ {
2305
+ className: cn("fractal-content-switcher", fullWidth && "fractal-content-switcher--full-width", className),
2306
+ role: "group",
2307
+ "aria-label": ariaLabel,
2308
+ ref: switcherRef,
2309
+ children: [
2310
+ /* @__PURE__ */ jsx26("span", { className: "fractal-content-switcher__indicator", "aria-hidden": true }),
2311
+ items.map((item, index) => {
2312
+ const active = item.value === value;
2313
+ return /* @__PURE__ */ jsx26(
2314
+ "button",
2315
+ {
2316
+ className: cn("fractal-content-switcher__item", active && "fractal-content-switcher__item--active"),
2317
+ type: "button",
2318
+ "aria-pressed": active,
2319
+ disabled: item.disabled,
2320
+ onClick: () => onValueChange(item.value),
2321
+ onKeyDown: (event) => {
2322
+ if (event.key === "ArrowRight") {
2323
+ event.preventDefault();
2324
+ moveFocus(index, 1);
2325
+ }
2326
+ if (event.key === "ArrowLeft") {
2327
+ event.preventDefault();
2328
+ moveFocus(index, -1);
2329
+ }
2330
+ },
2331
+ ref: (element) => {
2332
+ buttonRefs.current[index] = element;
2333
+ },
2334
+ children: item.label
2335
+ },
2336
+ item.value
2337
+ );
2338
+ })
2339
+ ]
2340
+ }
2341
+ );
2342
+ }
2343
+
916
2344
  // src/components/Disclosure/Disclosure.tsx
917
- import { useId as useId2, useState as useState5 } from "react";
918
- import { ChevronRight, ChevronDown as ChevronDown2 } from "@mirror-physics/fractal-icons";
919
- import { jsx as jsx14, jsxs as jsxs11 } from "react/jsx-runtime";
2345
+ import { useId as useId6, useState as useState10 } from "react";
2346
+ import { ChevronRight as ChevronRight3, ChevronDown as ChevronDown2 } from "@mirror-physics/fractal-icons";
2347
+ import { jsx as jsx27, jsxs as jsxs23 } from "react/jsx-runtime";
920
2348
  function Disclosure({
921
2349
  title,
922
2350
  meta,
@@ -924,16 +2352,18 @@ function Disclosure({
924
2352
  defaultOpen = false,
925
2353
  open: controlledOpen,
926
2354
  onOpenChange,
2355
+ disabled = false,
927
2356
  className,
928
2357
  variant = "card"
929
2358
  }) {
930
- const reactId = useId2();
2359
+ const reactId = useId6();
931
2360
  const headerId = `${reactId}-header`;
932
2361
  const panelId = `${reactId}-panel`;
933
- const [internalOpen, setInternalOpen] = useState5(defaultOpen);
2362
+ const [internalOpen, setInternalOpen] = useState10(defaultOpen);
934
2363
  const isControlled = controlledOpen !== void 0;
935
2364
  const isOpen = isControlled ? controlledOpen : internalOpen;
936
2365
  const toggle = () => {
2366
+ if (disabled) return;
937
2367
  const next = !isOpen;
938
2368
  if (isControlled) {
939
2369
  onOpenChange?.(next);
@@ -942,17 +2372,18 @@ function Disclosure({
942
2372
  onOpenChange?.(next);
943
2373
  }
944
2374
  };
945
- return /* @__PURE__ */ jsxs11(
2375
+ return /* @__PURE__ */ jsxs23(
946
2376
  "div",
947
2377
  {
948
2378
  className: cn(
949
2379
  "fractal-disclosure",
950
2380
  variant === "card" && "fractal-disclosure--card",
951
2381
  isOpen && "fractal-disclosure--open",
2382
+ disabled && "fractal-disclosure--disabled",
952
2383
  className
953
2384
  ),
954
2385
  children: [
955
- /* @__PURE__ */ jsxs11(
2386
+ /* @__PURE__ */ jsxs23(
956
2387
  "button",
957
2388
  {
958
2389
  type: "button",
@@ -960,15 +2391,16 @@ function Disclosure({
960
2391
  className: "fractal-disclosure-trigger",
961
2392
  "aria-expanded": isOpen,
962
2393
  "aria-controls": panelId,
2394
+ disabled,
963
2395
  onClick: toggle,
964
2396
  children: [
965
- /* @__PURE__ */ jsx14("span", { className: "fractal-disclosure-icon", "aria-hidden": "true", children: isOpen ? /* @__PURE__ */ jsx14(ChevronDown2, { size: 16 }) : /* @__PURE__ */ jsx14(ChevronRight, { size: 16 }) }),
966
- /* @__PURE__ */ jsx14("span", { className: "fractal-disclosure-title", children: title }),
967
- meta && /* @__PURE__ */ jsx14("span", { className: "fractal-disclosure-meta", children: meta })
2397
+ /* @__PURE__ */ jsx27("span", { className: "fractal-disclosure-icon", "aria-hidden": "true", children: isOpen ? /* @__PURE__ */ jsx27(ChevronDown2, { size: 16 }) : /* @__PURE__ */ jsx27(ChevronRight3, { size: 16 }) }),
2398
+ /* @__PURE__ */ jsx27("span", { className: "fractal-disclosure-title", children: title }),
2399
+ meta && /* @__PURE__ */ jsx27("span", { className: "fractal-disclosure-meta", children: meta })
968
2400
  ]
969
2401
  }
970
2402
  ),
971
- /* @__PURE__ */ jsx14(
2403
+ /* @__PURE__ */ jsx27(
972
2404
  "div",
973
2405
  {
974
2406
  id: panelId,
@@ -976,7 +2408,7 @@ function Disclosure({
976
2408
  "aria-labelledby": headerId,
977
2409
  className: "fractal-disclosure-panel",
978
2410
  hidden: !isOpen,
979
- children: /* @__PURE__ */ jsx14("div", { className: "fractal-disclosure-panel-inner", children })
2411
+ children: /* @__PURE__ */ jsx27("div", { className: "fractal-disclosure-panel-inner", children })
980
2412
  }
981
2413
  )
982
2414
  ]
@@ -984,39 +2416,260 @@ function Disclosure({
984
2416
  );
985
2417
  }
986
2418
 
2419
+ // src/components/LatticeLoader/LatticeLoader.tsx
2420
+ import { jsx as jsx28 } from "react/jsx-runtime";
2421
+ var LATTICE_COUNT = 160;
2422
+ var WAVE_PERIOD = 80;
2423
+ function LatticeLoader({ className, label = "Loading", ...props }) {
2424
+ return /* @__PURE__ */ jsx28(
2425
+ "span",
2426
+ {
2427
+ "aria-label": label,
2428
+ className: cn("fractal-lattice-loader", className),
2429
+ role: "progressbar",
2430
+ ...props,
2431
+ children: Array.from({ length: LATTICE_COUNT }, (_, index) => /* @__PURE__ */ jsx28(
2432
+ "span",
2433
+ {
2434
+ "aria-hidden": "true",
2435
+ className: "fractal-lattice-loader__cell",
2436
+ style: { "--fractal-lattice-index": index % WAVE_PERIOD }
2437
+ },
2438
+ index
2439
+ ))
2440
+ }
2441
+ );
2442
+ }
2443
+
2444
+ // src/components/Ghost/Ghost.tsx
2445
+ import { jsx as jsx29, jsxs as jsxs24 } from "react/jsx-runtime";
2446
+ var toCssLength = (value) => typeof value === "number" ? `${value}px` : value;
2447
+ function Ghost({ className, width, height, radius, style, ...props }) {
2448
+ return /* @__PURE__ */ jsx29(
2449
+ "div",
2450
+ {
2451
+ "aria-hidden": "true",
2452
+ className: cn("fractal-ghost", className),
2453
+ style: {
2454
+ width: toCssLength(width),
2455
+ height: toCssLength(height),
2456
+ borderRadius: toCssLength(radius),
2457
+ ...style
2458
+ },
2459
+ ...props
2460
+ }
2461
+ );
2462
+ }
2463
+ function GhostLine({ className, size = "md", width = "100%", ...props }) {
2464
+ return /* @__PURE__ */ jsx29(Ghost, { className: cn("fractal-ghost-line", `fractal-ghost-line--${size}`, className), width, ...props });
2465
+ }
2466
+ function ButtonGhost({ className, size = "lg", controlSize, iconOnly = false, width, ...props }) {
2467
+ return /* @__PURE__ */ jsx29(Ghost, { "data-control-size": controlSize ?? size, className: cn("fractal-ghost-button", `fractal-ghost-button--${size}`, iconOnly && "fractal-ghost-button--icon-only", className), width, ...props });
2468
+ }
2469
+ function CardGhost({ className, lines = 3, showHeader = true, ...props }) {
2470
+ return /* @__PURE__ */ jsxs24("div", { "aria-hidden": "true", className: cn("fractal-ghost-card", className), ...props, children: [
2471
+ showHeader && /* @__PURE__ */ jsx29(GhostLine, { width: "38%" }),
2472
+ /* @__PURE__ */ jsx29("div", { className: "fractal-ghost-stack", children: Array.from({ length: lines }, (_, index) => /* @__PURE__ */ jsx29(GhostLine, { width: index === lines - 1 ? "62%" : "100%" }, index)) })
2473
+ ] });
2474
+ }
2475
+ function DataTableGhost({
2476
+ className,
2477
+ showHeader = true,
2478
+ headers,
2479
+ columns = 4,
2480
+ rows = 5,
2481
+ columnWidths,
2482
+ size = "md",
2483
+ ...props
2484
+ }) {
2485
+ const cells = Array.from({ length: columns }, (_, index) => index);
2486
+ return /* @__PURE__ */ jsx29("div", { "aria-busy": "true", "aria-label": "Loading table", className: cn("fractal-ghost-table", `fractal-ghost-table--${size}`, className), role: "status", ...props, children: /* @__PURE__ */ jsxs24("table", { className: "fractal-ghost-table__table", children: [
2487
+ (showHeader || headers) && /* @__PURE__ */ jsx29("thead", { children: /* @__PURE__ */ jsx29("tr", { className: "fractal-ghost-table__head-row", children: cells.map((index) => /* @__PURE__ */ jsx29("th", { style: { width: columnWidths?.[index] }, children: headers?.[index] ?? /* @__PURE__ */ jsx29(GhostLine, { size: "sm", width: index === columns - 1 ? "62%" : "76%" }) }, index)) }) }),
2488
+ /* @__PURE__ */ jsx29("tbody", { children: Array.from({ length: rows }, (_, rowIndex) => /* @__PURE__ */ jsx29("tr", { className: "fractal-ghost-table__row", children: cells.map((columnIndex) => /* @__PURE__ */ jsx29("td", { style: { width: columnWidths?.[columnIndex] }, children: /* @__PURE__ */ jsx29(GhostLine, { width: `${[72, 54, 82, 63, 46][(rowIndex + columnIndex) % 5]}%` }) }, columnIndex)) }, rowIndex)) })
2489
+ ] }) });
2490
+ }
2491
+ function InputGhost({ className, labelWidth = "26%", ...props }) {
2492
+ return /* @__PURE__ */ jsx29(FormFieldGhost, { className, labelWidth, ...props });
2493
+ }
2494
+ function TextareaGhost({ className, labelWidth = "26%", ...props }) {
2495
+ return /* @__PURE__ */ jsx29(FormFieldGhost, { className, labelWidth, multiline: true, ...props });
2496
+ }
2497
+ function LabelGhost({ className, width = "26%", ...props }) {
2498
+ return /* @__PURE__ */ jsx29(GhostLine, { className: cn("fractal-ghost-label", className), size: "sm", width, ...props });
2499
+ }
2500
+ function FormFieldGhost({ className, labelWidth = "26%", multiline = false, controlSize = "lg", ...props }) {
2501
+ return /* @__PURE__ */ jsxs24("div", { "aria-hidden": "true", "data-control-size": controlSize, className: cn("fractal-ghost-field", className), ...props, children: [
2502
+ /* @__PURE__ */ jsx29(LabelGhost, { width: labelWidth }),
2503
+ /* @__PURE__ */ jsx29(Ghost, { className: cn("fractal-ghost-field__control", multiline && "fractal-ghost-field__control--multiline") })
2504
+ ] });
2505
+ }
2506
+ function AlertGhost({ className, ...props }) {
2507
+ return /* @__PURE__ */ jsxs24("div", { "aria-hidden": "true", className: cn("fractal-ghost-alert", className), ...props, children: [
2508
+ /* @__PURE__ */ jsx29(GhostLine, { width: "30%" }),
2509
+ /* @__PURE__ */ jsx29(GhostLine, { size: "sm", width: "78%" })
2510
+ ] });
2511
+ }
2512
+ function CheckboxGhost({ className, ...props }) {
2513
+ return /* @__PURE__ */ jsxs24("div", { "aria-hidden": "true", className: cn("fractal-ghost-choice", className), ...props, children: [
2514
+ /* @__PURE__ */ jsx29(Ghost, { className: "fractal-ghost-choice__control" }),
2515
+ /* @__PURE__ */ jsx29(GhostLine, { width: "42%" })
2516
+ ] });
2517
+ }
2518
+ var ToggleGhost = CheckboxGhost;
2519
+ function ChipGhost({ className, width = 74, ...props }) {
2520
+ return /* @__PURE__ */ jsx29(Ghost, { className: cn("fractal-ghost-chip", className), height: 24, width, ...props });
2521
+ }
2522
+ function LinkGhost({ className, width = 96, ...props }) {
2523
+ return /* @__PURE__ */ jsx29(GhostLine, { className: cn("fractal-ghost-link", className), size: "sm", width, ...props });
2524
+ }
2525
+ var TextButtonGhost = LinkGhost;
2526
+ function DropdownGhost({ className, controlSize = "lg", ...props }) {
2527
+ return /* @__PURE__ */ jsxs24("div", { "aria-hidden": "true", "data-control-size": controlSize, className: cn("fractal-ghost-dropdown", className), ...props, children: [
2528
+ /* @__PURE__ */ jsx29(GhostLine, { width: "38%" }),
2529
+ /* @__PURE__ */ jsx29(Ghost, { className: "fractal-ghost-dropdown__chevron" })
2530
+ ] });
2531
+ }
2532
+ function ContentSwitcherGhost({ className, options = 3, ...props }) {
2533
+ return /* @__PURE__ */ jsx29("div", { "aria-hidden": "true", className: cn("fractal-ghost-switcher", className), ...props, children: Array.from({ length: options }, (_, index) => /* @__PURE__ */ jsx29(Ghost, {}, index)) });
2534
+ }
2535
+ var TabsGhost = ContentSwitcherGhost;
2536
+ function DisclosureGhost({ className, ...props }) {
2537
+ return /* @__PURE__ */ jsxs24("div", { "aria-hidden": "true", className: cn("fractal-ghost-disclosure", className), ...props, children: [
2538
+ /* @__PURE__ */ jsx29(Ghost, { className: "fractal-ghost-disclosure__icon" }),
2539
+ /* @__PURE__ */ jsx29(GhostLine, { width: "42%" })
2540
+ ] });
2541
+ }
2542
+ function PaginationGhost({ className, pages = 5, ...props }) {
2543
+ return /* @__PURE__ */ jsx29("div", { "aria-hidden": "true", className: cn("fractal-ghost-pagination", className), ...props, children: Array.from({ length: pages }, (_, index) => /* @__PURE__ */ jsx29(Ghost, {}, index)) });
2544
+ }
2545
+ function FileDropzoneGhost({ className, ...props }) {
2546
+ return /* @__PURE__ */ jsxs24("div", { "aria-hidden": "true", className: cn("fractal-ghost-dropzone", className), ...props, children: [
2547
+ /* @__PURE__ */ jsx29(Ghost, { className: "fractal-ghost-dropzone__icon" }),
2548
+ /* @__PURE__ */ jsx29(GhostLine, { width: "26%" }),
2549
+ /* @__PURE__ */ jsx29(GhostLine, { size: "sm", width: "46%" })
2550
+ ] });
2551
+ }
2552
+ function PromptCardGhost({ className, ...props }) {
2553
+ return /* @__PURE__ */ jsx29(CardGhost, { className: cn("fractal-ghost-prompt-card", className), lines: 4, ...props });
2554
+ }
2555
+ function ConversationGhost({
2556
+ className,
2557
+ "aria-label": ariaLabel = "Loading conversation",
2558
+ ...props
2559
+ }) {
2560
+ return /* @__PURE__ */ jsxs24(
2561
+ "div",
2562
+ {
2563
+ "aria-busy": "true",
2564
+ "aria-label": ariaLabel,
2565
+ className: cn("fractal-ghost-conversation", className),
2566
+ role: "status",
2567
+ ...props,
2568
+ children: [
2569
+ /* @__PURE__ */ jsx29("div", { "aria-hidden": "true", className: "fractal-ghost-conversation__message fractal-ghost-conversation__message--user", children: /* @__PURE__ */ jsxs24("div", { className: "fractal-ghost-conversation__bubble", children: [
2570
+ /* @__PURE__ */ jsx29(GhostLine, { width: "100%" }),
2571
+ /* @__PURE__ */ jsx29(GhostLine, { width: "86%" }),
2572
+ /* @__PURE__ */ jsx29(GhostLine, { width: "64%" })
2573
+ ] }) }),
2574
+ /* @__PURE__ */ jsxs24("div", { "aria-hidden": "true", className: "fractal-ghost-conversation__message fractal-ghost-conversation__message--assistant", children: [
2575
+ /* @__PURE__ */ jsx29(GhostLine, { width: "92%" }),
2576
+ /* @__PURE__ */ jsx29(GhostLine, { width: "78%" }),
2577
+ /* @__PURE__ */ jsx29(GhostLine, { width: "58%" })
2578
+ ] })
2579
+ ]
2580
+ }
2581
+ );
2582
+ }
2583
+ function SortableTableGhost(props) {
2584
+ return /* @__PURE__ */ jsx29(DataTableGhost, { ...props });
2585
+ }
2586
+ function SidebarGhost({ className, items = 6, ...props }) {
2587
+ return /* @__PURE__ */ jsxs24("aside", { "aria-hidden": "true", className: cn("fractal-ghost-sidebar", className), ...props, children: [
2588
+ /* @__PURE__ */ jsx29(Ghost, { className: "fractal-ghost-sidebar__brand" }),
2589
+ /* @__PURE__ */ jsx29("div", { className: "fractal-ghost-sidebar__items", children: Array.from({ length: items }, (_, index) => /* @__PURE__ */ jsxs24("div", { className: "fractal-ghost-sidebar__item", children: [
2590
+ /* @__PURE__ */ jsx29(Ghost, {}),
2591
+ /* @__PURE__ */ jsx29(GhostLine, { width: `${[54, 68, 44, 61][index % 4]}%` })
2592
+ ] }, index)) })
2593
+ ] });
2594
+ }
2595
+ function ModalGhost({ className, title, lines = 3, ...props }) {
2596
+ return /* @__PURE__ */ jsx29(SurfaceGhost, { className: cn("fractal-ghost-modal", className), title, lines, ...props });
2597
+ }
2598
+ function SidePanelGhost({ className, title, lines = 5, ...props }) {
2599
+ return /* @__PURE__ */ jsx29(SurfaceGhost, { className: cn("fractal-ghost-side-panel", className), title, lines, ...props });
2600
+ }
2601
+ function SurfaceGhost({ className, title, lines = 3, ...props }) {
2602
+ return /* @__PURE__ */ jsxs24("div", { "aria-hidden": "true", className: cn("fractal-ghost-surface", className), ...props, children: [
2603
+ /* @__PURE__ */ jsxs24("div", { className: "fractal-ghost-surface__header", children: [
2604
+ /* @__PURE__ */ jsx29(GhostLine, { width: "42%" }),
2605
+ /* @__PURE__ */ jsx29(Ghost, { className: "fractal-ghost-surface__close" })
2606
+ ] }),
2607
+ title && /* @__PURE__ */ jsx29("div", { className: "fractal-ghost-surface__title", children: title }),
2608
+ /* @__PURE__ */ jsx29("div", { className: "fractal-ghost-stack", children: Array.from({ length: lines }, (_, index) => /* @__PURE__ */ jsx29(GhostLine, { width: index === lines - 1 ? "64%" : "100%" }, index)) })
2609
+ ] });
2610
+ }
2611
+
987
2612
  // src/components/Sidebar/Sidebar.tsx
988
- import * as React10 from "react";
989
- import { jsx as jsx15, jsxs as jsxs12 } from "react/jsx-runtime";
990
- var Sidebar = React10.forwardRef(
991
- ({ className, width, style, ...props }, ref) => /* @__PURE__ */ jsx15(
2613
+ import * as React15 from "react";
2614
+ import { Fragment as Fragment3, jsx as jsx30, jsxs as jsxs25 } from "react/jsx-runtime";
2615
+ var Sidebar = React15.forwardRef(
2616
+ ({ className, width, compact = false, style, ...props }, ref) => /* @__PURE__ */ jsx30(
992
2617
  "aside",
993
2618
  {
994
2619
  ref,
995
2620
  className: cn("fractal-sidebar", className),
2621
+ "data-compact": compact || void 0,
996
2622
  style: { ...style, ...width ? { "--fractal-sidebar-width": width } : {} },
997
2623
  ...props
998
2624
  }
999
2625
  )
1000
2626
  );
1001
2627
  Sidebar.displayName = "Sidebar";
1002
- var SidebarHeader = React10.forwardRef(
1003
- ({ className, ...props }, ref) => /* @__PURE__ */ jsx15("div", { ref, className: cn("fractal-sidebar__header", className), ...props })
2628
+ var SidebarHeader = React15.forwardRef(
2629
+ ({ className, ...props }, ref) => /* @__PURE__ */ jsx30("div", { ref, className: cn("fractal-sidebar__header", className), ...props })
1004
2630
  );
1005
2631
  SidebarHeader.displayName = "SidebarHeader";
1006
- var SidebarBrand = React10.forwardRef(
1007
- ({ className, type = "button", ...props }, ref) => /* @__PURE__ */ jsx15("button", { ref, type, className: cn("fractal-sidebar__brand", className), ...props })
2632
+ var SidebarBrand = React15.forwardRef(
2633
+ ({ className, type = "button", ...props }, ref) => /* @__PURE__ */ jsx30("button", { ref, type, className: cn("fractal-sidebar__brand", className), ...props })
1008
2634
  );
1009
2635
  SidebarBrand.displayName = "SidebarBrand";
1010
- var SidebarNav = React10.forwardRef(
1011
- ({ className, ...props }, ref) => /* @__PURE__ */ jsx15("nav", { ref, className: cn("fractal-sidebar__nav", className), ...props })
2636
+ function SidebarAccountMenu({
2637
+ name,
2638
+ items,
2639
+ onSelect,
2640
+ triggerLabel = "Account menu",
2641
+ icon,
2642
+ className
2643
+ }) {
2644
+ return /* @__PURE__ */ jsx30(
2645
+ Dropdown,
2646
+ {
2647
+ align: "left",
2648
+ className: cn("fractal-sidebar__account", className),
2649
+ items,
2650
+ onSelect,
2651
+ panelClassName: "fractal-sidebar__account-panel",
2652
+ selectedValue: "",
2653
+ triggerClassName: "fractal-sidebar__account-trigger",
2654
+ triggerContent: /* @__PURE__ */ jsxs25(Fragment3, { children: [
2655
+ icon && /* @__PURE__ */ jsx30("span", { className: "fractal-sidebar__account-icon", "aria-hidden": "true", children: icon }),
2656
+ /* @__PURE__ */ jsx30("span", { className: "fractal-sidebar__account-name", children: name })
2657
+ ] }),
2658
+ triggerLabel,
2659
+ triggerVariant: "tertiary"
2660
+ }
2661
+ );
2662
+ }
2663
+ var SidebarNav = React15.forwardRef(
2664
+ ({ className, ...props }, ref) => /* @__PURE__ */ jsx30("nav", { ref, className: cn("fractal-sidebar__nav", className), ...props })
1012
2665
  );
1013
2666
  SidebarNav.displayName = "SidebarNav";
1014
- var SidebarSection = React10.forwardRef(
1015
- ({ className, ...props }, ref) => /* @__PURE__ */ jsx15("div", { ref, className: cn("fractal-sidebar__section", className), ...props })
2667
+ var SidebarSection = React15.forwardRef(
2668
+ ({ className, ...props }, ref) => /* @__PURE__ */ jsx30("div", { ref, className: cn("fractal-sidebar__section", className), ...props })
1016
2669
  );
1017
2670
  SidebarSection.displayName = "SidebarSection";
1018
- var SidebarNavItem = React10.forwardRef(
1019
- ({ active = false, icon, endAdornment, className, type = "button", children, ...props }, ref) => /* @__PURE__ */ jsxs12(
2671
+ var SidebarNavItem = React15.forwardRef(
2672
+ ({ active = false, icon, endAdornment, className, type = "button", children, ...props }, ref) => /* @__PURE__ */ jsxs25(
1020
2673
  "button",
1021
2674
  {
1022
2675
  ref,
@@ -1026,28 +2679,60 @@ var SidebarNavItem = React10.forwardRef(
1026
2679
  "aria-current": active ? "page" : void 0,
1027
2680
  ...props,
1028
2681
  children: [
1029
- icon && /* @__PURE__ */ jsx15("span", { className: "fractal-sidebar__nav-item-icon", "aria-hidden": "true", children: icon }),
1030
- /* @__PURE__ */ jsx15("span", { className: "fractal-sidebar__nav-item-label", children }),
1031
- endAdornment && /* @__PURE__ */ jsx15("span", { className: "fractal-sidebar__nav-item-end", children: endAdornment })
2682
+ icon && /* @__PURE__ */ jsx30("span", { className: "fractal-sidebar__nav-item-icon", "aria-hidden": "true", children: icon }),
2683
+ /* @__PURE__ */ jsx30("span", { className: "fractal-sidebar__nav-item-label", children }),
2684
+ endAdornment && /* @__PURE__ */ jsx30("span", { className: "fractal-sidebar__nav-item-end", children: endAdornment })
1032
2685
  ]
1033
2686
  }
1034
2687
  )
1035
2688
  );
1036
2689
  SidebarNavItem.displayName = "SidebarNavItem";
1037
- var SidebarFooter = React10.forwardRef(
1038
- ({ className, ...props }, ref) => /* @__PURE__ */ jsx15("div", { ref, className: cn("fractal-sidebar__footer", className), ...props })
2690
+ var SidebarFooter = React15.forwardRef(
2691
+ ({ className, ...props }, ref) => /* @__PURE__ */ jsx30("div", { ref, className: cn("fractal-sidebar__footer", className), ...props })
1039
2692
  );
1040
2693
  SidebarFooter.displayName = "SidebarFooter";
1041
2694
 
1042
2695
  // src/components/SidePanel/SidePanel.tsx
1043
- import { createPortal as createPortal2 } from "react-dom";
1044
- import { Close } from "@mirror-physics/fractal-icons";
1045
- import { jsx as jsx16, jsxs as jsxs13 } from "react/jsx-runtime";
1046
- function SidePanel({ open, onClose, title, description, children, footer, size = "md", className }) {
2696
+ import * as React16 from "react";
2697
+ import { createPortal as createPortal4 } from "react-dom";
2698
+ import { Close as Close2 } from "@mirror-physics/fractal-icons";
2699
+ import { jsx as jsx31, jsxs as jsxs26 } from "react/jsx-runtime";
2700
+ function SidePanel({ open, onClose, title, description, children, footer, size = "md", loading = false, loadingLabel = "Loading", className }) {
1047
2701
  useEscapeDismiss(80, onClose, open);
2702
+ const bodyRef = React16.useRef(null);
2703
+ const [scrollEdges, setScrollEdges] = React16.useState({ above: false, below: false });
2704
+ React16.useLayoutEffect(() => {
2705
+ const body = bodyRef.current;
2706
+ if (!open || !body) return;
2707
+ const measure = () => {
2708
+ const maxScroll = Math.max(0, body.scrollHeight - body.clientHeight);
2709
+ const top = Math.max(0, Math.min(body.scrollTop, maxScroll));
2710
+ const above = maxScroll > 1 && top > 0;
2711
+ const below = maxScroll - top > 1;
2712
+ setScrollEdges((previous) => previous.above === above && previous.below === below ? previous : { above, below });
2713
+ };
2714
+ const resize = new ResizeObserver(measure);
2715
+ const observeContent = () => {
2716
+ resize.disconnect();
2717
+ resize.observe(body);
2718
+ for (const child of body.children) resize.observe(child);
2719
+ measure();
2720
+ };
2721
+ const mutations = new MutationObserver(observeContent);
2722
+ mutations.observe(body, { childList: true, subtree: true, characterData: true, attributes: true });
2723
+ body.addEventListener("scroll", measure, { passive: true });
2724
+ body.addEventListener("load", measure, true);
2725
+ observeContent();
2726
+ return () => {
2727
+ resize.disconnect();
2728
+ mutations.disconnect();
2729
+ body.removeEventListener("scroll", measure);
2730
+ body.removeEventListener("load", measure, true);
2731
+ };
2732
+ }, [open, loading]);
1048
2733
  if (!open) return null;
1049
- return createPortal2(
1050
- /* @__PURE__ */ jsx16("div", { className: "fractal-side-panel-overlay", onMouseDown: onClose, children: /* @__PURE__ */ jsxs13(
2734
+ return createPortal4(
2735
+ /* @__PURE__ */ jsx31("div", { className: "fractal-side-panel-overlay", onMouseDown: onClose, children: /* @__PURE__ */ jsxs26(
1051
2736
  "aside",
1052
2737
  {
1053
2738
  className: cn("fractal-side-panel", `fractal-side-panel--${size}`, className),
@@ -1056,15 +2741,15 @@ function SidePanel({ open, onClose, title, description, children, footer, size =
1056
2741
  role: "dialog",
1057
2742
  onMouseDown: (event) => event.stopPropagation(),
1058
2743
  children: [
1059
- /* @__PURE__ */ jsxs13("header", { className: "fractal-side-panel__header", children: [
1060
- /* @__PURE__ */ jsxs13("div", { className: "fractal-side-panel__heading", children: [
1061
- /* @__PURE__ */ jsx16("h2", { className: "fractal-side-panel__title", children: title }),
1062
- description && /* @__PURE__ */ jsx16("p", { className: "fractal-side-panel__description", children: description })
2744
+ /* @__PURE__ */ jsxs26("header", { className: "fractal-side-panel__header", "data-scroll-divider": scrollEdges.above, children: [
2745
+ /* @__PURE__ */ jsxs26("div", { className: "fractal-side-panel__heading", children: [
2746
+ typeof title === "string" ? /* @__PURE__ */ jsx31("h2", { className: "fractal-side-panel__title", children: title }) : /* @__PURE__ */ jsx31("div", { className: "fractal-side-panel__title", children: title }),
2747
+ description && /* @__PURE__ */ jsx31("p", { className: "fractal-side-panel__description", children: description })
1063
2748
  ] }),
1064
- /* @__PURE__ */ jsx16("button", { type: "button", className: "fractal-side-panel__close", "aria-label": "Close panel", onClick: onClose, children: /* @__PURE__ */ jsx16(Close, { size: 18 }) })
2749
+ /* @__PURE__ */ jsx31("button", { type: "button", className: "fractal-side-panel__close", "aria-label": "Close panel", onClick: onClose, children: /* @__PURE__ */ jsx31(Close2, { size: 18 }) })
1065
2750
  ] }),
1066
- /* @__PURE__ */ jsx16("div", { className: "fractal-side-panel__body", children }),
1067
- footer && /* @__PURE__ */ jsx16("footer", { className: "fractal-side-panel__footer", children: footer })
2751
+ /* @__PURE__ */ jsx31("div", { ref: bodyRef, "aria-busy": loading || void 0, className: cn("fractal-side-panel__body", loading && "fractal-side-panel__body--loading"), children: loading ? /* @__PURE__ */ jsx31(UILoader, { label: loadingLabel }) : children }),
2752
+ footer && /* @__PURE__ */ jsx31("footer", { className: "fractal-side-panel__footer", "data-scroll-divider": scrollEdges.below, children: footer })
1068
2753
  ]
1069
2754
  }
1070
2755
  ) }),
@@ -1073,47 +2758,325 @@ function SidePanel({ open, onClose, title, description, children, footer, size =
1073
2758
  }
1074
2759
 
1075
2760
  // src/components/Textarea/Textarea.tsx
1076
- import * as React11 from "react";
1077
- import { jsx as jsx17 } from "react/jsx-runtime";
1078
- var Textarea = React11.forwardRef(
1079
- ({ className, ...props }, ref) => /* @__PURE__ */ jsx17("textarea", { ref, className: cn("fractal-textarea", className), ...props })
2761
+ import * as React17 from "react";
2762
+ import { jsx as jsx32, jsxs as jsxs27 } from "react/jsx-runtime";
2763
+ var Textarea = React17.forwardRef(
2764
+ ({ className, fieldLabel, textStyle = "source", id, placeholder, ...props }, ref) => {
2765
+ const generatedId = React17.useId();
2766
+ const fieldId = id ?? generatedId;
2767
+ const input = /* @__PURE__ */ jsx32(
2768
+ "textarea",
2769
+ {
2770
+ ref,
2771
+ id: fieldLabel ? fieldId : id,
2772
+ placeholder: fieldLabel ? placeholder || " " : placeholder,
2773
+ className: cn("fractal-textarea", className),
2774
+ "data-text-style": textStyle,
2775
+ ...props
2776
+ }
2777
+ );
2778
+ return fieldLabel ? /* @__PURE__ */ jsxs27("div", { className: "fractal-floating-field fractal-floating-field--textarea", "data-control-size": "md", children: [
2779
+ input,
2780
+ /* @__PURE__ */ jsx32("label", { className: "fractal-floating-field__label", htmlFor: fieldId, children: fieldLabel })
2781
+ ] }) : input;
2782
+ }
1080
2783
  );
1081
2784
  Textarea.displayName = "Textarea";
2785
+
2786
+ // src/components/TextButton/TextButton.tsx
2787
+ import * as React18 from "react";
2788
+ import { jsx as jsx33, jsxs as jsxs28 } from "react/jsx-runtime";
2789
+ var TextButton = React18.forwardRef(
2790
+ ({ className, icon, iconPosition = "start", size = "md", tone = "primary", children, ...props }, ref) => /* @__PURE__ */ jsxs28(
2791
+ "button",
2792
+ {
2793
+ ref,
2794
+ className: cn("fractal-text-button", `fractal-text-button--${size}`, `fractal-text-button--${tone}`, className),
2795
+ type: "button",
2796
+ ...props,
2797
+ children: [
2798
+ icon && iconPosition === "start" && /* @__PURE__ */ jsx33("span", { className: "fractal-text-button__icon", "aria-hidden": "true", children: icon }),
2799
+ /* @__PURE__ */ jsx33("span", { children }),
2800
+ icon && iconPosition === "end" && /* @__PURE__ */ jsx33("span", { className: "fractal-text-button__icon", "aria-hidden": "true", children: icon })
2801
+ ]
2802
+ }
2803
+ )
2804
+ );
2805
+ TextButton.displayName = "TextButton";
2806
+
2807
+ // src/components/FileDropzone/FileDropzone.tsx
2808
+ import * as React19 from "react";
2809
+ import { Close as Close3, FileArrowUp } from "@mirror-physics/fractal-icons";
2810
+ import { jsx as jsx34, jsxs as jsxs29 } from "react/jsx-runtime";
2811
+ function FileDropzone({
2812
+ files,
2813
+ onFilesChange,
2814
+ accept,
2815
+ multiple = true,
2816
+ disabled = false,
2817
+ title = "Drop files here or click to browse",
2818
+ description = "Files stay attached to this record.",
2819
+ className
2820
+ }) {
2821
+ const inputId = React19.useId();
2822
+ const inputRef = React19.useRef(null);
2823
+ const [isDragging, setIsDragging] = React19.useState(false);
2824
+ const addFiles = (nextFiles) => {
2825
+ if (disabled) return;
2826
+ const additions = Array.from(nextFiles);
2827
+ if (additions.length === 0) return;
2828
+ onFilesChange(multiple ? [...files, ...additions] : additions.slice(0, 1));
2829
+ };
2830
+ return /* @__PURE__ */ jsxs29("div", { className: cn("fractal-file-dropzone", className), children: [
2831
+ /* @__PURE__ */ jsx34(
2832
+ "input",
2833
+ {
2834
+ ref: inputRef,
2835
+ id: inputId,
2836
+ className: "fractal-file-dropzone__input",
2837
+ type: "file",
2838
+ accept,
2839
+ multiple,
2840
+ disabled,
2841
+ onChange: (event) => {
2842
+ if (event.target.files) addFiles(event.target.files);
2843
+ event.target.value = "";
2844
+ }
2845
+ }
2846
+ ),
2847
+ /* @__PURE__ */ jsxs29(
2848
+ "button",
2849
+ {
2850
+ className: "fractal-file-dropzone__target",
2851
+ "data-dragging": isDragging || void 0,
2852
+ type: "button",
2853
+ disabled,
2854
+ onClick: () => inputRef.current?.click(),
2855
+ onDragEnter: (event) => {
2856
+ event.preventDefault();
2857
+ if (!disabled) setIsDragging(true);
2858
+ },
2859
+ onDragOver: (event) => event.preventDefault(),
2860
+ onDragLeave: (event) => {
2861
+ event.preventDefault();
2862
+ setIsDragging(false);
2863
+ },
2864
+ onDrop: (event) => {
2865
+ event.preventDefault();
2866
+ setIsDragging(false);
2867
+ addFiles(event.dataTransfer.files);
2868
+ },
2869
+ children: [
2870
+ /* @__PURE__ */ jsx34(FileArrowUp, { "aria-hidden": "true", size: 20 }),
2871
+ /* @__PURE__ */ jsxs29("span", { className: "fractal-file-dropzone__copy", children: [
2872
+ /* @__PURE__ */ jsx34("span", { className: "fractal-file-dropzone__title", children: title }),
2873
+ description && /* @__PURE__ */ jsx34("span", { className: "fractal-file-dropzone__description", children: description })
2874
+ ] })
2875
+ ]
2876
+ }
2877
+ ),
2878
+ files.length > 0 && /* @__PURE__ */ jsx34("ul", { className: "fractal-file-dropzone__files", "aria-label": "Attached files", children: files.map((file, index) => /* @__PURE__ */ jsxs29("li", { className: "fractal-file-dropzone__file", children: [
2879
+ /* @__PURE__ */ jsx34("span", { className: "fractal-file-dropzone__file-name", children: file.name }),
2880
+ /* @__PURE__ */ jsx34("span", { className: "fractal-file-dropzone__file-meta", children: formatFileSize(file.size) }),
2881
+ /* @__PURE__ */ jsx34(
2882
+ "button",
2883
+ {
2884
+ className: "fractal-file-dropzone__remove",
2885
+ type: "button",
2886
+ "aria-label": `Remove ${file.name}`,
2887
+ disabled,
2888
+ onClick: () => onFilesChange(files.filter((_, fileIndex) => fileIndex !== index)),
2889
+ children: /* @__PURE__ */ jsx34(Close3, { "aria-hidden": "true", size: 14 })
2890
+ }
2891
+ )
2892
+ ] }, `${file.name}-${file.size}-${index}`)) })
2893
+ ] });
2894
+ }
2895
+ function formatFileSize(bytes) {
2896
+ if (bytes < 1024) return `${bytes} B`;
2897
+ if (bytes < 1024 * 1024) return `${Math.round(bytes / 1024)} KB`;
2898
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
2899
+ }
2900
+
2901
+ // src/components/Pagination/Pagination.tsx
2902
+ import { ArrowLeft, ArrowRight } from "@mirror-physics/fractal-icons";
2903
+ import { jsx as jsx35, jsxs as jsxs30 } from "react/jsx-runtime";
2904
+ function Pagination({
2905
+ page,
2906
+ pageSize,
2907
+ totalItems,
2908
+ onPageChange,
2909
+ pageSizeOptions,
2910
+ onPageSizeChange,
2911
+ ariaLabel = "Pagination",
2912
+ className
2913
+ }) {
2914
+ const totalPages = Math.max(1, Math.ceil(totalItems / pageSize));
2915
+ const currentPage = Math.min(Math.max(page, 1), totalPages);
2916
+ const start = totalItems === 0 ? 0 : (currentPage - 1) * pageSize + 1;
2917
+ const end = Math.min(currentPage * pageSize, totalItems);
2918
+ const canChangePageSize = pageSizeOptions != null && pageSizeOptions.length > 0 && onPageSizeChange != null;
2919
+ return /* @__PURE__ */ jsxs30("nav", { className: cn("fractal-pagination", className), "aria-label": ariaLabel, children: [
2920
+ /* @__PURE__ */ jsx35("span", { className: "fractal-pagination__summary", children: totalItems === 0 ? "No results" : `${start}-${end} of ${totalItems}` }),
2921
+ /* @__PURE__ */ jsxs30("div", { className: "fractal-pagination__controls", children: [
2922
+ canChangePageSize && /* @__PURE__ */ jsx35(
2923
+ Dropdown,
2924
+ {
2925
+ align: "right",
2926
+ className: "fractal-pagination__page-size",
2927
+ direction: "up",
2928
+ items: pageSizeOptions.map((option) => ({
2929
+ value: String(option),
2930
+ description: `${option} per page`
2931
+ })),
2932
+ onSelect: (value) => onPageSizeChange(Number(value)),
2933
+ selectedValue: String(pageSize),
2934
+ size: "sm",
2935
+ triggerLabel: "Results per page",
2936
+ triggerVariant: "tertiary"
2937
+ }
2938
+ ),
2939
+ /* @__PURE__ */ jsx35(
2940
+ "button",
2941
+ {
2942
+ className: "fractal-pagination__button",
2943
+ type: "button",
2944
+ "aria-label": "Previous page",
2945
+ title: "Previous page",
2946
+ disabled: currentPage === 1 || totalItems === 0,
2947
+ onClick: () => onPageChange(currentPage - 1),
2948
+ children: /* @__PURE__ */ jsx35(ArrowLeft, { "aria-hidden": "true", size: 16 })
2949
+ }
2950
+ ),
2951
+ /* @__PURE__ */ jsxs30("span", { className: "fractal-pagination__page", "aria-current": "page", children: [
2952
+ currentPage,
2953
+ " of ",
2954
+ totalPages
2955
+ ] }),
2956
+ /* @__PURE__ */ jsx35(
2957
+ "button",
2958
+ {
2959
+ className: "fractal-pagination__button",
2960
+ type: "button",
2961
+ "aria-label": "Next page",
2962
+ title: "Next page",
2963
+ disabled: currentPage === totalPages || totalItems === 0,
2964
+ onClick: () => onPageChange(currentPage + 1),
2965
+ children: /* @__PURE__ */ jsx35(ArrowRight, { "aria-hidden": "true", size: 16 })
2966
+ }
2967
+ )
2968
+ ] })
2969
+ ] });
2970
+ }
2971
+
2972
+ // src/components/Tooltip/Tooltip.tsx
2973
+ import * as TooltipPrimitive from "@radix-ui/react-tooltip";
2974
+ import { jsx as jsx36, jsxs as jsxs31 } from "react/jsx-runtime";
2975
+ function Tooltip({
2976
+ content,
2977
+ children,
2978
+ placement = "bottom",
2979
+ maxWidth = 320,
2980
+ delay = 0,
2981
+ disabled = false,
2982
+ className
2983
+ }) {
2984
+ if (disabled || content == null || content === "") return children;
2985
+ return /* @__PURE__ */ jsx36(TooltipPrimitive.Provider, { delayDuration: delay, children: /* @__PURE__ */ jsxs31(TooltipPrimitive.Root, { children: [
2986
+ /* @__PURE__ */ jsx36(TooltipPrimitive.Trigger, { asChild: true, children }),
2987
+ /* @__PURE__ */ jsx36(TooltipPrimitive.Portal, { children: /* @__PURE__ */ jsx36(
2988
+ TooltipPrimitive.Content,
2989
+ {
2990
+ side: placement,
2991
+ sideOffset: 8,
2992
+ collisionPadding: 8,
2993
+ className: cn("fractal-tooltip", className),
2994
+ style: { "--tooltip-max-width": typeof maxWidth === "number" ? `${maxWidth}px` : maxWidth },
2995
+ children: content
2996
+ }
2997
+ ) })
2998
+ ] }) });
2999
+ }
1082
3000
  export {
1083
3001
  Alert,
1084
3002
  AlertDescription,
3003
+ AlertGhost,
1085
3004
  AlertTitle,
3005
+ Breadcrumbs,
1086
3006
  Button,
3007
+ ButtonGhost,
1087
3008
  Card,
1088
3009
  CardContent,
1089
3010
  CardDescription,
1090
3011
  CardFooter,
3012
+ CardGhost,
1091
3013
  CardHeader,
1092
3014
  CardTitle,
3015
+ Checkbox,
3016
+ CheckboxGhost,
1093
3017
  Chip,
3018
+ ChipGhost,
3019
+ CodeBlock,
3020
+ ColumnEditor,
3021
+ ContentSwitcher,
3022
+ ContentSwitcherGhost,
3023
+ ConversationGhost,
1094
3024
  DataTable,
3025
+ DataTableGhost,
1095
3026
  Disclosure,
3027
+ DisclosureGhost,
1096
3028
  Dropdown,
3029
+ DropdownGhost,
1097
3030
  FeatureCard,
3031
+ FileDropzone,
3032
+ FileDropzoneGhost,
1098
3033
  FolderCard,
3034
+ Ghost,
3035
+ GhostLine,
1099
3036
  Input,
3037
+ InputGhost,
1100
3038
  Label,
3039
+ LabelGhost,
3040
+ LatticeLoader,
1101
3041
  Link,
3042
+ LinkGhost,
1102
3043
  Modal,
3044
+ ModalGhost,
3045
+ NumberInput,
3046
+ Pagination,
3047
+ PaginationGhost,
3048
+ PasswordInput,
1103
3049
  PromptCard,
3050
+ PromptCardGhost,
1104
3051
  PromptGrid,
3052
+ Radio,
1105
3053
  SidePanel,
3054
+ SidePanelGhost,
1106
3055
  Sidebar,
3056
+ SidebarAccountMenu,
1107
3057
  SidebarBrand,
1108
3058
  SidebarFooter,
3059
+ SidebarGhost,
1109
3060
  SidebarHeader,
1110
3061
  SidebarNav,
1111
3062
  SidebarNavItem,
1112
3063
  SidebarSection,
3064
+ Slider,
1113
3065
  SortableTable,
3066
+ SortableTableGhost,
3067
+ TableSearch,
1114
3068
  Tabs,
3069
+ TabsGhost,
3070
+ TextButton,
3071
+ TextButtonGhost,
1115
3072
  Textarea,
3073
+ TextareaGhost,
3074
+ Toast,
3075
+ ToastViewport,
1116
3076
  Toggle,
3077
+ ToggleGhost,
3078
+ Tooltip,
3079
+ UILoader,
1117
3080
  cn,
1118
3081
  useEscapeDismiss
1119
3082
  };