@mirror-physics/fractal-ui 0.2.0-next.9 → 0.2.0-next.90

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-46TOMY4G.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);
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);
122
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,365 @@ 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
+
512
+ // src/utils/FloatingFieldBorder.tsx
513
+ import { jsx as jsx5 } from "react/jsx-runtime";
514
+ function FloatingFieldBorder({ label }) {
515
+ return /* @__PURE__ */ jsx5("fieldset", { className: "fractal-floating-field__border", "aria-hidden": "true", children: /* @__PURE__ */ jsx5("legend", { children: /* @__PURE__ */ jsx5("span", { children: label }) }) });
516
+ }
517
+
518
+ // src/components/Input/Input.tsx
519
+ import { jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
520
+ var Input = React3.forwardRef(
521
+ ({ className, type, controlSize = "lg", fieldLabel, id, placeholder, ...props }, ref) => {
522
+ const generatedId = React3.useId();
523
+ const fieldId = id ?? generatedId;
524
+ const floatingLabel = type === "search" ? void 0 : fieldLabel;
525
+ const input = /* @__PURE__ */ jsx6(
526
+ "input",
527
+ {
528
+ type,
529
+ ref,
530
+ id: floatingLabel ? fieldId : id,
531
+ placeholder: floatingLabel ? placeholder || " " : placeholder,
532
+ "data-control-size": controlSize,
533
+ className: cn("fractal-control", "fractal-input", className),
534
+ ...props,
535
+ "aria-label": props["aria-label"] ?? (type === "search" && !props["aria-labelledby"] ? fieldLabel ?? "Search" : void 0)
536
+ }
537
+ );
538
+ if (!floatingLabel) return input;
539
+ return /* @__PURE__ */ jsxs5(
540
+ "div",
541
+ {
542
+ className: "fractal-floating-field",
543
+ "data-control-size": controlSize,
544
+ "data-always-float": ["date", "datetime-local", "time", "month", "week", "color", "file", "range"].includes(type ?? "") || void 0,
545
+ children: [
546
+ input,
547
+ /* @__PURE__ */ jsx6(FloatingFieldBorder, { label: fieldLabel ?? "" }),
548
+ /* @__PURE__ */ jsx6("label", { className: "fractal-floating-field__label", htmlFor: fieldId, children: fieldLabel })
549
+ ]
550
+ }
551
+ );
552
+ }
553
+ );
554
+ Input.displayName = "Input";
555
+
556
+ // src/components/TableSearch/TableSearch.tsx
300
557
  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",
558
+ import { MagnifyingGlass, MagnifyingGlassLines } from "@mirror-physics/fractal-icons";
559
+ import { jsx as jsx7, jsxs as jsxs6 } from "react/jsx-runtime";
560
+ var TableSearch = React4.forwardRef(
561
+ ({
562
+ searchType = "text",
563
+ controlSize = "lg",
564
+ className,
565
+ inputClassName,
566
+ placeholder,
567
+ title,
568
+ ...props
569
+ }, ref) => {
570
+ const isGlob = searchType === "glob";
571
+ const SearchIcon = isGlob ? MagnifyingGlassLines : MagnifyingGlass;
572
+ return /* @__PURE__ */ jsxs6(
573
+ "div",
574
+ {
575
+ className: cn("fractal-table-search", className),
576
+ "data-search-type": searchType,
577
+ "data-control-size": controlSize,
578
+ children: [
579
+ /* @__PURE__ */ jsx7(SearchIcon, { className: "fractal-table-search__icon", size: 17, "aria-hidden": "true" }),
580
+ /* @__PURE__ */ jsx7(
581
+ Input,
582
+ {
583
+ ...props,
584
+ controlSize,
585
+ ref,
586
+ type: "search",
587
+ className: cn("fractal-table-search__input", inputClassName),
588
+ placeholder: placeholder ?? (isGlob ? "Glob search \u2014 use *, ?, or [a-z]" : "Search"),
589
+ title: title ?? (isGlob ? "Glob pattern: *, **, ?, [abc], [a-z], and [!abc]" : void 0)
590
+ }
591
+ )
592
+ ]
593
+ }
594
+ );
595
+ }
596
+ );
597
+ TableSearch.displayName = "TableSearch";
598
+
599
+ // src/components/NumberInput/NumberInput.tsx
600
+ import * as React5 from "react";
601
+ import { Minus, Plus } from "@mirror-physics/fractal-icons";
602
+ import { jsx as jsx8, jsxs as jsxs7 } from "react/jsx-runtime";
603
+ function NumberInput({
604
+ unit,
605
+ controlSize = "lg",
606
+ className,
607
+ value,
608
+ defaultValue,
609
+ disabled,
610
+ readOnly,
611
+ min,
612
+ max,
613
+ ...props
614
+ }) {
615
+ const inputRef = React5.useRef(null);
616
+ const numericValue = Number(value ?? defaultValue);
617
+ const characterCount = Math.max(String(value ?? defaultValue ?? "").length, 1);
618
+ const controlLabel = typeof props["aria-label"] === "string" ? props["aria-label"] : props.fieldLabel ?? "value";
619
+ const decrementDisabled = disabled || readOnly || Number.isFinite(numericValue) && min !== void 0 && numericValue <= Number(min);
620
+ const incrementDisabled = disabled || readOnly || Number.isFinite(numericValue) && max !== void 0 && numericValue >= Number(max);
621
+ const changeByStep = (direction) => {
622
+ const input = inputRef.current;
623
+ if (!input) return;
624
+ if (direction === -1) input.stepDown();
625
+ else input.stepUp();
626
+ input.dispatchEvent(new Event("input", { bubbles: true }));
627
+ input.focus();
628
+ };
629
+ return /* @__PURE__ */ jsxs7("div", { "data-control-size": controlSize, className: cn("fractal-number-input", unit && "fractal-number-input--with-unit", className), children: [
630
+ /* @__PURE__ */ jsx8(
631
+ Input,
632
+ {
633
+ ref: inputRef,
634
+ controlSize,
635
+ type: "number",
636
+ value,
637
+ defaultValue,
638
+ disabled,
639
+ readOnly,
640
+ min,
641
+ max,
642
+ ...props
643
+ }
644
+ ),
645
+ unit && /* @__PURE__ */ jsx8(
646
+ "span",
647
+ {
648
+ className: "fractal-number-input-unit",
649
+ style: { left: `calc(var(--control-padding-inline) + ${characterCount}ch)` },
650
+ "aria-hidden": "true",
651
+ children: unit
652
+ }
653
+ ),
654
+ /* @__PURE__ */ jsxs7("div", { className: "fractal-number-input-controls", children: [
655
+ /* @__PURE__ */ jsx8(
656
+ "button",
657
+ {
658
+ type: "button",
659
+ className: "fractal-number-input-step",
660
+ "aria-label": `Decrease ${controlLabel}`,
661
+ disabled: decrementDisabled,
662
+ onClick: () => changeByStep(-1),
663
+ children: /* @__PURE__ */ jsx8(Minus, { "aria-hidden": "true", size: 16 })
664
+ }
665
+ ),
666
+ /* @__PURE__ */ jsx8(
667
+ "button",
668
+ {
669
+ type: "button",
670
+ className: "fractal-number-input-step",
671
+ "aria-label": `Increase ${controlLabel}`,
672
+ disabled: incrementDisabled,
673
+ onClick: () => changeByStep(1),
674
+ children: /* @__PURE__ */ jsx8(Plus, { "aria-hidden": "true", size: 16 })
675
+ }
676
+ )
677
+ ] })
678
+ ] });
679
+ }
680
+
681
+ // src/components/Slider/Slider.tsx
682
+ import { jsx as jsx9, jsxs as jsxs8 } from "react/jsx-runtime";
683
+ function Slider(props) {
684
+ const {
685
+ min = 0,
686
+ max = 100,
687
+ step = 1,
688
+ disabled = false,
689
+ className,
690
+ showValues = true,
691
+ formatValue = (value) => value,
692
+ knobs = 1
693
+ } = props;
694
+ const span = Math.max(max - min, 1);
695
+ const rawValues = knobs === 2 ? props.value : [props.value];
696
+ const firstValue = clamp(rawValues[0], min, max);
697
+ const secondValue = knobs === 2 ? clamp(rawValues[1], firstValue, max) : firstValue;
698
+ const selectionStart = knobs === 2 ? firstValue : min;
699
+ const selectionEnd = secondValue;
700
+ const selectionStartPercent = (selectionStart - min) / span * 100;
701
+ const selectionEndPercent = (selectionEnd - min) / span * 100;
702
+ const changeSingleValue = (value) => {
703
+ ;
704
+ props.onValueChange(clamp(value, min, max));
705
+ };
706
+ const changeRangeValue = (nextLower, nextUpper) => {
707
+ ;
708
+ props.onValueChange([
709
+ clamp(nextLower, min, nextUpper),
710
+ clamp(nextUpper, nextLower, max)
711
+ ]);
712
+ };
713
+ const getKeyboardValue = (event, value) => {
714
+ const pageStep = step * 10;
715
+ const adjustments = {
716
+ ArrowDown: -step,
717
+ ArrowLeft: -step,
718
+ ArrowRight: step,
719
+ ArrowUp: step,
720
+ PageDown: -pageStep,
721
+ PageUp: pageStep
722
+ };
723
+ if (event.key === "Home") return min;
724
+ if (event.key === "End") return max;
725
+ if (!(event.key in adjustments)) return null;
726
+ return value + adjustments[event.key];
727
+ };
728
+ return /* @__PURE__ */ jsxs8(
729
+ "div",
305
730
  {
306
- type,
307
- ref,
308
- className: cn("fractal-input", className),
309
- ...props
731
+ className: cn("fractal-slider", disabled && "fractal-slider--disabled", className),
732
+ "data-disabled": disabled || void 0,
733
+ "data-knobs": knobs,
734
+ children: [
735
+ showValues && /* @__PURE__ */ jsxs8("div", { className: "fractal-slider-values", "aria-hidden": "true", children: [
736
+ /* @__PURE__ */ jsx9("output", { children: formatValue(firstValue) }),
737
+ knobs === 2 && /* @__PURE__ */ jsx9("output", { children: formatValue(secondValue) })
738
+ ] }),
739
+ /* @__PURE__ */ jsxs8(
740
+ "div",
741
+ {
742
+ className: "fractal-slider-control",
743
+ style: {
744
+ "--fractal-slider-selection-start": `${selectionStartPercent}%`,
745
+ "--fractal-slider-selection-end": `${selectionEndPercent}%`
746
+ },
747
+ children: [
748
+ /* @__PURE__ */ jsx9("span", { className: "fractal-slider-track", "aria-hidden": "true" }),
749
+ /* @__PURE__ */ jsx9("span", { className: "fractal-slider-selection", "aria-hidden": "true" }),
750
+ /* @__PURE__ */ jsx9(
751
+ "input",
752
+ {
753
+ "aria-label": knobs === 2 ? props.lowerAriaLabel ?? "Lower value" : props.ariaLabel ?? "Slider value",
754
+ disabled,
755
+ max,
756
+ min,
757
+ onChange: (event) => {
758
+ const nextValue = event.currentTarget.valueAsNumber;
759
+ if (knobs === 2) changeRangeValue(Math.min(nextValue, secondValue), secondValue);
760
+ else changeSingleValue(nextValue);
761
+ },
762
+ onKeyDown: (event) => {
763
+ const nextValue = getKeyboardValue(event, firstValue);
764
+ if (nextValue === null) return;
765
+ event.preventDefault();
766
+ if (knobs === 2) changeRangeValue(Math.min(nextValue, secondValue), secondValue);
767
+ else changeSingleValue(nextValue);
768
+ },
769
+ step,
770
+ type: "range",
771
+ value: firstValue
772
+ }
773
+ ),
774
+ knobs === 2 && /* @__PURE__ */ jsx9(
775
+ "input",
776
+ {
777
+ "aria-label": props.upperAriaLabel ?? "Upper value",
778
+ disabled,
779
+ max,
780
+ min,
781
+ onChange: (event) => {
782
+ const nextValue = event.currentTarget.valueAsNumber;
783
+ changeRangeValue(firstValue, Math.max(nextValue, firstValue));
784
+ },
785
+ onKeyDown: (event) => {
786
+ const nextValue = getKeyboardValue(event, secondValue);
787
+ if (nextValue === null) return;
788
+ event.preventDefault();
789
+ changeRangeValue(firstValue, Math.max(nextValue, firstValue));
790
+ },
791
+ step,
792
+ type: "range",
793
+ value: secondValue
794
+ }
795
+ )
796
+ ]
797
+ }
798
+ )
799
+ ]
310
800
  }
311
- )
801
+ );
802
+ }
803
+ function clamp(value, minimum, maximum) {
804
+ return Math.min(maximum, Math.max(minimum, value));
805
+ }
806
+
807
+ // src/components/PasswordInput/PasswordInput.tsx
808
+ import * as React6 from "react";
809
+ import { Eye, EyeSlash } from "@mirror-physics/fractal-icons";
810
+ import { jsx as jsx10, jsxs as jsxs9 } from "react/jsx-runtime";
811
+ var PasswordInput = React6.forwardRef(
812
+ ({
813
+ className,
814
+ controlSize = "lg",
815
+ disabled,
816
+ showPasswordLabel = "Show password",
817
+ hidePasswordLabel = "Hide password",
818
+ ...props
819
+ }, ref) => {
820
+ const [passwordVisible, setPasswordVisible] = React6.useState(false);
821
+ const toggleLabel = passwordVisible ? hidePasswordLabel : showPasswordLabel;
822
+ return /* @__PURE__ */ jsxs9(
823
+ "div",
824
+ {
825
+ className: cn("fractal-password-input", className),
826
+ "data-control-size": controlSize,
827
+ "data-password-visible": passwordVisible ? "" : void 0,
828
+ children: [
829
+ /* @__PURE__ */ jsx10(
830
+ Input,
831
+ {
832
+ ...props,
833
+ controlSize,
834
+ ref,
835
+ className: "fractal-password-input-field",
836
+ disabled,
837
+ type: passwordVisible ? "text" : "password"
838
+ }
839
+ ),
840
+ /* @__PURE__ */ jsx10(
841
+ "button",
842
+ {
843
+ type: "button",
844
+ className: "fractal-password-input-toggle",
845
+ "aria-label": toggleLabel,
846
+ "aria-pressed": passwordVisible,
847
+ disabled,
848
+ title: toggleLabel,
849
+ onClick: () => setPasswordVisible((visible) => !visible),
850
+ children: passwordVisible ? /* @__PURE__ */ jsx10(EyeSlash, { "aria-hidden": "true", size: 18 }) : /* @__PURE__ */ jsx10(Eye, { "aria-hidden": "true", size: 18 })
851
+ }
852
+ )
853
+ ]
854
+ }
855
+ );
856
+ }
312
857
  );
313
- Input.displayName = "Input";
858
+ PasswordInput.displayName = "PasswordInput";
314
859
 
315
860
  // 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(
861
+ import * as React7 from "react";
862
+ import { jsx as jsx11 } from "react/jsx-runtime";
863
+ var Label = React7.forwardRef(
864
+ ({ className, controlSize = "lg", ...props }, ref) => /* @__PURE__ */ jsx11(
320
865
  "label",
321
866
  {
322
867
  ref,
868
+ "data-control-size": controlSize,
323
869
  className: cn("fractal-label", className),
324
870
  ...props
325
871
  }
@@ -328,44 +874,147 @@ var Label = React5.forwardRef(
328
874
  Label.displayName = "Label";
329
875
 
330
876
  // src/components/Alert/Alert.tsx
331
- import * as React6 from "react";
332
- import { jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
877
+ import * as React8 from "react";
878
+ import { CircleCheck, CircleClose, CircleInfo, Warning } from "@mirror-physics/fractal-icons";
879
+ import { jsx as jsx12, jsxs as jsxs10 } from "react/jsx-runtime";
333
880
  var defaultIcons = {
334
- default: "\u2139",
335
- info: "\u2139",
336
- success: "\u2713",
337
- warning: "\u26A0",
338
- error: "\u2715"
881
+ default: CircleInfo,
882
+ info: CircleInfo,
883
+ success: CircleCheck,
884
+ warning: Warning,
885
+ error: CircleClose
886
+ };
887
+ var Alert = React8.forwardRef(
888
+ ({ className, variant = "default", icon, children, ...props }, ref) => {
889
+ const DefaultIcon = defaultIcons[variant];
890
+ return /* @__PURE__ */ jsxs10(
891
+ "div",
892
+ {
893
+ ref,
894
+ role: "alert",
895
+ className: cn("fractal-alert", `fractal-alert--${variant}`, className),
896
+ ...props,
897
+ children: [
898
+ /* @__PURE__ */ jsx12("span", { className: "fractal-alert-icon", "aria-hidden": true, children: icon ?? /* @__PURE__ */ jsx12(DefaultIcon, { size: 18 }) }),
899
+ /* @__PURE__ */ jsx12("div", { className: "fractal-alert-content", children })
900
+ ]
901
+ }
902
+ );
903
+ }
904
+ );
905
+ Alert.displayName = "Alert";
906
+ var AlertTitle = React8.forwardRef(
907
+ ({ className, ...props }, ref) => /* @__PURE__ */ jsx12("h5", { ref, className: cn("fractal-alert-title", className), ...props })
908
+ );
909
+ AlertTitle.displayName = "AlertTitle";
910
+ var AlertDescription = React8.forwardRef(
911
+ ({ className, ...props }, ref) => /* @__PURE__ */ jsx12("div", { ref, className: cn("fractal-alert-description", className), ...props })
912
+ );
913
+ AlertDescription.displayName = "AlertDescription";
914
+
915
+ // src/components/Toast/Toast.tsx
916
+ import * as React9 from "react";
917
+ import { createPortal } from "react-dom";
918
+ import { CircleCheck as CircleCheck2, CircleClose as CircleClose2, CircleInfo as CircleInfo2, Close, Warning as Warning2 } from "@mirror-physics/fractal-icons";
919
+ import { jsx as jsx13, jsxs as jsxs11 } from "react/jsx-runtime";
920
+ var defaultIcons2 = {
921
+ default: CircleInfo2,
922
+ info: CircleInfo2,
923
+ success: CircleCheck2,
924
+ warning: Warning2,
925
+ error: CircleClose2
339
926
  };
340
- var Alert = React6.forwardRef(
341
- ({ className, variant = "default", icon, children, ...props }, ref) => /* @__PURE__ */ jsxs4(
927
+ var Toast = React9.forwardRef(function Toast2({
928
+ action,
929
+ children,
930
+ className,
931
+ dismissLabel = "Dismiss notification",
932
+ duration,
933
+ icon,
934
+ onBlurCapture,
935
+ onDismiss,
936
+ onFocusCapture,
937
+ onPointerEnter,
938
+ onPointerLeave,
939
+ role,
940
+ variant = "default",
941
+ ...props
942
+ }, ref) {
943
+ const [paused, setPaused] = React9.useState(false);
944
+ const dismissRef = React9.useRef(onDismiss);
945
+ const DefaultIcon = defaultIcons2[variant];
946
+ const resolvedDuration = duration === void 0 ? variant === "error" ? null : 5e3 : duration;
947
+ React9.useEffect(() => {
948
+ dismissRef.current = onDismiss;
949
+ }, [onDismiss]);
950
+ React9.useEffect(() => {
951
+ if (!dismissRef.current || paused || resolvedDuration === null || resolvedDuration <= 0) return;
952
+ const timeout = window.setTimeout(() => dismissRef.current?.(), resolvedDuration);
953
+ return () => window.clearTimeout(timeout);
954
+ }, [paused, resolvedDuration]);
955
+ const announcementRole = role ?? (variant === "error" ? "alert" : "status");
956
+ return /* @__PURE__ */ jsxs11(
342
957
  "div",
343
958
  {
344
959
  ref,
345
- role: "alert",
346
- className: cn("fractal-alert", `fractal-alert--${variant}`, className),
960
+ "aria-atomic": "true",
961
+ "aria-live": announcementRole === "alert" ? "assertive" : "polite",
962
+ className: cn("fractal-toast", `fractal-toast--${variant}`, className),
963
+ role: announcementRole,
964
+ onBlurCapture: (event) => {
965
+ onBlurCapture?.(event);
966
+ if (!event.currentTarget.contains(event.relatedTarget)) setPaused(false);
967
+ },
968
+ onFocusCapture: (event) => {
969
+ onFocusCapture?.(event);
970
+ setPaused(true);
971
+ },
972
+ onPointerEnter: (event) => {
973
+ onPointerEnter?.(event);
974
+ setPaused(true);
975
+ },
976
+ onPointerLeave: (event) => {
977
+ onPointerLeave?.(event);
978
+ setPaused(false);
979
+ },
347
980
  ...props,
348
981
  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 })
982
+ /* @__PURE__ */ jsx13("span", { "aria-hidden": "true", className: "fractal-toast__icon", children: icon ?? /* @__PURE__ */ jsx13(DefaultIcon, { size: 18 }) }),
983
+ /* @__PURE__ */ jsx13("div", { className: "fractal-toast__content", children }),
984
+ action && /* @__PURE__ */ jsx13("div", { className: "fractal-toast__action", children: action }),
985
+ /* @__PURE__ */ jsx13(
986
+ "button",
987
+ {
988
+ "aria-label": dismissLabel,
989
+ className: "fractal-toast__dismiss",
990
+ onClick: onDismiss,
991
+ type: "button",
992
+ children: /* @__PURE__ */ jsx13(Close, { "aria-hidden": "true", size: 14 })
993
+ }
994
+ )
351
995
  ]
352
996
  }
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";
997
+ );
998
+ });
999
+ var ToastViewport = React9.forwardRef(function ToastViewport2({ children, className, portal = true, position = "top-center", ...props }, ref) {
1000
+ const viewport = /* @__PURE__ */ jsx13(
1001
+ "div",
1002
+ {
1003
+ ref,
1004
+ className: cn("fractal-toast-viewport", `fractal-toast-viewport--${position}`, className),
1005
+ "data-position": position,
1006
+ ...props,
1007
+ children
1008
+ }
1009
+ );
1010
+ if (!portal || typeof document === "undefined") return viewport;
1011
+ return createPortal(viewport, document.body);
1012
+ });
364
1013
 
365
1014
  // src/components/Toggle/Toggle.tsx
366
- import { jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
1015
+ import { jsx as jsx14, jsxs as jsxs12 } from "react/jsx-runtime";
367
1016
  function Toggle({ checked, onChange, label, className }) {
368
- return /* @__PURE__ */ jsxs5(
1017
+ return /* @__PURE__ */ jsxs12(
369
1018
  "button",
370
1019
  {
371
1020
  type: "button",
@@ -374,18 +1023,19 @@ function Toggle({ checked, onChange, label, className }) {
374
1023
  className: cn("fractal-toggle", className),
375
1024
  onClick: () => onChange(!checked),
376
1025
  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 })
1026
+ /* @__PURE__ */ jsx14("span", { className: cn("fractal-toggle-track", checked && "fractal-toggle-track--checked"), children: /* @__PURE__ */ jsx14("span", { className: cn("fractal-toggle-thumb", checked && "fractal-toggle-thumb--checked") }) }),
1027
+ label && /* @__PURE__ */ jsx14("span", { className: "fractal-toggle-label", children: label })
379
1028
  ]
380
1029
  }
381
1030
  );
382
1031
  }
383
1032
 
384
1033
  // src/components/Modal/Modal.tsx
385
- import { createPortal } from "react-dom";
1034
+ import * as React10 from "react";
1035
+ import { createPortal as createPortal2 } from "react-dom";
386
1036
 
387
1037
  // src/hooks/useEscapeDismiss.ts
388
- import { useEffect, useRef } from "react";
1038
+ import { useEffect as useEffect3, useRef as useRef4 } from "react";
389
1039
  var nextId = 0;
390
1040
  var stack = [];
391
1041
  function insertSorted(entry) {
@@ -418,9 +1068,9 @@ function ensureGlobalListener() {
418
1068
  );
419
1069
  }
420
1070
  function useEscapeDismiss(priority, handler, enabled = true) {
421
- const handlerRef = useRef(handler);
1071
+ const handlerRef = useRef4(handler);
422
1072
  handlerRef.current = handler;
423
- useEffect(() => {
1073
+ useEffect3(() => {
424
1074
  if (!enabled) return;
425
1075
  ensureGlobalListener();
426
1076
  const id = nextId++;
@@ -434,27 +1084,99 @@ function useEscapeDismiss(priority, handler, enabled = true) {
434
1084
  }, [priority, enabled]);
435
1085
  }
436
1086
 
1087
+ // src/components/UILoader/UILoader.tsx
1088
+ import { useId as useId2 } from "react";
1089
+ import { jsx as jsx15, jsxs as jsxs13 } from "react/jsx-runtime";
1090
+ 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";
1091
+ 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";
1092
+ 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";
1093
+ function UILoader({ size = 48, tone = "neutral", className, style, label = "Loading", ariaHidden = false }) {
1094
+ const rawId = useId2().replace(/[^a-zA-Z0-9_-]/g, "");
1095
+ const clipId = `fractal-ui-loader-clip-${rawId}`;
1096
+ const gradientId = `fractal-ui-loader-gradient-${rawId}`;
1097
+ const height = typeof size === "number" ? `${size}px` : size;
1098
+ return /* @__PURE__ */ jsx15(
1099
+ "span",
1100
+ {
1101
+ "aria-hidden": ariaHidden || void 0,
1102
+ "aria-label": ariaHidden ? void 0 : label,
1103
+ className: cn("fractal-ui-loader", `fractal-ui-loader--${tone}`, className),
1104
+ role: ariaHidden ? void 0 : "status",
1105
+ style,
1106
+ children: /* @__PURE__ */ jsxs13("svg", { "aria-hidden": "true", fill: "none", viewBox: "0 0 144 92", style: { height, width: "auto" }, children: [
1107
+ /* @__PURE__ */ jsxs13("defs", { children: [
1108
+ /* @__PURE__ */ jsxs13("linearGradient", { id: gradientId, x1: "59", x2: "94.3", y1: "0", y2: "89.1", gradientUnits: "userSpaceOnUse", children: [
1109
+ /* @__PURE__ */ jsx15("stop", { className: "fractal-ui-loader__stop-start", offset: "0" }),
1110
+ /* @__PURE__ */ jsx15("stop", { className: "fractal-ui-loader__stop-end", offset: "1" })
1111
+ ] }),
1112
+ /* @__PURE__ */ jsxs13("linearGradient", { id: `${gradientId}-shine`, x1: "0", x2: "1", y1: "0", y2: "0", children: [
1113
+ /* @__PURE__ */ jsx15("stop", { offset: "0.4", stopColor: "white", stopOpacity: "0" }),
1114
+ /* @__PURE__ */ jsx15("stop", { offset: "0.5", stopColor: "white", stopOpacity: "0.62" }),
1115
+ /* @__PURE__ */ jsx15("stop", { offset: "0.6", stopColor: "white", stopOpacity: "0" })
1116
+ ] }),
1117
+ /* @__PURE__ */ jsxs13("clipPath", { id: clipId, children: [
1118
+ /* @__PURE__ */ jsx15("path", { d: LEFT }),
1119
+ /* @__PURE__ */ jsx15("path", { d: CENTER }),
1120
+ /* @__PURE__ */ jsx15("path", { d: RIGHT })
1121
+ ] })
1122
+ ] }),
1123
+ /* @__PURE__ */ jsx15("path", { d: CENTER, fill: `url(#${gradientId})` }),
1124
+ /* @__PURE__ */ jsx15("path", { d: RIGHT, fill: `url(#${gradientId})` }),
1125
+ /* @__PURE__ */ jsx15("path", { d: LEFT, fill: `url(#${gradientId})` }),
1126
+ /* @__PURE__ */ jsx15("g", { clipPath: `url(#${clipId})`, children: /* @__PURE__ */ jsx15("g", { className: "fractal-ui-loader__shine-axis", transform: "translate(72 46) rotate(68.4)", children: /* @__PURE__ */ jsx15("rect", { className: "fractal-ui-loader__shine", x: "-160", y: "-100", width: "320", height: "200", fill: `url(#${gradientId}-shine)` }) }) })
1127
+ ] })
1128
+ }
1129
+ );
1130
+ }
1131
+
437
1132
  // 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);
1133
+ import { jsx as jsx16, jsxs as jsxs14 } from "react/jsx-runtime";
1134
+ var CloseIcon = () => /* @__PURE__ */ jsx16("svg", { width: "16", height: "16", viewBox: "0 0 16 16", fill: "none", xmlns: "http://www.w3.org/2000/svg", children: /* @__PURE__ */ jsx16("path", { d: "M12 4L4 12M4 4L12 12", stroke: "currentColor", strokeWidth: "1.5", strokeLinecap: "round", strokeLinejoin: "round" }) });
1135
+ function Modal({ open, onClose, title, children, danger, warn, footer, noClose, closeDisabled = false, size = "sm", loading = false, loadingLabel = "Loading", scrollable = false, className }) {
1136
+ const [bodyScrolled, setBodyScrolled] = React10.useState(false);
1137
+ useEscapeDismiss(80, onClose, open && !noClose && !closeDisabled);
1138
+ React10.useEffect(() => {
1139
+ if (open) setBodyScrolled(false);
1140
+ }, [open]);
442
1141
  if (!open) return null;
443
1142
  const sizeClass = `fractal-modal-panel--${size}`;
444
1143
  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(
1144
+ return createPortal2(
1145
+ /* @__PURE__ */ jsx16("div", { className: "fractal-modal-overlay", onClick: noClose || closeDisabled ? void 0 : onClose, children: /* @__PURE__ */ jsxs14(
447
1146
  "div",
448
1147
  {
449
- className: cn("fractal-modal-panel", sizeClass, accentClass, className),
1148
+ className: cn("fractal-modal-panel", sizeClass, accentClass, scrollable && "fractal-modal-panel--scrollable", className),
450
1149
  onClick: (e) => e.stopPropagation(),
451
1150
  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 })
1151
+ /* @__PURE__ */ jsxs14(
1152
+ "div",
1153
+ {
1154
+ className: cn(
1155
+ "fractal-modal-title-row",
1156
+ scrollable && "fractal-modal-title-row--scrollable",
1157
+ scrollable && bodyScrolled && "fractal-modal-title-row--scrolled"
1158
+ ),
1159
+ children: [
1160
+ /* @__PURE__ */ jsx16("h3", { className: cn("fractal-modal-title", danger && "fractal-modal-title--danger", warn && "fractal-modal-title--warn"), children: title }),
1161
+ !noClose && /* @__PURE__ */ jsx16("button", { type: "button", onClick: onClose, title: "Close", disabled: closeDisabled, className: "fractal-modal-close", children: /* @__PURE__ */ jsx16(CloseIcon, {}) })
1162
+ ]
1163
+ }
1164
+ ),
1165
+ /* @__PURE__ */ jsx16(
1166
+ "div",
1167
+ {
1168
+ "aria-busy": loading || void 0,
1169
+ className: cn(
1170
+ "fractal-modal-body",
1171
+ size === "lg" && "fractal-modal-body--lg",
1172
+ loading && "fractal-modal-body--loading",
1173
+ scrollable && "fractal-modal-body--scrollable"
1174
+ ),
1175
+ onScroll: scrollable ? (event) => setBodyScrolled(event.currentTarget.scrollTop > 0) : void 0,
1176
+ children: loading ? /* @__PURE__ */ jsx16(UILoader, { label: loadingLabel }) : children
1177
+ }
1178
+ ),
1179
+ footer && /* @__PURE__ */ jsx16("div", { className: cn("fractal-modal-footer", scrollable && "fractal-modal-footer--divided"), children: footer })
458
1180
  ]
459
1181
  }
460
1182
  ) }),
@@ -462,94 +1184,461 @@ function Modal({ open, onClose, title, children, danger, warn, footer, noClose,
462
1184
  );
463
1185
  }
464
1186
 
1187
+ // src/components/TableLayout/TableOverflowMenu.tsx
1188
+ import { useId as useId3, useLayoutEffect, useRef as useRef5, useState as useState5 } from "react";
1189
+ import { createPortal as createPortal3 } from "react-dom";
1190
+ import { Ellipsis } from "@mirror-physics/fractal-icons";
1191
+ import { jsx as jsx17, jsxs as jsxs15 } from "react/jsx-runtime";
1192
+ function TableOverflowMenu({ items, label, disabled, onSelect }) {
1193
+ const id = useId3();
1194
+ const trigger = useRef5(null);
1195
+ const panel = useRef5(null);
1196
+ const initialLast = useRef5(false);
1197
+ const [open, setOpen] = useState5(false);
1198
+ const [position, setPosition] = useState5({ top: 0, left: 0 });
1199
+ const buttons = () => [...panel.current?.querySelectorAll("button:not(:disabled)") ?? []];
1200
+ const close = (restore = true) => {
1201
+ setOpen(false);
1202
+ if (restore) trigger.current?.focus({ preventScroll: true });
1203
+ };
1204
+ useEscapeDismiss(100, () => close(), open);
1205
+ useLayoutEffect(() => {
1206
+ if (!open || !trigger.current || !panel.current) return;
1207
+ const place = () => {
1208
+ const anchor = trigger.current.getBoundingClientRect();
1209
+ const menu = panel.current.getBoundingClientRect();
1210
+ const above = window.innerHeight - anchor.bottom < menu.height + 8 && anchor.top > window.innerHeight - anchor.bottom;
1211
+ setPosition({
1212
+ top: Math.max(8, Math.min(above ? anchor.top - menu.height - 4 : anchor.bottom + 4, window.innerHeight - menu.height - 8)),
1213
+ left: Math.max(8, Math.min(anchor.right - menu.width, window.innerWidth - menu.width - 8))
1214
+ });
1215
+ };
1216
+ place();
1217
+ const enabled = buttons();
1218
+ (initialLast.current ? enabled[enabled.length - 1] : enabled[0])?.focus({ preventScroll: true });
1219
+ if (enabled.length === 0) panel.current.focus({ preventScroll: true });
1220
+ const outside = (event) => {
1221
+ if (!panel.current?.contains(event.target) && !trigger.current?.contains(event.target)) setOpen(false);
1222
+ };
1223
+ const scroll = (event) => {
1224
+ if (!panel.current?.contains(event.target)) setOpen(false);
1225
+ };
1226
+ const observer = new ResizeObserver(place);
1227
+ observer.observe(panel.current);
1228
+ document.addEventListener("pointerdown", outside);
1229
+ window.addEventListener("scroll", scroll, true);
1230
+ window.addEventListener("resize", place);
1231
+ return () => {
1232
+ observer.disconnect();
1233
+ document.removeEventListener("pointerdown", outside);
1234
+ window.removeEventListener("scroll", scroll, true);
1235
+ window.removeEventListener("resize", place);
1236
+ };
1237
+ }, [open]);
1238
+ useLayoutEffect(() => {
1239
+ if (disabled || items.length === 0) setOpen(false);
1240
+ }, [disabled, items.length]);
1241
+ if (items.length === 0) return null;
1242
+ return /* @__PURE__ */ jsxs15("div", { className: "fractal-table-overflow-trigger", children: [
1243
+ /* @__PURE__ */ jsx17(
1244
+ Button,
1245
+ {
1246
+ ref: trigger,
1247
+ buttonType: "ghost",
1248
+ controlSize: "sm",
1249
+ hasIconOnly: true,
1250
+ icon: /* @__PURE__ */ jsx17(Ellipsis, { "aria-hidden": true }),
1251
+ "aria-label": label,
1252
+ "aria-haspopup": "menu",
1253
+ "aria-expanded": open,
1254
+ "aria-controls": open ? id : void 0,
1255
+ disabled,
1256
+ onClick: () => {
1257
+ initialLast.current = false;
1258
+ setOpen(!open);
1259
+ },
1260
+ onKeyDown: (event) => {
1261
+ if (event.key === "ArrowDown" || event.key === "ArrowUp") {
1262
+ event.preventDefault();
1263
+ initialLast.current = event.key === "ArrowUp";
1264
+ setOpen(true);
1265
+ }
1266
+ }
1267
+ }
1268
+ ),
1269
+ open && createPortal3(/* @__PURE__ */ jsx17(
1270
+ "div",
1271
+ {
1272
+ ref: panel,
1273
+ id,
1274
+ role: "menu",
1275
+ tabIndex: -1,
1276
+ "aria-label": label,
1277
+ className: "fractal-table-overflow-menu",
1278
+ style: position,
1279
+ onClick: (event) => event.stopPropagation(),
1280
+ onKeyDown: (event) => {
1281
+ event.stopPropagation();
1282
+ const enabled = buttons(), index = enabled.indexOf(document.activeElement);
1283
+ if (event.key === "Escape") {
1284
+ event.preventDefault();
1285
+ close();
1286
+ } else if (event.key === "Tab") {
1287
+ close();
1288
+ } else if (["ArrowDown", "ArrowUp", "Home", "End"].includes(event.key)) {
1289
+ event.preventDefault();
1290
+ const next = event.key === "Home" ? 0 : event.key === "End" ? enabled.length - 1 : (index + (event.key === "ArrowDown" ? 1 : -1) + enabled.length) % enabled.length;
1291
+ enabled[next]?.focus();
1292
+ } else if (event.key.length === 1 && /\S/.test(event.key)) {
1293
+ const ordered = [...enabled.slice(index + 1), ...enabled.slice(0, index + 1)];
1294
+ ordered.find((button) => button.textContent?.trim().toLocaleLowerCase().startsWith(event.key.toLocaleLowerCase()))?.focus();
1295
+ }
1296
+ },
1297
+ children: items.map((item) => /* @__PURE__ */ jsxs15(
1298
+ "button",
1299
+ {
1300
+ type: "button",
1301
+ role: "menuitem",
1302
+ tabIndex: -1,
1303
+ disabled: item.disabled,
1304
+ onClick: () => {
1305
+ onSelect(item.value);
1306
+ close();
1307
+ },
1308
+ children: [
1309
+ item.icon && /* @__PURE__ */ jsx17("span", { "aria-hidden": "true", children: item.icon }),
1310
+ item.label
1311
+ ]
1312
+ },
1313
+ item.value
1314
+ ))
1315
+ }
1316
+ ), document.body)
1317
+ ] });
1318
+ }
1319
+
1320
+ // src/components/TableLayout/TableLayout.tsx
1321
+ import { useLayoutEffect as useLayoutEffect2, useRef as useRef6, useState as useState6 } from "react";
1322
+ import { jsx as jsx18 } from "react/jsx-runtime";
1323
+ var length = (value) => typeof value === "number" ? `${value}px` : value;
1324
+ function columnStyle(column) {
1325
+ return {
1326
+ width: column.width,
1327
+ "--table-column-width": !column.width || column.width.includes("%") ? void 0 : `calc(${column.width} - 1px)`,
1328
+ "--table-column-min": length(column.minWidth),
1329
+ "--table-column-max": length(column.maxWidth)
1330
+ };
1331
+ }
1332
+ function useOverflow() {
1333
+ const ref = useRef6(null);
1334
+ const [overflow, setOverflow] = useState6(false);
1335
+ useLayoutEffect2(() => {
1336
+ const el = ref.current;
1337
+ if (!el) return;
1338
+ const measure = () => setOverflow(el.scrollWidth > el.clientWidth + 1 || el.scrollHeight > el.clientHeight + 1);
1339
+ const resize = new ResizeObserver(measure);
1340
+ const observe = () => {
1341
+ resize.disconnect();
1342
+ resize.observe(el);
1343
+ for (const child of el.children) resize.observe(child);
1344
+ measure();
1345
+ };
1346
+ const mutation = new MutationObserver(observe);
1347
+ mutation.observe(el, { childList: true, subtree: true, characterData: true });
1348
+ observe();
1349
+ return () => {
1350
+ resize.disconnect();
1351
+ mutation.disconnect();
1352
+ };
1353
+ }, []);
1354
+ return { ref, overflow };
1355
+ }
1356
+ function TableViewport({ children, maxHeight, label }) {
1357
+ const { ref, overflow } = useOverflow();
1358
+ return /* @__PURE__ */ jsx18("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 });
1359
+ }
1360
+ function TableCellContent({ children, label }) {
1361
+ const { ref, overflow } = useOverflow();
1362
+ useLayoutEffect2(() => {
1363
+ const cell = ref.current;
1364
+ if (!cell) return;
1365
+ const wheel = (event) => {
1366
+ if (event.defaultPrevented || event.ctrlKey || !event.cancelable || Math.abs(event.deltaX) <= Math.abs(event.deltaY)) return;
1367
+ const range = cell.scrollWidth - cell.clientWidth;
1368
+ if (range <= 1) return;
1369
+ const rtl = getComputedStyle(cell).direction === "rtl";
1370
+ const left = rtl ? -range : 0, right = rtl ? 0 : range;
1371
+ if (event.deltaX > 0 ? cell.scrollLeft < right - 1 : cell.scrollLeft > left + 1) return;
1372
+ const table = cell.closest(".fractal-table-viewport");
1373
+ if (!table) return;
1374
+ const tableRange = table.scrollWidth - table.clientWidth;
1375
+ const tableRtl = getComputedStyle(table).direction === "rtl";
1376
+ const tableLeft = tableRtl ? -tableRange : 0, tableRight = tableRtl ? 0 : tableRange;
1377
+ if (event.deltaX > 0 ? table.scrollLeft >= tableRight - 1 : table.scrollLeft <= tableLeft + 1) return;
1378
+ const unit = event.deltaMode === 1 ? parseFloat(getComputedStyle(cell).lineHeight) || 16 : event.deltaMode === 2 ? table.clientWidth : 1;
1379
+ event.preventDefault();
1380
+ table.scrollLeft += event.deltaX * unit;
1381
+ };
1382
+ cell.addEventListener("wheel", wheel, { passive: false });
1383
+ return () => cell.removeEventListener("wheel", wheel);
1384
+ }, [ref]);
1385
+ return /* @__PURE__ */ jsx18(
1386
+ "div",
1387
+ {
1388
+ ref,
1389
+ className: "fractal-table-cell-scroll",
1390
+ "data-overflow": overflow || void 0,
1391
+ tabIndex: overflow ? 0 : void 0,
1392
+ role: overflow ? "region" : void 0,
1393
+ "aria-label": overflow ? `${label}, scroll horizontally for full content` : void 0,
1394
+ onKeyDown: (event) => {
1395
+ if (!overflow || event.target !== event.currentTarget) return;
1396
+ if (["ArrowLeft", "ArrowRight", "Home", "End"].includes(event.key)) {
1397
+ event.preventDefault();
1398
+ event.stopPropagation();
1399
+ const el = event.currentTarget;
1400
+ if (event.key === "ArrowLeft" || event.key === "ArrowRight") el.scrollBy({ left: event.key === "ArrowLeft" ? -40 : 40 });
1401
+ else el.scrollLeft = event.key === "Home" ? 0 : el.scrollWidth * (getComputedStyle(el).direction === "rtl" ? -1 : 1);
1402
+ } else if (event.key === " " || event.key === "Enter") event.stopPropagation();
1403
+ },
1404
+ children: /* @__PURE__ */ jsx18("div", { className: "fractal-table-cell-content", children })
1405
+ }
1406
+ );
1407
+ }
1408
+
465
1409
  // 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(
1410
+ import { jsx as jsx19, jsxs as jsxs16 } from "react/jsx-runtime";
1411
+ function DataTable({ columns, data, emptyMessage, header, size = "md", className, overflowMenu, layout = "fill", stickyHeader = false, stickyFirstColumn = false, maxHeight, "aria-label": ariaLabel = "Data table" }) {
1412
+ return /* @__PURE__ */ jsx19("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: [
1413
+ header && /* @__PURE__ */ jsx19("div", { className: "fractal-datatable-header", children: header }),
1414
+ data.length === 0 ? /* @__PURE__ */ jsx19("div", { className: "fractal-datatable-empty", children: emptyMessage || "No data" }) : /* @__PURE__ */ jsx19(TableViewport, { maxHeight, label: ariaLabel, children: /* @__PURE__ */ jsxs16("table", { "aria-label": ariaLabel, className: "fractal-datatable-table", children: [
1415
+ /* @__PURE__ */ jsx19("thead", { children: /* @__PURE__ */ jsxs16("tr", { className: "fractal-datatable-thead-row", children: [
1416
+ columns.map((col, columnIndex) => /* @__PURE__ */ jsx19("th", { scope: "col", className: "fractal-datatable-th", style: columnStyle(col), "data-pinned-column": columnIndex === 0 ? "data" : void 0, children: /* @__PURE__ */ jsx19(TableCellContent, { label: `${col.header} column`, children: col.header }) }, col.key)),
1417
+ overflowMenu && /* @__PURE__ */ jsx19("th", { scope: "col", "aria-label": "Row actions", className: "fractal-datatable-th fractal-table-overflow-cell", "data-pinned-column": "overflow" })
1418
+ ] }) }),
1419
+ /* @__PURE__ */ jsx19("tbody", { children: data.map((row, rowIndex) => /* @__PURE__ */ jsxs16(
473
1420
  "tr",
474
1421
  {
475
1422
  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))
1423
+ children: [
1424
+ columns.map((col, columnIndex) => /* @__PURE__ */ jsx19("td", { className: "fractal-datatable-td", style: columnStyle(col), "data-pinned-column": columnIndex === 0 ? "data" : void 0, children: /* @__PURE__ */ jsx19(TableCellContent, { label: `${col.header || "Value"}, row ${rowIndex + 1}`, children: col.render(row, rowIndex) }) }, col.key)),
1425
+ overflowMenu && /* @__PURE__ */ jsx19("td", { className: "fractal-datatable-td fractal-table-overflow-cell", "data-pinned-column": "overflow", children: /* @__PURE__ */ jsx19(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) }) })
1426
+ ]
477
1427
  },
478
1428
  rowIndex
479
1429
  )) })
480
- ] })
1430
+ ] }) })
481
1431
  ] }) });
482
1432
  }
483
1433
 
484
1434
  // 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;
1435
+ import * as React11 from "react";
1436
+ import { Close as Close2, Check as Check2, MagnifyingGlass as MagnifyingGlass2, ChevronDown, ChevronLeft, ChevronRight as ChevronRight2, ChevronUp } from "@mirror-physics/fractal-icons";
1437
+
1438
+ // src/components/Dropdown/position.ts
1439
+ function isDropdownTriggerVisible(trigger, bounds) {
1440
+ return trigger.right > bounds.left && trigger.left < bounds.right && trigger.bottom > bounds.top && trigger.top < bounds.bottom;
1441
+ }
1442
+ function positionDropdown(trigger, panel, bounds, direction, align) {
1443
+ const gap = 4;
1444
+ const width = Math.min(panel.width, Math.max(0, bounds.right - bounds.left));
1445
+ let maxHeight = Math.max(0, bounds.bottom - bounds.top);
1446
+ let left;
1447
+ let top;
1448
+ if (direction === "left" || direction === "right") {
1449
+ const after = bounds.right - trigger.right - gap;
1450
+ const before = trigger.left - bounds.left - gap;
1451
+ let placeRight = direction === "right";
1452
+ if (width > (placeRight ? after : before) && (placeRight ? before > after : after > before)) {
1453
+ placeRight = !placeRight;
1454
+ }
1455
+ left = placeRight ? trigger.right + gap : trigger.left - width - gap;
1456
+ top = trigger.top;
1457
+ } else {
1458
+ const below = Math.max(0, bounds.bottom - trigger.bottom - gap);
1459
+ const above = Math.max(0, trigger.top - bounds.top - gap);
1460
+ let placeAbove = direction === "up";
1461
+ if (panel.height > (placeAbove ? above : below) && (placeAbove ? below > above : above > below)) {
1462
+ placeAbove = !placeAbove;
1463
+ }
1464
+ maxHeight = Math.min(maxHeight, placeAbove ? above : below);
1465
+ top = placeAbove ? trigger.top - Math.min(panel.height, maxHeight) - gap : trigger.bottom + gap;
1466
+ left = align === "right" ? trigger.right - width : trigger.left;
1467
+ }
1468
+ const height = Math.min(panel.height, maxHeight);
1469
+ return {
1470
+ left: Math.max(bounds.left, Math.min(left, bounds.right - width)),
1471
+ top: Math.max(bounds.top, Math.min(top, bounds.bottom - height)),
1472
+ maxHeight
1473
+ };
1474
+ }
1475
+
1476
+ // src/components/Dropdown/Dropdown.tsx
1477
+ import { Fragment as Fragment2, jsx as jsx20, jsxs as jsxs17 } from "react/jsx-runtime";
489
1478
  function Dropdown({
490
1479
  items,
491
- selectedValue,
1480
+ selectedValue = "",
492
1481
  onSelect,
493
- triggerLabel = "Options",
1482
+ multiple = false,
1483
+ selectedValues = [],
1484
+ onSelectionChange,
1485
+ searchable = false,
1486
+ searchPlaceholder = "Find an option",
1487
+ emptyMessage = "No matching options.",
1488
+ triggerLabel,
1489
+ fieldLabel,
1490
+ clearable = Boolean(fieldLabel),
494
1491
  direction = "down",
495
1492
  align = "left",
1493
+ maxHeight = 320,
1494
+ collisionPadding = 8,
1495
+ triggerOrientation = "horizontal",
496
1496
  triggerContent,
497
1497
  label,
1498
+ iconOnly = false,
498
1499
  noChevron = false,
499
- size = "md",
1500
+ size: sizeAlias = "lg",
1501
+ controlSize,
1502
+ triggerId,
500
1503
  triggerVariant = "outline",
501
1504
  className,
502
1505
  triggerClassName,
503
1506
  panelClassName,
504
- closeOnSelect = true
1507
+ closeOnSelect = true,
1508
+ disabled = false
505
1509
  }) {
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(
1510
+ const [query, setQuery] = React11.useState("");
1511
+ const searchRef = React11.useRef(null);
1512
+ const searchContainerRef = React11.useRef(null);
1513
+ const optionsRef = React11.useRef(null);
1514
+ const [optionsScrolled, setOptionsScrolled] = React11.useState(false);
1515
+ const accessibleLabel = triggerLabel ?? fieldLabel ?? "Options";
1516
+ const size = controlSize ?? sizeAlias;
1517
+ const canClear = clearable && !disabled && (multiple ? selectedValues.length > 0 && Boolean(onSelectionChange) : Boolean(selectedValue) && Boolean(onSelect));
1518
+ const [open, setOpen] = React11.useState(false);
1519
+ React11.useEffect(() => {
1520
+ if (!open) setQuery("");
1521
+ }, [open]);
1522
+ const rootRef = React11.useRef(null);
1523
+ const menuId = React11.useId();
1524
+ const focusOnOpen = React11.useRef(false);
1525
+ const triggerRef = React11.useRef(null);
1526
+ const panelRef = React11.useRef(null);
1527
+ const [panelStyle, setPanelStyle] = React11.useState({});
1528
+ const renderableItems = items.filter(
511
1529
  (item) => item.icon != null || item.description != null && (typeof item.description === "string" ? item.description !== "" : true)
512
1530
  );
513
- if (visibleItems.length === 0) return null;
514
- const selectableItems = visibleItems.filter((i) => i.kind !== "header");
515
- 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;
1531
+ const normalizedQuery = query.trim().toLocaleLowerCase();
1532
+ let groupTitle = "";
1533
+ const matching = new Set(renderableItems.filter((item) => {
1534
+ if (item.kind === "header") {
1535
+ groupTitle = String(item.description ?? "");
1536
+ return false;
531
1537
  }
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;
1538
+ return `${groupTitle} ${typeof item.description === "string" ? item.description : item.value}`.toLocaleLowerCase().includes(normalizedQuery);
1539
+ }).map((item) => item.value));
1540
+ const visibleItems = renderableItems.filter((item, index) => {
1541
+ if (item.kind !== "header") return matching.has(item.value);
1542
+ const nextHeader = renderableItems.findIndex((next, i) => i > index && next.kind === "header");
1543
+ return renderableItems.slice(index + 1, nextHeader < 0 ? void 0 : nextHeader).some((next) => matching.has(next.value));
1544
+ });
1545
+ const selectableItems = visibleItems.filter((i) => i.kind !== "header");
1546
+ const selected = items.find((i) => i.value === selectedValue && i.kind !== "header") ?? (fieldLabel && !selectedValue && !multiple ? void 0 : selectableItems[0] ?? visibleItems[0]);
1547
+ const ChevronIcon = direction === "up" ? ChevronUp : direction === "left" ? ChevronLeft : direction === "right" ? ChevronRight2 : ChevronDown;
1548
+ useEscapeDismiss(100, () => {
1549
+ setOpen(false);
1550
+ triggerRef.current?.focus();
1551
+ }, open);
1552
+ React11.useLayoutEffect(() => {
1553
+ if (!open) return;
1554
+ if (optionsRef.current) optionsRef.current.scrollTop = 0;
1555
+ setOptionsScrolled(false);
1556
+ }, [open, query]);
1557
+ React11.useLayoutEffect(() => {
1558
+ const root = rootRef.current;
1559
+ const trigger = triggerRef.current;
1560
+ const panel = panelRef.current;
1561
+ if (!open || !root || !trigger || !panel) return;
1562
+ const updatePosition = () => {
1563
+ const viewport = window.visualViewport;
1564
+ const bounds = {
1565
+ left: viewport?.offsetLeft ?? 0,
1566
+ top: viewport?.offsetTop ?? 0,
1567
+ right: (viewport?.offsetLeft ?? 0) + (viewport?.width ?? document.documentElement.clientWidth),
1568
+ bottom: (viewport?.offsetTop ?? 0) + (viewport?.height ?? document.documentElement.clientHeight)
1569
+ };
1570
+ for (let ancestor = root.parentElement; ancestor; ancestor = ancestor.parentElement) {
1571
+ const style = getComputedStyle(ancestor);
1572
+ const rect = ancestor.getBoundingClientRect();
1573
+ if (/(auto|scroll|hidden|clip)/.test(style.overflowX)) {
1574
+ bounds.left = Math.max(bounds.left, rect.left + ancestor.clientLeft);
1575
+ bounds.right = Math.min(bounds.right, rect.left + ancestor.clientLeft + ancestor.clientWidth);
1576
+ }
1577
+ if (/(auto|scroll|hidden|clip)/.test(style.overflowY)) {
1578
+ bounds.top = Math.max(bounds.top, rect.top + ancestor.clientTop);
1579
+ bounds.bottom = Math.min(bounds.bottom, rect.top + ancestor.clientTop + ancestor.clientHeight);
1580
+ }
541
1581
  }
542
- } else {
543
- left = 0;
544
- const panelRight = triggerRect.left + panelRect.width;
545
- if (panelRight > vw) {
546
- left = void 0;
547
- right = 0;
1582
+ const triggerRect = trigger.getBoundingClientRect();
1583
+ if (!isDropdownTriggerVisible(triggerRect, bounds)) {
1584
+ focusOnOpen.current = false;
1585
+ setOpen(false);
1586
+ return false;
1587
+ }
1588
+ const padding = Math.max(0, collisionPadding);
1589
+ bounds.left += padding;
1590
+ bounds.right = Math.max(bounds.left, bounds.right - padding);
1591
+ bounds.top += padding;
1592
+ bounds.bottom = Math.max(bounds.top, bounds.bottom - padding);
1593
+ const rootRect = root.getBoundingClientRect();
1594
+ const availableWidth = bounds.right - bounds.left;
1595
+ const heightLimit = Math.max(0, maxHeight);
1596
+ panel.style.maxWidth = `${availableWidth}px`;
1597
+ panel.style.minWidth = `${Math.min(triggerRect.width, availableWidth)}px`;
1598
+ const panelRect = panel.getBoundingClientRect();
1599
+ const desiredHeight = Math.min(heightLimit, (optionsRef.current?.scrollHeight ?? 0) + (searchContainerRef.current?.offsetHeight ?? 0) + parseFloat(getComputedStyle(panel).paddingTop) + parseFloat(getComputedStyle(panel).paddingBottom) + panel.offsetHeight - panel.clientHeight);
1600
+ const position = positionDropdown(triggerRect, { width: panelRect.width, height: desiredHeight }, bounds, direction, align);
1601
+ const nextStyle = {
1602
+ left: position.left - rootRect.left - root.clientLeft + root.scrollLeft,
1603
+ top: position.top - rootRect.top - root.clientTop + root.scrollTop,
1604
+ minWidth: Math.min(triggerRect.width, availableWidth),
1605
+ maxWidth: availableWidth,
1606
+ maxHeight: Math.min(heightLimit, position.maxHeight)
1607
+ };
1608
+ for (const key of Object.keys(nextStyle)) {
1609
+ panel.style[key] = `${nextStyle[key]}px`;
548
1610
  }
1611
+ setPanelStyle((previous) => Object.entries(nextStyle).every(([key, value]) => previous[key] === value) ? previous : nextStyle);
1612
+ return true;
1613
+ };
1614
+ if (!updatePosition()) return;
1615
+ if (searchable && !panel.contains(document.activeElement)) searchRef.current?.focus({ preventScroll: true });
1616
+ if (focusOnOpen.current) {
1617
+ focusOnOpen.current = false;
1618
+ const option = panel.querySelector('[aria-selected="true"]') ?? panel.querySelector('[role="option"]');
1619
+ option?.focus({ preventScroll: true });
1620
+ option?.scrollIntoView({ block: "nearest" });
1621
+ }
1622
+ const resize = new ResizeObserver(updatePosition);
1623
+ resize.observe(root);
1624
+ resize.observe(panel);
1625
+ if (optionsRef.current) resize.observe(optionsRef.current);
1626
+ for (let ancestor = root.parentElement; ancestor; ancestor = ancestor.parentElement) {
1627
+ resize.observe(ancestor);
549
1628
  }
550
- setPanelStyle({ top, left, right });
551
- }, [open, isUp, align]);
552
- React7.useEffect(() => {
1629
+ window.addEventListener("resize", updatePosition);
1630
+ window.addEventListener("scroll", updatePosition, true);
1631
+ window.visualViewport?.addEventListener("resize", updatePosition);
1632
+ window.visualViewport?.addEventListener("scroll", updatePosition);
1633
+ return () => {
1634
+ resize.disconnect();
1635
+ window.removeEventListener("resize", updatePosition);
1636
+ window.removeEventListener("scroll", updatePosition, true);
1637
+ window.visualViewport?.removeEventListener("resize", updatePosition);
1638
+ window.visualViewport?.removeEventListener("scroll", updatePosition);
1639
+ };
1640
+ }, [open, direction, align, maxHeight, collisionPadding, items, label, query, searchable]);
1641
+ React11.useEffect(() => {
553
1642
  if (!open) return;
554
1643
  const handleClickOutside = (e) => {
555
1644
  const target = e.target;
@@ -559,75 +1648,128 @@ function Dropdown({
559
1648
  document.addEventListener("mousedown", handleClickOutside);
560
1649
  return () => document.removeEventListener("mousedown", handleClickOutside);
561
1650
  }, [open]);
562
- return /* @__PURE__ */ jsxs8("div", { className: cn("fractal-dropdown", className), children: [
563
- /* @__PURE__ */ jsxs8(
1651
+ if (renderableItems.length === 0) return null;
1652
+ return /* @__PURE__ */ jsxs17("div", { ref: rootRef, "data-control-size": size, "data-filled": multiple ? selectedValues.length > 0 : Boolean(selectedValue), "data-open": open, "data-clearable": canClear || void 0, className: cn("fractal-dropdown", fieldLabel && "fractal-floating-field fractal-floating-field--dropdown", className), children: [
1653
+ /* @__PURE__ */ jsxs17(
564
1654
  "button",
565
1655
  {
566
1656
  ref: triggerRef,
1657
+ id: triggerId ?? `${menuId}-trigger`,
1658
+ "data-control-size": size,
567
1659
  type: "button",
568
- "aria-haspopup": "listbox",
1660
+ disabled,
1661
+ "aria-haspopup": searchable ? "dialog" : "listbox",
569
1662
  "aria-expanded": open,
570
- "aria-label": triggerLabel,
1663
+ "aria-controls": open ? searchable ? menuId : `${menuId}-options` : void 0,
1664
+ onKeyDown: (event) => {
1665
+ if (event.key === "ArrowDown" || event.key === "ArrowUp") {
1666
+ event.preventDefault();
1667
+ if (open) {
1668
+ const option = panelRef.current?.querySelector('[aria-selected="true"]') ?? panelRef.current?.querySelector('[role="option"]');
1669
+ option?.focus({ preventScroll: true });
1670
+ option?.scrollIntoView({ block: "nearest" });
1671
+ } else {
1672
+ focusOnOpen.current = true;
1673
+ setOpen(true);
1674
+ }
1675
+ }
1676
+ },
1677
+ "aria-label": accessibleLabel,
571
1678
  onClick: () => setOpen((o) => !o),
572
1679
  className: cn(
1680
+ "fractal-control",
573
1681
  "fractal-dropdown-trigger",
574
1682
  `fractal-dropdown-trigger--${size}`,
575
1683
  `fractal-dropdown-trigger--${triggerVariant}`,
1684
+ `fractal-dropdown-trigger--${triggerOrientation}`,
1685
+ iconOnly && "fractal-dropdown-trigger--icon-only",
576
1686
  triggerClassName
577
1687
  ),
578
1688
  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 }) })
1689
+ /* @__PURE__ */ jsx20("span", { className: fieldLabel ? "fractal-dropdown-field-value" : void 0, style: fieldLabel ? void 0 : { display: "contents" }, children: triggerContent != null ? triggerContent : /* @__PURE__ */ jsxs17(Fragment2, { children: [
1690
+ selected?.icon != null && /* @__PURE__ */ jsx20("span", { className: "fractal-dropdown-icon fractal-dropdown-icon--trigger", children: selected?.icon }),
1691
+ selected?.description != null && (typeof selected?.description !== "string" || selected?.description !== "") && /* @__PURE__ */ jsx20("span", { className: "fractal-dropdown-selected-text", children: selected?.description })
1692
+ ] }) }),
1693
+ !noChevron && !iconOnly && /* @__PURE__ */ jsx20("span", { className: "fractal-dropdown-chevron", "aria-hidden": true, children: /* @__PURE__ */ jsx20(ChevronIcon, { size: size === "sm" ? 14 : 16 }) })
584
1694
  ]
585
1695
  }
586
1696
  ),
587
- open && /* @__PURE__ */ jsxs8(
1697
+ canClear && /* @__PURE__ */ jsx20("button", { type: "button", className: "fractal-dropdown-clear", "aria-label": `Clear ${accessibleLabel}`, onClick: () => {
1698
+ if (multiple) onSelectionChange?.([]);
1699
+ else onSelect?.("");
1700
+ setOpen(false);
1701
+ setQuery("");
1702
+ triggerRef.current?.focus();
1703
+ }, children: /* @__PURE__ */ jsx20(Close2, { "aria-hidden": "true" }) }),
1704
+ fieldLabel ? /* @__PURE__ */ jsx20(FloatingFieldBorder, { label: fieldLabel }) : null,
1705
+ fieldLabel ? /* @__PURE__ */ jsx20("label", { className: "fractal-floating-field__label", htmlFor: triggerId ?? `${menuId}-trigger`, children: fieldLabel }) : null,
1706
+ open && /* @__PURE__ */ jsxs17(
588
1707
  "div",
589
1708
  {
590
1709
  ref: panelRef,
591
- role: "listbox",
592
- "aria-label": triggerLabel,
593
- className: cn("fractal-dropdown-panel", `fractal-dropdown-panel--${size}`, panelClassName),
594
- style: panelStyle,
595
- 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: [
602
- topDivider,
603
- /* @__PURE__ */ jsx9("div", { role: "presentation", className: "fractal-dropdown-label", children: item.description }),
604
- 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
- })
1710
+ id: menuId,
1711
+ onKeyDown: (event) => {
1712
+ if (event.target === searchRef.current && event.key !== "ArrowDown") return;
1713
+ if (!["ArrowDown", "ArrowUp", "Home", "End"].includes(event.key)) return;
1714
+ event.preventDefault();
1715
+ const options = Array.from(panelRef.current?.querySelectorAll('[role="option"]') ?? []);
1716
+ const current = options.indexOf(document.activeElement);
1717
+ const index = event.key === "Home" ? 0 : event.key === "End" ? options.length - 1 : (current + (event.key === "ArrowDown" ? 1 : -1) + options.length) % options.length;
1718
+ options[index]?.focus({ preventScroll: true });
1719
+ options[index]?.scrollIntoView({ block: "nearest" });
1720
+ },
1721
+ role: searchable ? "dialog" : void 0,
1722
+ "aria-label": accessibleLabel,
1723
+ className: cn("fractal-dropdown-panel", `fractal-dropdown-panel--${size}`, panelClassName),
1724
+ style: panelStyle,
1725
+ children: [
1726
+ searchable && /* @__PURE__ */ jsx20("div", { ref: searchContainerRef, className: "fractal-dropdown-search-container", "data-scrolled": optionsScrolled, children: /* @__PURE__ */ jsxs17("div", { className: "fractal-dropdown-search", children: [
1727
+ /* @__PURE__ */ jsx20(MagnifyingGlass2, { "aria-hidden": "true" }),
1728
+ /* @__PURE__ */ jsx20("input", { ref: searchRef, type: "search", "aria-label": searchPlaceholder, placeholder: searchPlaceholder, value: query, onChange: (event) => setQuery(event.target.value) })
1729
+ ] }) }),
1730
+ /* @__PURE__ */ jsxs17("div", { ref: optionsRef, className: "fractal-dropdown-options", onScroll: (event) => setOptionsScrolled(event.currentTarget.scrollTop > 0), role: "listbox", id: `${menuId}-options`, "aria-label": accessibleLabel, "aria-multiselectable": multiple || void 0, children: [
1731
+ label != null && /* @__PURE__ */ jsx20("div", { className: "fractal-dropdown-label", children: label }),
1732
+ visibleItems.map((item, index) => {
1733
+ const topDivider = item.border === "top" ? /* @__PURE__ */ jsx20("div", { className: "fractal-dropdown-separator" }) : null;
1734
+ const bottomDivider = item.border === "bottom" ? /* @__PURE__ */ jsx20("div", { className: "fractal-dropdown-separator" }) : null;
1735
+ if (item.kind === "header") {
1736
+ return /* @__PURE__ */ jsxs17(React11.Fragment, { children: [
1737
+ topDivider,
1738
+ /* @__PURE__ */ jsx20("div", { role: "presentation", className: "fractal-dropdown-label", children: item.description }),
1739
+ bottomDivider
1740
+ ] }, `header-${index}`);
1741
+ }
1742
+ const isSelected = multiple ? selectedValues.includes(item.value) : item.value === selectedValue;
1743
+ return /* @__PURE__ */ jsxs17(React11.Fragment, { children: [
1744
+ topDivider,
1745
+ /* @__PURE__ */ jsxs17(
1746
+ "button",
1747
+ {
1748
+ type: "button",
1749
+ role: "option",
1750
+ "aria-selected": isSelected,
1751
+ onClick: () => {
1752
+ if (multiple) onSelectionChange?.(isSelected ? selectedValues.filter((value) => value !== item.value) : [...selectedValues, item.value]);
1753
+ else onSelect?.(item.value);
1754
+ if (!multiple && closeOnSelect) {
1755
+ setOpen(false);
1756
+ triggerRef.current?.focus();
1757
+ }
1758
+ },
1759
+ className: "fractal-dropdown-option",
1760
+ children: [
1761
+ multiple && /* @__PURE__ */ jsx20("span", { className: "fractal-dropdown-checkbox", "data-checked": isSelected, "aria-hidden": "true", children: isSelected && /* @__PURE__ */ jsx20(Check2, {}) }),
1762
+ item.icon != null && /* @__PURE__ */ jsx20("span", { className: "fractal-dropdown-icon", children: item.icon }),
1763
+ item.description != null && (typeof item.description !== "string" || item.description !== "") && /* @__PURE__ */ jsx20("span", { className: "fractal-dropdown-option-text", children: item.description }),
1764
+ item.trailing != null && /* @__PURE__ */ jsx20("span", { className: "fractal-dropdown-trailing", children: item.trailing })
1765
+ ]
1766
+ }
1767
+ ),
1768
+ bottomDivider
1769
+ ] }, item.value);
1770
+ }),
1771
+ !selectableItems.length && /* @__PURE__ */ jsx20("p", { role: "status", className: "fractal-dropdown-label", children: emptyMessage })
1772
+ ] })
631
1773
  ]
632
1774
  }
633
1775
  )
@@ -635,10 +1777,10 @@ function Dropdown({
635
1777
  }
636
1778
 
637
1779
  // 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(
1780
+ import * as React12 from "react";
1781
+ import { jsx as jsx21 } from "react/jsx-runtime";
1782
+ var Link = React12.forwardRef(
1783
+ ({ className, theme = "default", size = "md", ...props }, ref) => /* @__PURE__ */ jsx21(
642
1784
  "a",
643
1785
  {
644
1786
  ref,
@@ -650,10 +1792,10 @@ var Link = React8.forwardRef(
650
1792
  Link.displayName = "Link";
651
1793
 
652
1794
  // 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(
1795
+ import * as React13 from "react";
1796
+ import { jsx as jsx22 } from "react/jsx-runtime";
1797
+ var Chip = React13.forwardRef(
1798
+ ({ className, tone = "neutral", ...props }, ref) => /* @__PURE__ */ jsx22(
657
1799
  "span",
658
1800
  {
659
1801
  ref,
@@ -664,36 +1806,128 @@ var Chip = React9.forwardRef(
664
1806
  );
665
1807
  Chip.displayName = "Chip";
666
1808
 
1809
+ // src/components/Checkbox/Checkbox.tsx
1810
+ import { Checkbox as CheckboxIcon, CheckboxChecked, Minus as Minus2 } from "@mirror-physics/fractal-icons";
1811
+ import { jsx as jsx23, jsxs as jsxs18 } from "react/jsx-runtime";
1812
+ function Checkbox({
1813
+ checked,
1814
+ indeterminate = false,
1815
+ onCheckedChange,
1816
+ className,
1817
+ disabled,
1818
+ onClick,
1819
+ onKeyDown,
1820
+ ...props
1821
+ }) {
1822
+ return /* @__PURE__ */ jsx23(
1823
+ "button",
1824
+ {
1825
+ ...props,
1826
+ className: cn(
1827
+ "fractal-checkbox",
1828
+ (checked || indeterminate) && "fractal-checkbox--checked",
1829
+ indeterminate && "fractal-checkbox--indeterminate",
1830
+ className
1831
+ ),
1832
+ type: "button",
1833
+ role: "checkbox",
1834
+ "aria-checked": indeterminate ? "mixed" : checked,
1835
+ disabled,
1836
+ onClick: (event) => {
1837
+ onClick?.(event);
1838
+ if (!event.defaultPrevented) onCheckedChange(indeterminate || !checked);
1839
+ },
1840
+ onKeyDown: (event) => {
1841
+ onKeyDown?.(event);
1842
+ if (event.key === " " || event.key === "Enter") event.stopPropagation();
1843
+ },
1844
+ children: indeterminate ? /* @__PURE__ */ jsxs18("span", { className: "fractal-checkbox__indeterminate-icon", "aria-hidden": "true", children: [
1845
+ /* @__PURE__ */ jsx23(CheckboxIcon, { size: 18 }),
1846
+ /* @__PURE__ */ jsx23(Minus2, { className: "fractal-checkbox__indeterminate-mark", size: 12 })
1847
+ ] }) : checked ? /* @__PURE__ */ jsx23(CheckboxChecked, { "aria-hidden": "true", size: 18 }) : /* @__PURE__ */ jsx23(CheckboxIcon, { "aria-hidden": "true", size: 18 })
1848
+ }
1849
+ );
1850
+ }
1851
+
1852
+ // src/components/Radio/Radio.tsx
1853
+ import { jsx as jsx24, jsxs as jsxs19 } from "react/jsx-runtime";
1854
+ function Radio({
1855
+ checked,
1856
+ className,
1857
+ description,
1858
+ disabled,
1859
+ label,
1860
+ onCheckedChange,
1861
+ ...props
1862
+ }) {
1863
+ return /* @__PURE__ */ jsxs19("label", { className: cn("fractal-radio", disabled && "fractal-radio--disabled", className), children: [
1864
+ /* @__PURE__ */ jsx24(
1865
+ "input",
1866
+ {
1867
+ ...props,
1868
+ checked,
1869
+ className: "fractal-radio__input",
1870
+ disabled,
1871
+ onChange: (event) => onCheckedChange?.(event.currentTarget.checked),
1872
+ type: "radio"
1873
+ }
1874
+ ),
1875
+ /* @__PURE__ */ jsx24("span", { "aria-hidden": "true", className: "fractal-radio__control", children: /* @__PURE__ */ jsx24("span", { className: "fractal-radio__dot" }) }),
1876
+ /* @__PURE__ */ jsxs19("span", { className: "fractal-radio__content", children: [
1877
+ /* @__PURE__ */ jsx24("span", { className: "fractal-radio__label", children: label }),
1878
+ description && /* @__PURE__ */ jsx24("span", { className: "fractal-radio__description", children: description })
1879
+ ] })
1880
+ ] });
1881
+ }
1882
+
667
1883
  // src/components/SortableTable/SortableTable.tsx
668
- import { useMemo, useState as useState3 } from "react";
1884
+ import { useMemo as useMemo2, useState as useState8 } from "react";
669
1885
  import { ArrowUp, ArrowDown, ArrowsUpDown } from "@mirror-physics/fractal-icons";
670
- import { jsx as jsx12, jsxs as jsxs9 } from "react/jsx-runtime";
1886
+ import { jsx as jsx25, jsxs as jsxs20 } from "react/jsx-runtime";
671
1887
  function SortableTable({
672
1888
  columns,
673
1889
  data,
1890
+ "aria-label": ariaLabel,
674
1891
  emptyMessage,
675
1892
  header,
676
1893
  size = "md",
1894
+ embedded = false,
1895
+ layout = "fill",
1896
+ stickyHeader = false,
1897
+ stickyFirstColumn = false,
1898
+ maxHeight,
1899
+ overflowMenu,
677
1900
  className,
1901
+ defaultSort,
678
1902
  defaultSortKey = void 0,
679
1903
  defaultSortDirection = null,
680
1904
  sortKey: controlledKey,
681
1905
  sortDirection: controlledDirection,
682
1906
  onSortChange,
1907
+ pagination,
683
1908
  highlightRow,
684
- onRowClick
1909
+ selection,
1910
+ isRowDisabled,
1911
+ isRowSelectionDisabled,
1912
+ onRowClick,
1913
+ onRowDoubleClick,
1914
+ rowAction
685
1915
  }) {
686
- const [internalKey, setInternalKey] = useState3(defaultSortKey ?? null);
687
- const [internalDirection, setInternalDirection] = useState3(defaultSortDirection);
1916
+ const [internalKey, setInternalKey] = useState8(defaultSortKey ?? null);
1917
+ const [internalDirection, setInternalDirection] = useState8(defaultSortDirection);
688
1918
  const isControlled = controlledKey !== void 0 && controlledDirection !== void 0;
689
1919
  const activeKey = isControlled ? controlledKey : internalKey;
690
1920
  const activeDirection = isControlled ? controlledDirection : internalDirection;
691
- const sorted = useMemo(() => {
692
- if (!activeKey || !activeDirection) return data;
693
- const col = columns.find((c) => c.key === activeKey);
1921
+ const baselineKey = defaultSort?.key ?? null;
1922
+ const baselineDirection = defaultSort?.direction ?? null;
1923
+ const effectiveKey = activeKey && activeDirection ? activeKey : baselineKey;
1924
+ const effectiveDirection = activeKey && activeDirection ? activeDirection : baselineDirection;
1925
+ const sorted = useMemo2(() => {
1926
+ if (!effectiveKey || !effectiveDirection) return data;
1927
+ const col = columns.find((c) => c.key === effectiveKey);
694
1928
  if (!col) return data;
695
1929
  const accessor = col.sortAccessor ?? ((row) => {
696
- const value = row[activeKey];
1930
+ const value = row[effectiveKey];
697
1931
  return typeof value === "string" || typeof value === "number" ? value : null;
698
1932
  });
699
1933
  const copy = [...data];
@@ -704,26 +1938,41 @@ function SortableTable({
704
1938
  if (av == null) return 1;
705
1939
  if (bv == null) return -1;
706
1940
  if (typeof av === "number" && typeof bv === "number") {
707
- return activeDirection === "asc" ? av - bv : bv - av;
1941
+ return effectiveDirection === "asc" ? av - bv : bv - av;
708
1942
  }
709
1943
  const as = String(av).toLowerCase();
710
1944
  const bs = String(bv).toLowerCase();
711
- if (as < bs) return activeDirection === "asc" ? -1 : 1;
712
- if (as > bs) return activeDirection === "asc" ? 1 : -1;
1945
+ if (as < bs) return effectiveDirection === "asc" ? -1 : 1;
1946
+ if (as > bs) return effectiveDirection === "asc" ? 1 : -1;
713
1947
  return 0;
714
1948
  });
715
1949
  return copy;
716
- }, [data, columns, activeKey, activeDirection]);
1950
+ }, [data, columns, effectiveKey, effectiveDirection]);
1951
+ const visibleRows = useMemo2(() => {
1952
+ if (!pagination) return sorted;
1953
+ const pageSize = Math.max(1, pagination.pageSize);
1954
+ const totalPages = Math.max(1, Math.ceil(sorted.length / pageSize));
1955
+ const page = Math.min(Math.max(1, pagination.page), totalPages);
1956
+ const startIndex = (page - 1) * pageSize;
1957
+ return sorted.slice(startIndex, startIndex + pageSize);
1958
+ }, [pagination, sorted]);
1959
+ const selectedRows = selection ? sorted.filter(selection.isSelected) : [];
1960
+ const selectableRows = selection ? sorted.filter((row) => !isRowDisabled?.(row) && !isRowSelectionDisabled?.(row)) : [];
1961
+ const selectedRowCount = selection ? selectableRows.filter(selection.isSelected).length : 0;
1962
+ const allRowsSelected = selectableRows.length > 0 && selectedRowCount === selectableRows.length;
1963
+ const someRowsSelected = selectedRows.length > 0 && !allRowsSelected;
717
1964
  const handleSortClick = (key) => {
718
1965
  let nextDirection;
719
- if (activeKey !== key) {
1966
+ if (!activeKey || !activeDirection) {
1967
+ nextDirection = baselineKey === key && baselineDirection ? baselineDirection : "asc";
1968
+ } else if (activeKey !== key) {
720
1969
  nextDirection = "asc";
1970
+ } else if (baselineKey === key && baselineDirection) {
1971
+ nextDirection = activeDirection === baselineDirection ? baselineDirection === "asc" ? "desc" : "asc" : null;
721
1972
  } else if (activeDirection === "asc") {
722
1973
  nextDirection = "desc";
723
- } else if (activeDirection === "desc") {
724
- nextDirection = null;
725
1974
  } else {
726
- nextDirection = "asc";
1975
+ nextDirection = null;
727
1976
  }
728
1977
  const nextKey = nextDirection === null ? null : key;
729
1978
  if (isControlled) {
@@ -734,101 +1983,200 @@ function SortableTable({
734
1983
  onSortChange?.(nextKey, nextDirection);
735
1984
  }
736
1985
  };
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(
1986
+ return /* @__PURE__ */ jsx25("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: [
1987
+ header && /* @__PURE__ */ jsx25("div", { className: "fractal-sortable-table-header", children: header }),
1988
+ visibleRows.length === 0 ? /* @__PURE__ */ jsx25("div", { className: "fractal-sortable-table-empty", children: emptyMessage || "No data" }) : /* @__PURE__ */ jsx25(TableViewport, { maxHeight, label: ariaLabel || "Data table", children: /* @__PURE__ */ jsxs20("table", { "aria-label": ariaLabel, className: "fractal-sortable-table-table", children: [
1989
+ /* @__PURE__ */ jsx25("thead", { children: /* @__PURE__ */ jsxs20("tr", { className: "fractal-sortable-table-thead-row", children: [
1990
+ selection && /* @__PURE__ */ jsx25(
754
1991
  "th",
755
1992
  {
756
- className: "fractal-sortable-table-th fractal-sortable-table-th--sortable",
757
- style: { width: col.width },
758
- children: /* @__PURE__ */ jsxs9(
759
- "button",
1993
+ className: "fractal-sortable-table-th fractal-sortable-table-selection-cell",
1994
+ "data-pinned-column": "selection",
1995
+ scope: "col",
1996
+ children: /* @__PURE__ */ jsx25(
1997
+ Checkbox,
760
1998
  {
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
- ]
1999
+ "aria-label": selection.selectAllLabel ?? "Select all rows",
2000
+ checked: allRowsSelected,
2001
+ disabled: selectableRows.length === 0 && selectedRows.length === 0,
2002
+ indeterminate: someRowsSelected,
2003
+ onCheckedChange: (checked) => {
2004
+ const rows = checked ? selectableRows : selectedRows;
2005
+ if (selection.onSelectAllChange) {
2006
+ selection.onSelectAllChange(rows, checked);
2007
+ return;
2008
+ }
2009
+ rows.forEach((row) => {
2010
+ if (selection.isSelected(row) !== checked) {
2011
+ selection.onSelectedChange(row, checked);
2012
+ }
2013
+ });
2014
+ }
772
2015
  }
773
2016
  )
774
- },
775
- col.key
776
- );
777
- }) }) }),
778
- /* @__PURE__ */ jsx12("tbody", { children: sorted.map((row, rowIndex) => {
2017
+ }
2018
+ ),
2019
+ columns.map((col, columnIndex) => {
2020
+ const isActive = activeKey === col.key && activeDirection !== null;
2021
+ if (!col.sortable) {
2022
+ return /* @__PURE__ */ jsx25(
2023
+ "th",
2024
+ {
2025
+ className: "fractal-sortable-table-th",
2026
+ scope: "col",
2027
+ style: columnStyle(col),
2028
+ "data-pinned-column": columnIndex === 0 ? "data" : void 0,
2029
+ children: /* @__PURE__ */ jsx25(TableCellContent, { label: `${col.header} column`, children: col.header })
2030
+ },
2031
+ col.key
2032
+ );
2033
+ }
2034
+ return /* @__PURE__ */ jsx25(
2035
+ "th",
2036
+ {
2037
+ className: "fractal-sortable-table-th fractal-sortable-table-th--sortable",
2038
+ scope: "col",
2039
+ style: columnStyle(col),
2040
+ "data-pinned-column": columnIndex === 0 ? "data" : void 0,
2041
+ children: /* @__PURE__ */ jsx25(TableCellContent, { label: `${col.header} column`, children: /* @__PURE__ */ jsxs20(
2042
+ "button",
2043
+ {
2044
+ type: "button",
2045
+ className: cn(
2046
+ "fractal-sortable-table-sort-button",
2047
+ isActive && "fractal-sortable-table-sort-button--active"
2048
+ ),
2049
+ onClick: () => handleSortClick(col.key),
2050
+ "aria-sort": isActive ? activeDirection === "asc" ? "ascending" : "descending" : "none",
2051
+ children: [
2052
+ /* @__PURE__ */ jsx25("span", { children: col.header }),
2053
+ /* @__PURE__ */ jsx25("span", { className: "fractal-sortable-table-sort-icon", "aria-hidden": "true", children: isActive && activeDirection === "asc" ? /* @__PURE__ */ jsx25(ArrowUp, { size: 14 }) : isActive && activeDirection === "desc" ? /* @__PURE__ */ jsx25(ArrowDown, { size: 14 }) : /* @__PURE__ */ jsx25(ArrowsUpDown, { size: 14 }) })
2054
+ ]
2055
+ }
2056
+ ) })
2057
+ },
2058
+ col.key
2059
+ );
2060
+ }),
2061
+ overflowMenu && /* @__PURE__ */ jsx25("th", { scope: "col", "aria-label": "Row actions", className: "fractal-sortable-table-th fractal-table-overflow-cell", "data-pinned-column": "overflow" })
2062
+ ] }) }),
2063
+ /* @__PURE__ */ jsx25("tbody", { children: visibleRows.map((row, rowIndex) => {
779
2064
  const highlighted = highlightRow?.(row, rowIndex) ?? false;
780
- return /* @__PURE__ */ jsx12(
2065
+ const disabled = isRowDisabled?.(row) ?? false;
2066
+ const selectionDisabled = disabled || (isRowSelectionDisabled?.(row) ?? false);
2067
+ const interactive = !disabled && Boolean(onRowClick || onRowDoubleClick);
2068
+ const action = !disabled && rowAction ? {
2069
+ label: rowAction.label(row, rowIndex),
2070
+ icon: rowAction.icon(row, rowIndex)
2071
+ } : null;
2072
+ return /* @__PURE__ */ jsxs20(
781
2073
  "tr",
782
2074
  {
783
2075
  className: cn(
784
2076
  "fractal-sortable-table-row",
785
2077
  highlighted && "fractal-sortable-table-row--highlighted",
786
- onRowClick && "fractal-sortable-table-row--interactive"
2078
+ interactive && "fractal-sortable-table-row--interactive",
2079
+ disabled && "fractal-sortable-table-row--disabled"
787
2080
  ),
788
- onClick: onRowClick ? () => onRowClick(row, rowIndex) : void 0,
789
- onKeyDown: onRowClick ? (event) => {
2081
+ "data-disabled": disabled || void 0,
2082
+ onClick: !disabled && onRowClick ? () => onRowClick(row, rowIndex) : void 0,
2083
+ onDoubleClick: !disabled && onRowDoubleClick ? () => onRowDoubleClick(row, rowIndex) : void 0,
2084
+ onKeyDown: interactive ? (event) => {
790
2085
  if (event.key === "Enter" || event.key === " ") {
791
2086
  event.preventDefault();
792
- onRowClick(row, rowIndex);
2087
+ const activateRow = onRowClick ?? onRowDoubleClick;
2088
+ activateRow?.(row, rowIndex);
793
2089
  }
794
2090
  } : 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
- ))
2091
+ "aria-label": action?.label,
2092
+ "aria-disabled": disabled || void 0,
2093
+ role: interactive ? "button" : void 0,
2094
+ tabIndex: interactive ? 0 : void 0,
2095
+ children: [
2096
+ selection && /* @__PURE__ */ jsx25("td", { className: "fractal-sortable-table-td fractal-sortable-table-selection-cell", "data-pinned-column": "selection", children: /* @__PURE__ */ jsx25(
2097
+ Checkbox,
2098
+ {
2099
+ "aria-label": selection.getRowLabel(row),
2100
+ checked: !selectionDisabled && selection.isSelected(row),
2101
+ disabled: selectionDisabled,
2102
+ onClick: (event) => event.stopPropagation(),
2103
+ onCheckedChange: (checked) => {
2104
+ if (!selectionDisabled) selection.onSelectedChange(row, checked);
2105
+ }
2106
+ }
2107
+ ) }),
2108
+ columns.map((col, columnIndex) => /* @__PURE__ */ jsxs20(
2109
+ "td",
2110
+ {
2111
+ className: cn(
2112
+ "fractal-sortable-table-td",
2113
+ action && columnIndex === columns.length - 1 && "fractal-sortable-table-td--row-action"
2114
+ ),
2115
+ style: { ...columnStyle(col), textAlign: col.align ?? "left" },
2116
+ "data-column-align": col.align,
2117
+ "data-pinned-column": columnIndex === 0 ? "data" : void 0,
2118
+ children: [
2119
+ /* @__PURE__ */ jsx25(TableCellContent, { label: `${col.header || "Value"}, row ${rowIndex + 1}`, children: col.render(row, rowIndex) }),
2120
+ action && columnIndex === columns.length - 1 && /* @__PURE__ */ jsx25("span", { className: "fractal-sortable-table-row-action", "aria-hidden": "true", children: /* @__PURE__ */ jsx25("span", { className: "fractal-sortable-table-row-action__icon", children: action.icon }) })
2121
+ ]
2122
+ },
2123
+ col.key
2124
+ )),
2125
+ overflowMenu && /* @__PURE__ */ jsx25(
2126
+ "td",
2127
+ {
2128
+ className: "fractal-sortable-table-td fractal-table-overflow-cell",
2129
+ "data-pinned-column": "overflow",
2130
+ onClick: (event) => event.stopPropagation(),
2131
+ onDoubleClick: (event) => event.stopPropagation(),
2132
+ onKeyDown: (event) => event.stopPropagation(),
2133
+ children: /* @__PURE__ */ jsx25(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) })
2134
+ }
2135
+ )
2136
+ ]
806
2137
  },
807
2138
  rowIndex
808
2139
  );
809
2140
  }) })
810
- ] })
2141
+ ] }) })
811
2142
  ] }) });
812
2143
  }
813
2144
 
814
2145
  // 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";
2146
+ import { useId as useId5, useLayoutEffect as useLayoutEffect4, useRef as useRef8, useState as useState9 } from "react";
2147
+ import { jsx as jsx26, jsxs as jsxs21 } from "react/jsx-runtime";
817
2148
  function Tabs({
818
2149
  items,
819
2150
  defaultValue,
820
2151
  value: controlledValue,
821
2152
  onValueChange,
822
2153
  className,
823
- ariaLabel
2154
+ ariaLabel,
2155
+ divider = false,
2156
+ variant = "line"
824
2157
  }) {
825
- const reactId = useId();
826
- const [internalValue, setInternalValue] = useState4(
2158
+ const reactId = useId5();
2159
+ const [internalValue, setInternalValue] = useState9(
827
2160
  defaultValue ?? items.find((i) => !i.disabled)?.id ?? items[0]?.id ?? ""
828
2161
  );
829
2162
  const isControlled = controlledValue !== void 0;
830
2163
  const activeId = isControlled ? controlledValue : internalValue;
831
- const tabRefs = useRef3({});
2164
+ const tabListRef = useRef8(null);
2165
+ const tabRefs = useRef8({});
2166
+ const [indicator, setIndicator] = useState9(null);
2167
+ useLayoutEffect4(() => {
2168
+ const updateIndicator = () => {
2169
+ const activeTab = tabRefs.current[activeId];
2170
+ if (!activeTab) return;
2171
+ setIndicator({ offset: activeTab.offsetLeft, width: activeTab.offsetWidth });
2172
+ };
2173
+ updateIndicator();
2174
+ const list = tabListRef.current;
2175
+ if (!list || typeof ResizeObserver === "undefined") return;
2176
+ const observer = new ResizeObserver(updateIndicator);
2177
+ observer.observe(list);
2178
+ return () => observer.disconnect();
2179
+ }, [activeId, items.length]);
832
2180
  const selectTab = (id) => {
833
2181
  if (isControlled) {
834
2182
  onValueChange?.(id);
@@ -838,6 +2186,10 @@ function Tabs({
838
2186
  }
839
2187
  };
840
2188
  const focusable = items.filter((i) => !i.disabled);
2189
+ const indicatorStyle = indicator ? {
2190
+ "--fractal-tabs-indicator-offset": `${indicator.offset}px`,
2191
+ "--fractal-tabs-indicator-width": `${indicator.width}px`
2192
+ } : void 0;
841
2193
  const onKeyDown = (event) => {
842
2194
  const currentIndex = focusable.findIndex((i) => i.id === activeId);
843
2195
  if (currentIndex === -1) return;
@@ -863,41 +2215,52 @@ function Tabs({
863
2215
  selectTab(nextId2);
864
2216
  requestAnimationFrame(() => tabRefs.current[nextId2]?.focus());
865
2217
  };
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
- }) }),
2218
+ return /* @__PURE__ */ jsxs21("div", { className: cn("fractal-tabs", `fractal-tabs--${variant}`, divider && "fractal-tabs--divider", className), children: [
2219
+ /* @__PURE__ */ jsx26(
2220
+ "div",
2221
+ {
2222
+ ref: tabListRef,
2223
+ role: "tablist",
2224
+ "aria-label": ariaLabel,
2225
+ className: "fractal-tabs-list",
2226
+ "data-indicator-ready": indicator ? "true" : void 0,
2227
+ style: indicatorStyle,
2228
+ children: items.map((item) => {
2229
+ const isActive = item.id === activeId;
2230
+ const tabId = `${reactId}-tab-${item.id}`;
2231
+ const panelId = `${reactId}-panel-${item.id}`;
2232
+ return /* @__PURE__ */ jsx26(
2233
+ "button",
2234
+ {
2235
+ ref: (el) => {
2236
+ tabRefs.current[item.id] = el;
2237
+ },
2238
+ id: tabId,
2239
+ role: "tab",
2240
+ type: "button",
2241
+ "aria-selected": isActive,
2242
+ "aria-controls": panelId,
2243
+ tabIndex: isActive ? 0 : -1,
2244
+ disabled: item.disabled,
2245
+ className: cn(
2246
+ "fractal-tabs-trigger",
2247
+ isActive && "fractal-tabs-trigger--active",
2248
+ item.disabled && "fractal-tabs-trigger--disabled"
2249
+ ),
2250
+ onClick: () => !item.disabled && selectTab(item.id),
2251
+ onKeyDown,
2252
+ children: item.label
2253
+ },
2254
+ item.id
2255
+ );
2256
+ })
2257
+ }
2258
+ ),
896
2259
  items.map((item) => {
897
2260
  const tabId = `${reactId}-tab-${item.id}`;
898
2261
  const panelId = `${reactId}-panel-${item.id}`;
899
2262
  const isActive = item.id === activeId;
900
- return /* @__PURE__ */ jsx13(
2263
+ return /* @__PURE__ */ jsx26(
901
2264
  "div",
902
2265
  {
903
2266
  id: panelId,
@@ -913,10 +2276,112 @@ function Tabs({
913
2276
  ] });
914
2277
  }
915
2278
 
2279
+ // src/components/ContentSwitcher/ContentSwitcher.tsx
2280
+ import * as React14 from "react";
2281
+ import { jsx as jsx27, jsxs as jsxs22 } from "react/jsx-runtime";
2282
+ function ContentSwitcher({ items, value, onValueChange, ariaLabel, fullWidth = false, className }) {
2283
+ const switcherRef = React14.useRef(null);
2284
+ const buttonRefs = React14.useRef([]);
2285
+ const indicatorMotionReadyRef = React14.useRef(false);
2286
+ const activeIndex = items.findIndex((item) => item.value === value);
2287
+ React14.useLayoutEffect(() => {
2288
+ const switcher = switcherRef.current;
2289
+ const activeButton = buttonRefs.current[activeIndex];
2290
+ let motionFrame = 0;
2291
+ if (!switcher || !activeButton) {
2292
+ switcher?.removeAttribute("data-indicator-ready");
2293
+ switcher?.removeAttribute("data-indicator-animate");
2294
+ indicatorMotionReadyRef.current = false;
2295
+ return;
2296
+ }
2297
+ const updateIndicator = () => {
2298
+ const switcherRect = switcher.getBoundingClientRect();
2299
+ const activeButtonRect = activeButton.getBoundingClientRect();
2300
+ switcher.style.setProperty("--fractal-content-switcher-indicator-x", `${activeButtonRect.left - switcherRect.left - switcher.clientLeft}px`);
2301
+ switcher.style.setProperty("--fractal-content-switcher-indicator-y", `${activeButtonRect.top - switcherRect.top - switcher.clientTop}px`);
2302
+ switcher.style.setProperty("--fractal-content-switcher-indicator-width", `${activeButtonRect.width}px`);
2303
+ switcher.style.setProperty("--fractal-content-switcher-indicator-height", `${activeButtonRect.height}px`);
2304
+ switcher.setAttribute("data-indicator-ready", "true");
2305
+ if (!indicatorMotionReadyRef.current && motionFrame === 0) {
2306
+ motionFrame = window.requestAnimationFrame(() => {
2307
+ indicatorMotionReadyRef.current = true;
2308
+ switcher.setAttribute("data-indicator-animate", "true");
2309
+ motionFrame = 0;
2310
+ });
2311
+ }
2312
+ };
2313
+ updateIndicator();
2314
+ if (typeof ResizeObserver === "undefined") {
2315
+ return () => {
2316
+ if (motionFrame !== 0) window.cancelAnimationFrame(motionFrame);
2317
+ };
2318
+ }
2319
+ const resizeObserver = new ResizeObserver(updateIndicator);
2320
+ resizeObserver.observe(switcher);
2321
+ buttonRefs.current.forEach((button) => {
2322
+ if (button) resizeObserver.observe(button);
2323
+ });
2324
+ return () => {
2325
+ resizeObserver.disconnect();
2326
+ if (motionFrame !== 0) window.cancelAnimationFrame(motionFrame);
2327
+ };
2328
+ }, [activeIndex, items.length]);
2329
+ const moveFocus = (index, direction) => {
2330
+ let nextIndex = index;
2331
+ do {
2332
+ nextIndex = (nextIndex + direction + items.length) % items.length;
2333
+ } while (items[nextIndex]?.disabled && nextIndex !== index);
2334
+ if (!items[nextIndex]?.disabled) {
2335
+ buttonRefs.current[nextIndex]?.focus();
2336
+ onValueChange(items[nextIndex].value);
2337
+ }
2338
+ };
2339
+ return /* @__PURE__ */ jsxs22(
2340
+ "div",
2341
+ {
2342
+ className: cn("fractal-content-switcher", fullWidth && "fractal-content-switcher--full-width", className),
2343
+ role: "group",
2344
+ "aria-label": ariaLabel,
2345
+ ref: switcherRef,
2346
+ children: [
2347
+ /* @__PURE__ */ jsx27("span", { className: "fractal-content-switcher__indicator", "aria-hidden": true }),
2348
+ items.map((item, index) => {
2349
+ const active = item.value === value;
2350
+ return /* @__PURE__ */ jsx27(
2351
+ "button",
2352
+ {
2353
+ className: cn("fractal-content-switcher__item", active && "fractal-content-switcher__item--active"),
2354
+ type: "button",
2355
+ "aria-pressed": active,
2356
+ disabled: item.disabled,
2357
+ onClick: () => onValueChange(item.value),
2358
+ onKeyDown: (event) => {
2359
+ if (event.key === "ArrowRight") {
2360
+ event.preventDefault();
2361
+ moveFocus(index, 1);
2362
+ }
2363
+ if (event.key === "ArrowLeft") {
2364
+ event.preventDefault();
2365
+ moveFocus(index, -1);
2366
+ }
2367
+ },
2368
+ ref: (element) => {
2369
+ buttonRefs.current[index] = element;
2370
+ },
2371
+ children: item.label
2372
+ },
2373
+ item.value
2374
+ );
2375
+ })
2376
+ ]
2377
+ }
2378
+ );
2379
+ }
2380
+
916
2381
  // 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";
2382
+ import { useId as useId6, useState as useState10 } from "react";
2383
+ import { ChevronRight as ChevronRight3, ChevronDown as ChevronDown2 } from "@mirror-physics/fractal-icons";
2384
+ import { jsx as jsx28, jsxs as jsxs23 } from "react/jsx-runtime";
920
2385
  function Disclosure({
921
2386
  title,
922
2387
  meta,
@@ -924,16 +2389,18 @@ function Disclosure({
924
2389
  defaultOpen = false,
925
2390
  open: controlledOpen,
926
2391
  onOpenChange,
2392
+ disabled = false,
927
2393
  className,
928
2394
  variant = "card"
929
2395
  }) {
930
- const reactId = useId2();
2396
+ const reactId = useId6();
931
2397
  const headerId = `${reactId}-header`;
932
2398
  const panelId = `${reactId}-panel`;
933
- const [internalOpen, setInternalOpen] = useState5(defaultOpen);
2399
+ const [internalOpen, setInternalOpen] = useState10(defaultOpen);
934
2400
  const isControlled = controlledOpen !== void 0;
935
2401
  const isOpen = isControlled ? controlledOpen : internalOpen;
936
2402
  const toggle = () => {
2403
+ if (disabled) return;
937
2404
  const next = !isOpen;
938
2405
  if (isControlled) {
939
2406
  onOpenChange?.(next);
@@ -942,17 +2409,18 @@ function Disclosure({
942
2409
  onOpenChange?.(next);
943
2410
  }
944
2411
  };
945
- return /* @__PURE__ */ jsxs11(
2412
+ return /* @__PURE__ */ jsxs23(
946
2413
  "div",
947
2414
  {
948
2415
  className: cn(
949
2416
  "fractal-disclosure",
950
2417
  variant === "card" && "fractal-disclosure--card",
951
2418
  isOpen && "fractal-disclosure--open",
2419
+ disabled && "fractal-disclosure--disabled",
952
2420
  className
953
2421
  ),
954
2422
  children: [
955
- /* @__PURE__ */ jsxs11(
2423
+ /* @__PURE__ */ jsxs23(
956
2424
  "button",
957
2425
  {
958
2426
  type: "button",
@@ -960,15 +2428,16 @@ function Disclosure({
960
2428
  className: "fractal-disclosure-trigger",
961
2429
  "aria-expanded": isOpen,
962
2430
  "aria-controls": panelId,
2431
+ disabled,
963
2432
  onClick: toggle,
964
2433
  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 })
2434
+ /* @__PURE__ */ jsx28("span", { className: "fractal-disclosure-icon", "aria-hidden": "true", children: isOpen ? /* @__PURE__ */ jsx28(ChevronDown2, { size: 16 }) : /* @__PURE__ */ jsx28(ChevronRight3, { size: 16 }) }),
2435
+ /* @__PURE__ */ jsx28("span", { className: "fractal-disclosure-title", children: title }),
2436
+ meta && /* @__PURE__ */ jsx28("span", { className: "fractal-disclosure-meta", children: meta })
968
2437
  ]
969
2438
  }
970
2439
  ),
971
- /* @__PURE__ */ jsx14(
2440
+ /* @__PURE__ */ jsx28(
972
2441
  "div",
973
2442
  {
974
2443
  id: panelId,
@@ -976,7 +2445,7 @@ function Disclosure({
976
2445
  "aria-labelledby": headerId,
977
2446
  className: "fractal-disclosure-panel",
978
2447
  hidden: !isOpen,
979
- children: /* @__PURE__ */ jsx14("div", { className: "fractal-disclosure-panel-inner", children })
2448
+ children: /* @__PURE__ */ jsx28("div", { className: "fractal-disclosure-panel-inner", children })
980
2449
  }
981
2450
  )
982
2451
  ]
@@ -984,39 +2453,260 @@ function Disclosure({
984
2453
  );
985
2454
  }
986
2455
 
2456
+ // src/components/LatticeLoader/LatticeLoader.tsx
2457
+ import { jsx as jsx29 } from "react/jsx-runtime";
2458
+ var LATTICE_COUNT = 160;
2459
+ var WAVE_PERIOD = 80;
2460
+ function LatticeLoader({ className, label = "Loading", ...props }) {
2461
+ return /* @__PURE__ */ jsx29(
2462
+ "span",
2463
+ {
2464
+ "aria-label": label,
2465
+ className: cn("fractal-lattice-loader", className),
2466
+ role: "progressbar",
2467
+ ...props,
2468
+ children: Array.from({ length: LATTICE_COUNT }, (_, index) => /* @__PURE__ */ jsx29(
2469
+ "span",
2470
+ {
2471
+ "aria-hidden": "true",
2472
+ className: "fractal-lattice-loader__cell",
2473
+ style: { "--fractal-lattice-index": index % WAVE_PERIOD }
2474
+ },
2475
+ index
2476
+ ))
2477
+ }
2478
+ );
2479
+ }
2480
+
2481
+ // src/components/Ghost/Ghost.tsx
2482
+ import { jsx as jsx30, jsxs as jsxs24 } from "react/jsx-runtime";
2483
+ var toCssLength = (value) => typeof value === "number" ? `${value}px` : value;
2484
+ function Ghost({ className, width, height, radius, style, ...props }) {
2485
+ return /* @__PURE__ */ jsx30(
2486
+ "div",
2487
+ {
2488
+ "aria-hidden": "true",
2489
+ className: cn("fractal-ghost", className),
2490
+ style: {
2491
+ width: toCssLength(width),
2492
+ height: toCssLength(height),
2493
+ borderRadius: toCssLength(radius),
2494
+ ...style
2495
+ },
2496
+ ...props
2497
+ }
2498
+ );
2499
+ }
2500
+ function GhostLine({ className, size = "md", width = "100%", ...props }) {
2501
+ return /* @__PURE__ */ jsx30(Ghost, { className: cn("fractal-ghost-line", `fractal-ghost-line--${size}`, className), width, ...props });
2502
+ }
2503
+ function ButtonGhost({ className, size = "lg", controlSize, iconOnly = false, width, ...props }) {
2504
+ return /* @__PURE__ */ jsx30(Ghost, { "data-control-size": controlSize ?? size, className: cn("fractal-ghost-button", `fractal-ghost-button--${size}`, iconOnly && "fractal-ghost-button--icon-only", className), width, ...props });
2505
+ }
2506
+ function CardGhost({ className, lines = 3, showHeader = true, ...props }) {
2507
+ return /* @__PURE__ */ jsxs24("div", { "aria-hidden": "true", className: cn("fractal-ghost-card", className), ...props, children: [
2508
+ showHeader && /* @__PURE__ */ jsx30(GhostLine, { width: "38%" }),
2509
+ /* @__PURE__ */ jsx30("div", { className: "fractal-ghost-stack", children: Array.from({ length: lines }, (_, index) => /* @__PURE__ */ jsx30(GhostLine, { width: index === lines - 1 ? "62%" : "100%" }, index)) })
2510
+ ] });
2511
+ }
2512
+ function DataTableGhost({
2513
+ className,
2514
+ showHeader = true,
2515
+ headers,
2516
+ columns = 4,
2517
+ rows = 5,
2518
+ columnWidths,
2519
+ size = "md",
2520
+ ...props
2521
+ }) {
2522
+ const cells = Array.from({ length: columns }, (_, index) => index);
2523
+ return /* @__PURE__ */ jsx30("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: [
2524
+ (showHeader || headers) && /* @__PURE__ */ jsx30("thead", { children: /* @__PURE__ */ jsx30("tr", { className: "fractal-ghost-table__head-row", children: cells.map((index) => /* @__PURE__ */ jsx30("th", { style: { width: columnWidths?.[index] }, children: headers?.[index] ?? /* @__PURE__ */ jsx30(GhostLine, { size: "sm", width: index === columns - 1 ? "62%" : "76%" }) }, index)) }) }),
2525
+ /* @__PURE__ */ jsx30("tbody", { children: Array.from({ length: rows }, (_, rowIndex) => /* @__PURE__ */ jsx30("tr", { className: "fractal-ghost-table__row", children: cells.map((columnIndex) => /* @__PURE__ */ jsx30("td", { style: { width: columnWidths?.[columnIndex] }, children: /* @__PURE__ */ jsx30(GhostLine, { width: `${[72, 54, 82, 63, 46][(rowIndex + columnIndex) % 5]}%` }) }, columnIndex)) }, rowIndex)) })
2526
+ ] }) });
2527
+ }
2528
+ function InputGhost({ className, labelWidth = "26%", ...props }) {
2529
+ return /* @__PURE__ */ jsx30(FormFieldGhost, { className, labelWidth, ...props });
2530
+ }
2531
+ function TextareaGhost({ className, labelWidth = "26%", ...props }) {
2532
+ return /* @__PURE__ */ jsx30(FormFieldGhost, { className, labelWidth, multiline: true, ...props });
2533
+ }
2534
+ function LabelGhost({ className, width = "26%", ...props }) {
2535
+ return /* @__PURE__ */ jsx30(GhostLine, { className: cn("fractal-ghost-label", className), size: "sm", width, ...props });
2536
+ }
2537
+ function FormFieldGhost({ className, labelWidth = "26%", multiline = false, controlSize = "lg", ...props }) {
2538
+ return /* @__PURE__ */ jsxs24("div", { "aria-hidden": "true", "data-control-size": controlSize, className: cn("fractal-ghost-field", className), ...props, children: [
2539
+ /* @__PURE__ */ jsx30(LabelGhost, { width: labelWidth }),
2540
+ /* @__PURE__ */ jsx30(Ghost, { className: cn("fractal-ghost-field__control", multiline && "fractal-ghost-field__control--multiline") })
2541
+ ] });
2542
+ }
2543
+ function AlertGhost({ className, ...props }) {
2544
+ return /* @__PURE__ */ jsxs24("div", { "aria-hidden": "true", className: cn("fractal-ghost-alert", className), ...props, children: [
2545
+ /* @__PURE__ */ jsx30(GhostLine, { width: "30%" }),
2546
+ /* @__PURE__ */ jsx30(GhostLine, { size: "sm", width: "78%" })
2547
+ ] });
2548
+ }
2549
+ function CheckboxGhost({ className, ...props }) {
2550
+ return /* @__PURE__ */ jsxs24("div", { "aria-hidden": "true", className: cn("fractal-ghost-choice", className), ...props, children: [
2551
+ /* @__PURE__ */ jsx30(Ghost, { className: "fractal-ghost-choice__control" }),
2552
+ /* @__PURE__ */ jsx30(GhostLine, { width: "42%" })
2553
+ ] });
2554
+ }
2555
+ var ToggleGhost = CheckboxGhost;
2556
+ function ChipGhost({ className, width = 74, ...props }) {
2557
+ return /* @__PURE__ */ jsx30(Ghost, { className: cn("fractal-ghost-chip", className), height: 24, width, ...props });
2558
+ }
2559
+ function LinkGhost({ className, width = 96, ...props }) {
2560
+ return /* @__PURE__ */ jsx30(GhostLine, { className: cn("fractal-ghost-link", className), size: "sm", width, ...props });
2561
+ }
2562
+ var TextButtonGhost = LinkGhost;
2563
+ function DropdownGhost({ className, controlSize = "lg", ...props }) {
2564
+ return /* @__PURE__ */ jsxs24("div", { "aria-hidden": "true", "data-control-size": controlSize, className: cn("fractal-ghost-dropdown", className), ...props, children: [
2565
+ /* @__PURE__ */ jsx30(GhostLine, { width: "38%" }),
2566
+ /* @__PURE__ */ jsx30(Ghost, { className: "fractal-ghost-dropdown__chevron" })
2567
+ ] });
2568
+ }
2569
+ function ContentSwitcherGhost({ className, options = 3, ...props }) {
2570
+ return /* @__PURE__ */ jsx30("div", { "aria-hidden": "true", className: cn("fractal-ghost-switcher", className), ...props, children: Array.from({ length: options }, (_, index) => /* @__PURE__ */ jsx30(Ghost, {}, index)) });
2571
+ }
2572
+ var TabsGhost = ContentSwitcherGhost;
2573
+ function DisclosureGhost({ className, ...props }) {
2574
+ return /* @__PURE__ */ jsxs24("div", { "aria-hidden": "true", className: cn("fractal-ghost-disclosure", className), ...props, children: [
2575
+ /* @__PURE__ */ jsx30(Ghost, { className: "fractal-ghost-disclosure__icon" }),
2576
+ /* @__PURE__ */ jsx30(GhostLine, { width: "42%" })
2577
+ ] });
2578
+ }
2579
+ function PaginationGhost({ className, pages = 5, ...props }) {
2580
+ return /* @__PURE__ */ jsx30("div", { "aria-hidden": "true", className: cn("fractal-ghost-pagination", className), ...props, children: Array.from({ length: pages }, (_, index) => /* @__PURE__ */ jsx30(Ghost, {}, index)) });
2581
+ }
2582
+ function FileDropzoneGhost({ className, ...props }) {
2583
+ return /* @__PURE__ */ jsxs24("div", { "aria-hidden": "true", className: cn("fractal-ghost-dropzone", className), ...props, children: [
2584
+ /* @__PURE__ */ jsx30(Ghost, { className: "fractal-ghost-dropzone__icon" }),
2585
+ /* @__PURE__ */ jsx30(GhostLine, { width: "26%" }),
2586
+ /* @__PURE__ */ jsx30(GhostLine, { size: "sm", width: "46%" })
2587
+ ] });
2588
+ }
2589
+ function PromptCardGhost({ className, ...props }) {
2590
+ return /* @__PURE__ */ jsx30(CardGhost, { className: cn("fractal-ghost-prompt-card", className), lines: 4, ...props });
2591
+ }
2592
+ function ConversationGhost({
2593
+ className,
2594
+ "aria-label": ariaLabel = "Loading conversation",
2595
+ ...props
2596
+ }) {
2597
+ return /* @__PURE__ */ jsxs24(
2598
+ "div",
2599
+ {
2600
+ "aria-busy": "true",
2601
+ "aria-label": ariaLabel,
2602
+ className: cn("fractal-ghost-conversation", className),
2603
+ role: "status",
2604
+ ...props,
2605
+ children: [
2606
+ /* @__PURE__ */ jsx30("div", { "aria-hidden": "true", className: "fractal-ghost-conversation__message fractal-ghost-conversation__message--user", children: /* @__PURE__ */ jsxs24("div", { className: "fractal-ghost-conversation__bubble", children: [
2607
+ /* @__PURE__ */ jsx30(GhostLine, { width: "100%" }),
2608
+ /* @__PURE__ */ jsx30(GhostLine, { width: "86%" }),
2609
+ /* @__PURE__ */ jsx30(GhostLine, { width: "64%" })
2610
+ ] }) }),
2611
+ /* @__PURE__ */ jsxs24("div", { "aria-hidden": "true", className: "fractal-ghost-conversation__message fractal-ghost-conversation__message--assistant", children: [
2612
+ /* @__PURE__ */ jsx30(GhostLine, { width: "92%" }),
2613
+ /* @__PURE__ */ jsx30(GhostLine, { width: "78%" }),
2614
+ /* @__PURE__ */ jsx30(GhostLine, { width: "58%" })
2615
+ ] })
2616
+ ]
2617
+ }
2618
+ );
2619
+ }
2620
+ function SortableTableGhost(props) {
2621
+ return /* @__PURE__ */ jsx30(DataTableGhost, { ...props });
2622
+ }
2623
+ function SidebarGhost({ className, items = 6, ...props }) {
2624
+ return /* @__PURE__ */ jsxs24("aside", { "aria-hidden": "true", className: cn("fractal-ghost-sidebar", className), ...props, children: [
2625
+ /* @__PURE__ */ jsx30(Ghost, { className: "fractal-ghost-sidebar__brand" }),
2626
+ /* @__PURE__ */ jsx30("div", { className: "fractal-ghost-sidebar__items", children: Array.from({ length: items }, (_, index) => /* @__PURE__ */ jsxs24("div", { className: "fractal-ghost-sidebar__item", children: [
2627
+ /* @__PURE__ */ jsx30(Ghost, {}),
2628
+ /* @__PURE__ */ jsx30(GhostLine, { width: `${[54, 68, 44, 61][index % 4]}%` })
2629
+ ] }, index)) })
2630
+ ] });
2631
+ }
2632
+ function ModalGhost({ className, title, lines = 3, ...props }) {
2633
+ return /* @__PURE__ */ jsx30(SurfaceGhost, { className: cn("fractal-ghost-modal", className), title, lines, ...props });
2634
+ }
2635
+ function SidePanelGhost({ className, title, lines = 5, ...props }) {
2636
+ return /* @__PURE__ */ jsx30(SurfaceGhost, { className: cn("fractal-ghost-side-panel", className), title, lines, ...props });
2637
+ }
2638
+ function SurfaceGhost({ className, title, lines = 3, ...props }) {
2639
+ return /* @__PURE__ */ jsxs24("div", { "aria-hidden": "true", className: cn("fractal-ghost-surface", className), ...props, children: [
2640
+ /* @__PURE__ */ jsxs24("div", { className: "fractal-ghost-surface__header", children: [
2641
+ /* @__PURE__ */ jsx30(GhostLine, { width: "42%" }),
2642
+ /* @__PURE__ */ jsx30(Ghost, { className: "fractal-ghost-surface__close" })
2643
+ ] }),
2644
+ title && /* @__PURE__ */ jsx30("div", { className: "fractal-ghost-surface__title", children: title }),
2645
+ /* @__PURE__ */ jsx30("div", { className: "fractal-ghost-stack", children: Array.from({ length: lines }, (_, index) => /* @__PURE__ */ jsx30(GhostLine, { width: index === lines - 1 ? "64%" : "100%" }, index)) })
2646
+ ] });
2647
+ }
2648
+
987
2649
  // 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(
2650
+ import * as React15 from "react";
2651
+ import { Fragment as Fragment3, jsx as jsx31, jsxs as jsxs25 } from "react/jsx-runtime";
2652
+ var Sidebar = React15.forwardRef(
2653
+ ({ className, width, compact = false, style, ...props }, ref) => /* @__PURE__ */ jsx31(
992
2654
  "aside",
993
2655
  {
994
2656
  ref,
995
2657
  className: cn("fractal-sidebar", className),
2658
+ "data-compact": compact || void 0,
996
2659
  style: { ...style, ...width ? { "--fractal-sidebar-width": width } : {} },
997
2660
  ...props
998
2661
  }
999
2662
  )
1000
2663
  );
1001
2664
  Sidebar.displayName = "Sidebar";
1002
- var SidebarHeader = React10.forwardRef(
1003
- ({ className, ...props }, ref) => /* @__PURE__ */ jsx15("div", { ref, className: cn("fractal-sidebar__header", className), ...props })
2665
+ var SidebarHeader = React15.forwardRef(
2666
+ ({ className, ...props }, ref) => /* @__PURE__ */ jsx31("div", { ref, className: cn("fractal-sidebar__header", className), ...props })
1004
2667
  );
1005
2668
  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 })
2669
+ var SidebarBrand = React15.forwardRef(
2670
+ ({ className, type = "button", ...props }, ref) => /* @__PURE__ */ jsx31("button", { ref, type, className: cn("fractal-sidebar__brand", className), ...props })
1008
2671
  );
1009
2672
  SidebarBrand.displayName = "SidebarBrand";
1010
- var SidebarNav = React10.forwardRef(
1011
- ({ className, ...props }, ref) => /* @__PURE__ */ jsx15("nav", { ref, className: cn("fractal-sidebar__nav", className), ...props })
2673
+ function SidebarAccountMenu({
2674
+ name,
2675
+ items,
2676
+ onSelect,
2677
+ triggerLabel = "Account menu",
2678
+ icon,
2679
+ className
2680
+ }) {
2681
+ return /* @__PURE__ */ jsx31(
2682
+ Dropdown,
2683
+ {
2684
+ align: "left",
2685
+ className: cn("fractal-sidebar__account", className),
2686
+ items,
2687
+ onSelect,
2688
+ panelClassName: "fractal-sidebar__account-panel",
2689
+ selectedValue: "",
2690
+ triggerClassName: "fractal-sidebar__account-trigger",
2691
+ triggerContent: /* @__PURE__ */ jsxs25(Fragment3, { children: [
2692
+ icon && /* @__PURE__ */ jsx31("span", { className: "fractal-sidebar__account-icon", "aria-hidden": "true", children: icon }),
2693
+ /* @__PURE__ */ jsx31("span", { className: "fractal-sidebar__account-name", children: name })
2694
+ ] }),
2695
+ triggerLabel,
2696
+ triggerVariant: "tertiary"
2697
+ }
2698
+ );
2699
+ }
2700
+ var SidebarNav = React15.forwardRef(
2701
+ ({ className, ...props }, ref) => /* @__PURE__ */ jsx31("nav", { ref, className: cn("fractal-sidebar__nav", className), ...props })
1012
2702
  );
1013
2703
  SidebarNav.displayName = "SidebarNav";
1014
- var SidebarSection = React10.forwardRef(
1015
- ({ className, ...props }, ref) => /* @__PURE__ */ jsx15("div", { ref, className: cn("fractal-sidebar__section", className), ...props })
2704
+ var SidebarSection = React15.forwardRef(
2705
+ ({ className, ...props }, ref) => /* @__PURE__ */ jsx31("div", { ref, className: cn("fractal-sidebar__section", className), ...props })
1016
2706
  );
1017
2707
  SidebarSection.displayName = "SidebarSection";
1018
- var SidebarNavItem = React10.forwardRef(
1019
- ({ active = false, icon, endAdornment, className, type = "button", children, ...props }, ref) => /* @__PURE__ */ jsxs12(
2708
+ var SidebarNavItem = React15.forwardRef(
2709
+ ({ active = false, icon, endAdornment, className, type = "button", children, ...props }, ref) => /* @__PURE__ */ jsxs25(
1020
2710
  "button",
1021
2711
  {
1022
2712
  ref,
@@ -1026,28 +2716,60 @@ var SidebarNavItem = React10.forwardRef(
1026
2716
  "aria-current": active ? "page" : void 0,
1027
2717
  ...props,
1028
2718
  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 })
2719
+ icon && /* @__PURE__ */ jsx31("span", { className: "fractal-sidebar__nav-item-icon", "aria-hidden": "true", children: icon }),
2720
+ /* @__PURE__ */ jsx31("span", { className: "fractal-sidebar__nav-item-label", children }),
2721
+ endAdornment && /* @__PURE__ */ jsx31("span", { className: "fractal-sidebar__nav-item-end", children: endAdornment })
1032
2722
  ]
1033
2723
  }
1034
2724
  )
1035
2725
  );
1036
2726
  SidebarNavItem.displayName = "SidebarNavItem";
1037
- var SidebarFooter = React10.forwardRef(
1038
- ({ className, ...props }, ref) => /* @__PURE__ */ jsx15("div", { ref, className: cn("fractal-sidebar__footer", className), ...props })
2727
+ var SidebarFooter = React15.forwardRef(
2728
+ ({ className, ...props }, ref) => /* @__PURE__ */ jsx31("div", { ref, className: cn("fractal-sidebar__footer", className), ...props })
1039
2729
  );
1040
2730
  SidebarFooter.displayName = "SidebarFooter";
1041
2731
 
1042
2732
  // 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 }) {
2733
+ import * as React16 from "react";
2734
+ import { createPortal as createPortal4 } from "react-dom";
2735
+ import { Close as Close3 } from "@mirror-physics/fractal-icons";
2736
+ import { jsx as jsx32, jsxs as jsxs26 } from "react/jsx-runtime";
2737
+ function SidePanel({ open, onClose, title, description, children, footer, size = "md", loading = false, loadingLabel = "Loading", className }) {
1047
2738
  useEscapeDismiss(80, onClose, open);
2739
+ const bodyRef = React16.useRef(null);
2740
+ const [scrollEdges, setScrollEdges] = React16.useState({ above: false, below: false });
2741
+ React16.useLayoutEffect(() => {
2742
+ const body = bodyRef.current;
2743
+ if (!open || !body) return;
2744
+ const measure = () => {
2745
+ const maxScroll = Math.max(0, body.scrollHeight - body.clientHeight);
2746
+ const top = Math.max(0, Math.min(body.scrollTop, maxScroll));
2747
+ const above = maxScroll > 1 && top > 0;
2748
+ const below = maxScroll - top > 1;
2749
+ setScrollEdges((previous) => previous.above === above && previous.below === below ? previous : { above, below });
2750
+ };
2751
+ const resize = new ResizeObserver(measure);
2752
+ const observeContent = () => {
2753
+ resize.disconnect();
2754
+ resize.observe(body);
2755
+ for (const child of body.children) resize.observe(child);
2756
+ measure();
2757
+ };
2758
+ const mutations = new MutationObserver(observeContent);
2759
+ mutations.observe(body, { childList: true, subtree: true, characterData: true, attributes: true });
2760
+ body.addEventListener("scroll", measure, { passive: true });
2761
+ body.addEventListener("load", measure, true);
2762
+ observeContent();
2763
+ return () => {
2764
+ resize.disconnect();
2765
+ mutations.disconnect();
2766
+ body.removeEventListener("scroll", measure);
2767
+ body.removeEventListener("load", measure, true);
2768
+ };
2769
+ }, [open, loading]);
1048
2770
  if (!open) return null;
1049
- return createPortal2(
1050
- /* @__PURE__ */ jsx16("div", { className: "fractal-side-panel-overlay", onMouseDown: onClose, children: /* @__PURE__ */ jsxs13(
2771
+ return createPortal4(
2772
+ /* @__PURE__ */ jsx32("div", { className: "fractal-side-panel-overlay", onMouseDown: onClose, children: /* @__PURE__ */ jsxs26(
1051
2773
  "aside",
1052
2774
  {
1053
2775
  className: cn("fractal-side-panel", `fractal-side-panel--${size}`, className),
@@ -1056,15 +2778,15 @@ function SidePanel({ open, onClose, title, description, children, footer, size =
1056
2778
  role: "dialog",
1057
2779
  onMouseDown: (event) => event.stopPropagation(),
1058
2780
  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 })
2781
+ /* @__PURE__ */ jsxs26("header", { className: "fractal-side-panel__header", "data-scroll-divider": scrollEdges.above, children: [
2782
+ /* @__PURE__ */ jsxs26("div", { className: "fractal-side-panel__heading", children: [
2783
+ typeof title === "string" ? /* @__PURE__ */ jsx32("h2", { className: "fractal-side-panel__title", children: title }) : /* @__PURE__ */ jsx32("div", { className: "fractal-side-panel__title", children: title }),
2784
+ description && /* @__PURE__ */ jsx32("p", { className: "fractal-side-panel__description", children: description })
1063
2785
  ] }),
1064
- /* @__PURE__ */ jsx16("button", { type: "button", className: "fractal-side-panel__close", "aria-label": "Close panel", onClick: onClose, children: /* @__PURE__ */ jsx16(Close, { size: 18 }) })
2786
+ /* @__PURE__ */ jsx32("button", { type: "button", className: "fractal-side-panel__close", "aria-label": "Close panel", onClick: onClose, children: /* @__PURE__ */ jsx32(Close3, { size: 18 }) })
1065
2787
  ] }),
1066
- /* @__PURE__ */ jsx16("div", { className: "fractal-side-panel__body", children }),
1067
- footer && /* @__PURE__ */ jsx16("footer", { className: "fractal-side-panel__footer", children: footer })
2788
+ /* @__PURE__ */ jsx32("div", { ref: bodyRef, "aria-busy": loading || void 0, className: cn("fractal-side-panel__body", loading && "fractal-side-panel__body--loading"), children: loading ? /* @__PURE__ */ jsx32(UILoader, { label: loadingLabel }) : children }),
2789
+ footer && /* @__PURE__ */ jsx32("footer", { className: "fractal-side-panel__footer", "data-scroll-divider": scrollEdges.below, children: footer })
1068
2790
  ]
1069
2791
  }
1070
2792
  ) }),
@@ -1073,17 +2795,57 @@ function SidePanel({ open, onClose, title, description, children, footer, size =
1073
2795
  }
1074
2796
 
1075
2797
  // 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 })
2798
+ import * as React17 from "react";
2799
+ import { jsx as jsx33, jsxs as jsxs27 } from "react/jsx-runtime";
2800
+ var Textarea = React17.forwardRef(
2801
+ ({ className, fieldLabel, textStyle = "source", id, placeholder, ...props }, ref) => {
2802
+ const generatedId = React17.useId();
2803
+ const fieldId = id ?? generatedId;
2804
+ const input = /* @__PURE__ */ jsx33(
2805
+ "textarea",
2806
+ {
2807
+ ref,
2808
+ id: fieldLabel ? fieldId : id,
2809
+ placeholder: fieldLabel ? placeholder || " " : placeholder,
2810
+ className: cn("fractal-textarea", className),
2811
+ "data-text-style": textStyle,
2812
+ ...props
2813
+ }
2814
+ );
2815
+ return fieldLabel ? /* @__PURE__ */ jsxs27("div", { className: "fractal-floating-field fractal-floating-field--textarea", "data-control-size": "md", children: [
2816
+ input,
2817
+ /* @__PURE__ */ jsx33(FloatingFieldBorder, { label: fieldLabel ?? "" }),
2818
+ /* @__PURE__ */ jsx33("label", { className: "fractal-floating-field__label", htmlFor: fieldId, children: fieldLabel })
2819
+ ] }) : input;
2820
+ }
1080
2821
  );
1081
2822
  Textarea.displayName = "Textarea";
1082
2823
 
2824
+ // src/components/TextButton/TextButton.tsx
2825
+ import * as React18 from "react";
2826
+ import { jsx as jsx34, jsxs as jsxs28 } from "react/jsx-runtime";
2827
+ var TextButton = React18.forwardRef(
2828
+ ({ className, icon, iconPosition = "start", size = "md", tone = "primary", children, ...props }, ref) => /* @__PURE__ */ jsxs28(
2829
+ "button",
2830
+ {
2831
+ ref,
2832
+ className: cn("fractal-text-button", `fractal-text-button--${size}`, `fractal-text-button--${tone}`, className),
2833
+ type: "button",
2834
+ ...props,
2835
+ children: [
2836
+ icon && iconPosition === "start" && /* @__PURE__ */ jsx34("span", { className: "fractal-text-button__icon", "aria-hidden": "true", children: icon }),
2837
+ /* @__PURE__ */ jsx34("span", { children }),
2838
+ icon && iconPosition === "end" && /* @__PURE__ */ jsx34("span", { className: "fractal-text-button__icon", "aria-hidden": "true", children: icon })
2839
+ ]
2840
+ }
2841
+ )
2842
+ );
2843
+ TextButton.displayName = "TextButton";
2844
+
1083
2845
  // src/components/FileDropzone/FileDropzone.tsx
1084
- import * as React12 from "react";
1085
- import { Close as Close2, FileArrowUp } from "@mirror-physics/fractal-icons";
1086
- import { jsx as jsx18, jsxs as jsxs14 } from "react/jsx-runtime";
2846
+ import * as React19 from "react";
2847
+ import { Close as Close4, FileArrowUp } from "@mirror-physics/fractal-icons";
2848
+ import { jsx as jsx35, jsxs as jsxs29 } from "react/jsx-runtime";
1087
2849
  function FileDropzone({
1088
2850
  files,
1089
2851
  onFilesChange,
@@ -1094,17 +2856,17 @@ function FileDropzone({
1094
2856
  description = "Files stay attached to this record.",
1095
2857
  className
1096
2858
  }) {
1097
- const inputId = React12.useId();
1098
- const inputRef = React12.useRef(null);
1099
- const [isDragging, setIsDragging] = React12.useState(false);
2859
+ const inputId = React19.useId();
2860
+ const inputRef = React19.useRef(null);
2861
+ const [isDragging, setIsDragging] = React19.useState(false);
1100
2862
  const addFiles = (nextFiles) => {
1101
2863
  if (disabled) return;
1102
2864
  const additions = Array.from(nextFiles);
1103
2865
  if (additions.length === 0) return;
1104
2866
  onFilesChange(multiple ? [...files, ...additions] : additions.slice(0, 1));
1105
2867
  };
1106
- return /* @__PURE__ */ jsxs14("div", { className: cn("fractal-file-dropzone", className), children: [
1107
- /* @__PURE__ */ jsx18(
2868
+ return /* @__PURE__ */ jsxs29("div", { className: cn("fractal-file-dropzone", className), children: [
2869
+ /* @__PURE__ */ jsx35(
1108
2870
  "input",
1109
2871
  {
1110
2872
  ref: inputRef,
@@ -1120,7 +2882,7 @@ function FileDropzone({
1120
2882
  }
1121
2883
  }
1122
2884
  ),
1123
- /* @__PURE__ */ jsxs14(
2885
+ /* @__PURE__ */ jsxs29(
1124
2886
  "button",
1125
2887
  {
1126
2888
  className: "fractal-file-dropzone__target",
@@ -1143,18 +2905,18 @@ function FileDropzone({
1143
2905
  addFiles(event.dataTransfer.files);
1144
2906
  },
1145
2907
  children: [
1146
- /* @__PURE__ */ jsx18(FileArrowUp, { "aria-hidden": "true", size: 20 }),
1147
- /* @__PURE__ */ jsxs14("span", { className: "fractal-file-dropzone__copy", children: [
1148
- /* @__PURE__ */ jsx18("span", { className: "fractal-file-dropzone__title", children: title }),
1149
- description && /* @__PURE__ */ jsx18("span", { className: "fractal-file-dropzone__description", children: description })
2908
+ /* @__PURE__ */ jsx35(FileArrowUp, { "aria-hidden": "true", size: 20 }),
2909
+ /* @__PURE__ */ jsxs29("span", { className: "fractal-file-dropzone__copy", children: [
2910
+ /* @__PURE__ */ jsx35("span", { className: "fractal-file-dropzone__title", children: title }),
2911
+ description && /* @__PURE__ */ jsx35("span", { className: "fractal-file-dropzone__description", children: description })
1150
2912
  ] })
1151
2913
  ]
1152
2914
  }
1153
2915
  ),
1154
- files.length > 0 && /* @__PURE__ */ jsx18("ul", { className: "fractal-file-dropzone__files", "aria-label": "Attached files", children: files.map((file, index) => /* @__PURE__ */ jsxs14("li", { className: "fractal-file-dropzone__file", children: [
1155
- /* @__PURE__ */ jsx18("span", { className: "fractal-file-dropzone__file-name", children: file.name }),
1156
- /* @__PURE__ */ jsx18("span", { className: "fractal-file-dropzone__file-meta", children: formatFileSize(file.size) }),
1157
- /* @__PURE__ */ jsx18(
2916
+ files.length > 0 && /* @__PURE__ */ jsx35("ul", { className: "fractal-file-dropzone__files", "aria-label": "Attached files", children: files.map((file, index) => /* @__PURE__ */ jsxs29("li", { className: "fractal-file-dropzone__file", children: [
2917
+ /* @__PURE__ */ jsx35("span", { className: "fractal-file-dropzone__file-name", children: file.name }),
2918
+ /* @__PURE__ */ jsx35("span", { className: "fractal-file-dropzone__file-meta", children: formatFileSize(file.size) }),
2919
+ /* @__PURE__ */ jsx35(
1158
2920
  "button",
1159
2921
  {
1160
2922
  className: "fractal-file-dropzone__remove",
@@ -1162,7 +2924,7 @@ function FileDropzone({
1162
2924
  "aria-label": `Remove ${file.name}`,
1163
2925
  disabled,
1164
2926
  onClick: () => onFilesChange(files.filter((_, fileIndex) => fileIndex !== index)),
1165
- children: /* @__PURE__ */ jsx18(Close2, { "aria-hidden": "true", size: 14 })
2927
+ children: /* @__PURE__ */ jsx35(Close4, { "aria-hidden": "true", size: 14 })
1166
2928
  }
1167
2929
  )
1168
2930
  ] }, `${file.name}-${file.size}-${index}`)) })
@@ -1173,42 +2935,189 @@ function formatFileSize(bytes) {
1173
2935
  if (bytes < 1024 * 1024) return `${Math.round(bytes / 1024)} KB`;
1174
2936
  return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
1175
2937
  }
2938
+
2939
+ // src/components/Pagination/Pagination.tsx
2940
+ import { ArrowLeft, ArrowRight } from "@mirror-physics/fractal-icons";
2941
+ import { jsx as jsx36, jsxs as jsxs30 } from "react/jsx-runtime";
2942
+ function Pagination({
2943
+ page,
2944
+ pageSize,
2945
+ totalItems,
2946
+ onPageChange,
2947
+ pageSizeOptions,
2948
+ onPageSizeChange,
2949
+ ariaLabel = "Pagination",
2950
+ className
2951
+ }) {
2952
+ const totalPages = Math.max(1, Math.ceil(totalItems / pageSize));
2953
+ const currentPage = Math.min(Math.max(page, 1), totalPages);
2954
+ const start = totalItems === 0 ? 0 : (currentPage - 1) * pageSize + 1;
2955
+ const end = Math.min(currentPage * pageSize, totalItems);
2956
+ const canChangePageSize = pageSizeOptions != null && pageSizeOptions.length > 0 && onPageSizeChange != null;
2957
+ return /* @__PURE__ */ jsxs30("nav", { className: cn("fractal-pagination", className), "aria-label": ariaLabel, children: [
2958
+ /* @__PURE__ */ jsx36("span", { className: "fractal-pagination__summary", children: totalItems === 0 ? "No results" : `${start}-${end} of ${totalItems}` }),
2959
+ /* @__PURE__ */ jsxs30("div", { className: "fractal-pagination__controls", children: [
2960
+ canChangePageSize && /* @__PURE__ */ jsx36(
2961
+ Dropdown,
2962
+ {
2963
+ align: "right",
2964
+ className: "fractal-pagination__page-size",
2965
+ direction: "up",
2966
+ items: pageSizeOptions.map((option) => ({
2967
+ value: String(option),
2968
+ description: `${option} per page`
2969
+ })),
2970
+ onSelect: (value) => onPageSizeChange(Number(value)),
2971
+ selectedValue: String(pageSize),
2972
+ size: "sm",
2973
+ triggerLabel: "Results per page",
2974
+ triggerVariant: "tertiary"
2975
+ }
2976
+ ),
2977
+ /* @__PURE__ */ jsx36(
2978
+ "button",
2979
+ {
2980
+ className: "fractal-pagination__button",
2981
+ type: "button",
2982
+ "aria-label": "Previous page",
2983
+ title: "Previous page",
2984
+ disabled: currentPage === 1 || totalItems === 0,
2985
+ onClick: () => onPageChange(currentPage - 1),
2986
+ children: /* @__PURE__ */ jsx36(ArrowLeft, { "aria-hidden": "true", size: 16 })
2987
+ }
2988
+ ),
2989
+ /* @__PURE__ */ jsxs30("span", { className: "fractal-pagination__page", "aria-current": "page", children: [
2990
+ currentPage,
2991
+ " of ",
2992
+ totalPages
2993
+ ] }),
2994
+ /* @__PURE__ */ jsx36(
2995
+ "button",
2996
+ {
2997
+ className: "fractal-pagination__button",
2998
+ type: "button",
2999
+ "aria-label": "Next page",
3000
+ title: "Next page",
3001
+ disabled: currentPage === totalPages || totalItems === 0,
3002
+ onClick: () => onPageChange(currentPage + 1),
3003
+ children: /* @__PURE__ */ jsx36(ArrowRight, { "aria-hidden": "true", size: 16 })
3004
+ }
3005
+ )
3006
+ ] })
3007
+ ] });
3008
+ }
3009
+
3010
+ // src/components/Tooltip/Tooltip.tsx
3011
+ import * as TooltipPrimitive from "@radix-ui/react-tooltip";
3012
+ import { jsx as jsx37, jsxs as jsxs31 } from "react/jsx-runtime";
3013
+ function Tooltip({
3014
+ content,
3015
+ children,
3016
+ placement = "bottom",
3017
+ maxWidth = 320,
3018
+ delay = 0,
3019
+ disabled = false,
3020
+ className
3021
+ }) {
3022
+ if (disabled || content == null || content === "") return children;
3023
+ return /* @__PURE__ */ jsx37(TooltipPrimitive.Provider, { delayDuration: delay, children: /* @__PURE__ */ jsxs31(TooltipPrimitive.Root, { children: [
3024
+ /* @__PURE__ */ jsx37(TooltipPrimitive.Trigger, { asChild: true, children }),
3025
+ /* @__PURE__ */ jsx37(TooltipPrimitive.Portal, { children: /* @__PURE__ */ jsxs31(
3026
+ TooltipPrimitive.Content,
3027
+ {
3028
+ side: placement,
3029
+ sideOffset: 8,
3030
+ collisionPadding: 8,
3031
+ className: cn("fractal-tooltip", className),
3032
+ style: { "--tooltip-max-width": typeof maxWidth === "number" ? `${maxWidth}px` : maxWidth },
3033
+ children: [
3034
+ content,
3035
+ /* @__PURE__ */ jsx37(TooltipPrimitive.Arrow, { className: "fractal-tooltip-arrow", width: 8, height: 4, "aria-hidden": "true" })
3036
+ ]
3037
+ }
3038
+ ) })
3039
+ ] }) });
3040
+ }
1176
3041
  export {
1177
3042
  Alert,
1178
3043
  AlertDescription,
3044
+ AlertGhost,
1179
3045
  AlertTitle,
3046
+ Breadcrumbs,
1180
3047
  Button,
3048
+ ButtonGhost,
1181
3049
  Card,
1182
3050
  CardContent,
1183
3051
  CardDescription,
1184
3052
  CardFooter,
3053
+ CardGhost,
1185
3054
  CardHeader,
1186
3055
  CardTitle,
3056
+ Checkbox,
3057
+ CheckboxGhost,
1187
3058
  Chip,
3059
+ ChipGhost,
3060
+ CodeBlock,
3061
+ ColumnEditor,
3062
+ ContentSwitcher,
3063
+ ContentSwitcherGhost,
3064
+ ConversationGhost,
1188
3065
  DataTable,
3066
+ DataTableGhost,
1189
3067
  Disclosure,
3068
+ DisclosureGhost,
1190
3069
  Dropdown,
3070
+ DropdownGhost,
1191
3071
  FeatureCard,
1192
3072
  FileDropzone,
3073
+ FileDropzoneGhost,
1193
3074
  FolderCard,
3075
+ Ghost,
3076
+ GhostLine,
1194
3077
  Input,
3078
+ InputGhost,
1195
3079
  Label,
3080
+ LabelGhost,
3081
+ LatticeLoader,
1196
3082
  Link,
3083
+ LinkGhost,
1197
3084
  Modal,
3085
+ ModalGhost,
3086
+ NumberInput,
3087
+ Pagination,
3088
+ PaginationGhost,
3089
+ PasswordInput,
1198
3090
  PromptCard,
3091
+ PromptCardGhost,
1199
3092
  PromptGrid,
3093
+ Radio,
1200
3094
  SidePanel,
3095
+ SidePanelGhost,
1201
3096
  Sidebar,
3097
+ SidebarAccountMenu,
1202
3098
  SidebarBrand,
1203
3099
  SidebarFooter,
3100
+ SidebarGhost,
1204
3101
  SidebarHeader,
1205
3102
  SidebarNav,
1206
3103
  SidebarNavItem,
1207
3104
  SidebarSection,
3105
+ Slider,
1208
3106
  SortableTable,
3107
+ SortableTableGhost,
3108
+ TableSearch,
1209
3109
  Tabs,
3110
+ TabsGhost,
3111
+ TextButton,
3112
+ TextButtonGhost,
1210
3113
  Textarea,
3114
+ TextareaGhost,
3115
+ Toast,
3116
+ ToastViewport,
1211
3117
  Toggle,
3118
+ ToggleGhost,
3119
+ Tooltip,
3120
+ UILoader,
1212
3121
  cn,
1213
3122
  useEscapeDismiss
1214
3123
  };