@mirror-physics/fractal-ui 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,1066 @@
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
+ }
9
+
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)"
21
+ },
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)"
30
+ },
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)"
39
+ },
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: "38px", padding: "8px 16px", iconOnly: "38px", 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";
71
+ }
72
+ if (disabled) {
73
+ if (buttonType === "ghost") {
74
+ return { ...base, backgroundColor: "transparent", border: "none", color: "var(--fg-disabled)" };
75
+ }
76
+ return { ...base, backgroundColor: "transparent", border: "1px solid var(--fg-disabled)", color: "var(--fg-disabled)" };
77
+ }
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;
122
+ }
123
+ }
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
+ ]
162
+ }
163
+ );
164
+ }
165
+ );
166
+ Button.displayName = "Button";
167
+
168
+ // src/components/Card/Card.tsx
169
+ import * as React2 from "react";
170
+ 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 })
174
+ );
175
+ Card.displayName = "Card";
176
+ var CardHeader = React2.forwardRef(
177
+ ({ className, ...props }, ref) => /* @__PURE__ */ jsx("div", { ref, className: cn("fractal-card-header", className), ...props })
178
+ );
179
+ CardHeader.displayName = "CardHeader";
180
+ var CardTitle = React2.forwardRef(
181
+ ({ className, ...props }, ref) => /* @__PURE__ */ jsx("h3", { ref, className: cn("fractal-card-title", className), ...props })
182
+ );
183
+ CardTitle.displayName = "CardTitle";
184
+ var CardDescription = React2.forwardRef(
185
+ ({ className, ...props }, ref) => /* @__PURE__ */ jsx("p", { ref, className: cn("fractal-card-description", className), ...props })
186
+ );
187
+ CardDescription.displayName = "CardDescription";
188
+ var CardContent = React2.forwardRef(
189
+ ({ className, ...props }, ref) => /* @__PURE__ */ jsx("div", { ref, className: cn("fractal-card-content", className), ...props })
190
+ );
191
+ CardContent.displayName = "CardContent";
192
+ var CardFooter = React2.forwardRef(
193
+ ({ className, ...props }, ref) => /* @__PURE__ */ jsx("div", { ref, className: cn("fractal-card-footer", className), ...props })
194
+ );
195
+ 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 })
201
+ ] })
202
+ );
203
+ FeatureCard.displayName = "FeatureCard";
204
+ var FolderCard = React2.forwardRef(
205
+ ({ name, sessionCount, selected, className, onClick, ...props }, ref) => /* @__PURE__ */ jsxs2(
206
+ "div",
207
+ {
208
+ ref,
209
+ role: "button",
210
+ tabIndex: 0,
211
+ onClick,
212
+ onKeyDown: (e) => {
213
+ if (e.key === "Enter" || e.key === " ") {
214
+ e.preventDefault();
215
+ onClick?.(e);
216
+ }
217
+ },
218
+ className: cn("fractal-folder-card", selected && "fractal-folder-card--selected", className),
219
+ ...props,
220
+ 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 })
224
+ ] }),
225
+ sessionCount !== void 0 && /* @__PURE__ */ jsx("div", { className: "fractal-folder-card__footer", children: /* @__PURE__ */ jsxs2("span", { className: "fractal-folder-card__sessions", children: [
226
+ sessionCount,
227
+ " ",
228
+ sessionCount === 1 ? "session" : "sessions"
229
+ ] }) })
230
+ ]
231
+ }
232
+ )
233
+ );
234
+ FolderCard.displayName = "FolderCard";
235
+
236
+ // src/components/PromptCard/PromptCard.tsx
237
+ import * as React3 from "react";
238
+ import { jsx as jsx2, jsxs as jsxs3 } from "react/jsx-runtime";
239
+ var titleToPrompt = (title) => typeof title === "string" || typeof title === "number" ? String(title) : "";
240
+ var PromptCard = React3.forwardRef(
241
+ ({
242
+ title,
243
+ icon,
244
+ prompt,
245
+ tone = "blue",
246
+ highlighted,
247
+ disabled,
248
+ className,
249
+ onClick,
250
+ onPromptSelect,
251
+ ...props
252
+ }, ref) => {
253
+ const handleClick = (event) => {
254
+ onClick?.(event);
255
+ if (!event.defaultPrevented) {
256
+ onPromptSelect?.(prompt ?? titleToPrompt(title));
257
+ }
258
+ };
259
+ return /* @__PURE__ */ jsxs3(
260
+ "button",
261
+ {
262
+ ref,
263
+ type: "button",
264
+ disabled,
265
+ className: cn(
266
+ "fractal-prompt-card",
267
+ `fractal-prompt-card--${tone}`,
268
+ highlighted && "fractal-prompt-card--highlighted",
269
+ className
270
+ ),
271
+ onClick: handleClick,
272
+ ...props,
273
+ 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 })
276
+ ]
277
+ }
278
+ );
279
+ }
280
+ );
281
+ PromptCard.displayName = "PromptCard";
282
+ var PromptGrid = React3.forwardRef(
283
+ ({ compact, padded, className, ...props }, ref) => /* @__PURE__ */ jsx2(
284
+ "div",
285
+ {
286
+ ref,
287
+ className: cn(
288
+ "fractal-prompt-grid",
289
+ compact && "fractal-prompt-grid--compact",
290
+ padded && "fractal-prompt-grid--padded",
291
+ className
292
+ ),
293
+ ...props
294
+ }
295
+ )
296
+ );
297
+ PromptGrid.displayName = "PromptGrid";
298
+
299
+ // src/components/Input/Input.tsx
300
+ 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",
305
+ {
306
+ type,
307
+ ref,
308
+ className: cn("fractal-input", className),
309
+ ...props
310
+ }
311
+ )
312
+ );
313
+ Input.displayName = "Input";
314
+
315
+ // 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(
320
+ "label",
321
+ {
322
+ ref,
323
+ className: cn("fractal-label", className),
324
+ ...props
325
+ }
326
+ )
327
+ );
328
+ Label.displayName = "Label";
329
+
330
+ // src/components/Alert/Alert.tsx
331
+ import * as React6 from "react";
332
+ import { jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
333
+ var defaultIcons = {
334
+ default: "\u2139",
335
+ info: "\u2139",
336
+ success: "\u2713",
337
+ warning: "\u26A0",
338
+ error: "\u2715"
339
+ };
340
+ var Alert = React6.forwardRef(
341
+ ({ className, variant = "default", icon, children, ...props }, ref) => /* @__PURE__ */ jsxs4(
342
+ "div",
343
+ {
344
+ ref,
345
+ role: "alert",
346
+ className: cn("fractal-alert", `fractal-alert--${variant}`, className),
347
+ ...props,
348
+ 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 })
351
+ ]
352
+ }
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";
364
+
365
+ // src/components/Toggle/Toggle.tsx
366
+ import { jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
367
+ function Toggle({ checked, onChange, label, className }) {
368
+ return /* @__PURE__ */ jsxs5(
369
+ "button",
370
+ {
371
+ type: "button",
372
+ role: "switch",
373
+ "aria-checked": checked,
374
+ className: cn("fractal-toggle", className),
375
+ onClick: () => onChange(!checked),
376
+ 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 })
379
+ ]
380
+ }
381
+ );
382
+ }
383
+
384
+ // src/components/Modal/Modal.tsx
385
+ import { createPortal } from "react-dom";
386
+
387
+ // src/hooks/useEscapeDismiss.ts
388
+ import { useEffect, useRef } from "react";
389
+ var nextId = 0;
390
+ var stack = [];
391
+ function insertSorted(entry) {
392
+ let i = 0;
393
+ while (i < stack.length && stack[i].priority >= entry.priority) {
394
+ if (stack[i].priority === entry.priority) break;
395
+ i++;
396
+ }
397
+ stack.splice(i, 0, entry);
398
+ }
399
+ function remove(id) {
400
+ const idx = stack.findIndex((e) => e.id === id);
401
+ if (idx >= 0) stack.splice(idx, 1);
402
+ }
403
+ var listenerInstalled = false;
404
+ function ensureGlobalListener() {
405
+ if (listenerInstalled) return;
406
+ listenerInstalled = true;
407
+ window.addEventListener(
408
+ "keydown",
409
+ (e) => {
410
+ if (e.key !== "Escape") return;
411
+ if (stack.length === 0) return;
412
+ e.preventDefault();
413
+ e.stopImmediatePropagation();
414
+ const top = stack[0];
415
+ top.handler();
416
+ },
417
+ { capture: true }
418
+ );
419
+ }
420
+ function useEscapeDismiss(priority, handler, enabled = true) {
421
+ const handlerRef = useRef(handler);
422
+ handlerRef.current = handler;
423
+ useEffect(() => {
424
+ if (!enabled) return;
425
+ ensureGlobalListener();
426
+ const id = nextId++;
427
+ const entry = {
428
+ id,
429
+ priority,
430
+ handler: () => handlerRef.current()
431
+ };
432
+ insertSorted(entry);
433
+ return () => remove(id);
434
+ }, [priority, enabled]);
435
+ }
436
+
437
+ // 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);
442
+ if (!open) return null;
443
+ const sizeClass = `fractal-modal-panel--${size}`;
444
+ 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(
447
+ "div",
448
+ {
449
+ className: cn("fractal-modal-panel", sizeClass, accentClass, className),
450
+ onClick: (e) => e.stopPropagation(),
451
+ 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 })
458
+ ]
459
+ }
460
+ ) }),
461
+ document.body
462
+ );
463
+ }
464
+
465
+ // 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(
473
+ "tr",
474
+ {
475
+ 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))
477
+ },
478
+ rowIndex
479
+ )) })
480
+ ] })
481
+ ] }) });
482
+ }
483
+
484
+ // 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;
489
+ function Dropdown({
490
+ items,
491
+ selectedValue,
492
+ onSelect,
493
+ triggerLabel = "Options",
494
+ direction = "down",
495
+ align = "left",
496
+ triggerContent,
497
+ label,
498
+ noChevron = false,
499
+ size = "md",
500
+ triggerVariant = "outline",
501
+ className,
502
+ triggerClassName,
503
+ panelClassName,
504
+ closeOnSelect = true
505
+ }) {
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(
511
+ (item) => item.icon != null || item.description != null && (typeof item.description === "string" ? item.description !== "" : true)
512
+ );
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;
531
+ }
532
+ const top = placeAbove ? -(panelRect.height + MARGIN) : triggerRect.height + MARGIN;
533
+ let left;
534
+ let right;
535
+ if (align === "right") {
536
+ right = 0;
537
+ const panelLeft = triggerRect.right - panelRect.width;
538
+ if (panelLeft < 0) {
539
+ right = void 0;
540
+ left = 0;
541
+ }
542
+ } else {
543
+ left = 0;
544
+ const panelRight = triggerRect.left + panelRect.width;
545
+ if (panelRight > vw) {
546
+ left = void 0;
547
+ right = 0;
548
+ }
549
+ }
550
+ setPanelStyle({ top, left, right });
551
+ }, [open, isUp, align]);
552
+ React7.useEffect(() => {
553
+ if (!open) return;
554
+ const handleClickOutside = (e) => {
555
+ const target = e.target;
556
+ if (triggerRef.current?.contains(target) || panelRef.current?.contains(target)) return;
557
+ setOpen(false);
558
+ };
559
+ document.addEventListener("mousedown", handleClickOutside);
560
+ return () => document.removeEventListener("mousedown", handleClickOutside);
561
+ }, [open]);
562
+ return /* @__PURE__ */ jsxs8("div", { className: cn("fractal-dropdown", className), children: [
563
+ /* @__PURE__ */ jsxs8(
564
+ "button",
565
+ {
566
+ ref: triggerRef,
567
+ type: "button",
568
+ "aria-haspopup": "listbox",
569
+ "aria-expanded": open,
570
+ "aria-label": triggerLabel,
571
+ onClick: () => setOpen((o) => !o),
572
+ className: cn(
573
+ "fractal-dropdown-trigger",
574
+ `fractal-dropdown-trigger--${size}`,
575
+ `fractal-dropdown-trigger--${triggerVariant}`,
576
+ triggerClassName
577
+ ),
578
+ 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 }) })
584
+ ]
585
+ }
586
+ ),
587
+ open && /* @__PURE__ */ jsxs8(
588
+ "div",
589
+ {
590
+ 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
+ })
631
+ ]
632
+ }
633
+ )
634
+ ] });
635
+ }
636
+
637
+ // 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(
642
+ "a",
643
+ {
644
+ ref,
645
+ className: cn("fractal-link", `fractal-link--${theme}`, `fractal-link--${size}`, className),
646
+ ...props
647
+ }
648
+ )
649
+ );
650
+ Link.displayName = "Link";
651
+
652
+ // 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(
657
+ "span",
658
+ {
659
+ ref,
660
+ className: cn("fractal-chip", `fractal-chip--${tone}`, className),
661
+ ...props
662
+ }
663
+ )
664
+ );
665
+ Chip.displayName = "Chip";
666
+
667
+ // src/components/SortableTable/SortableTable.tsx
668
+ import { useMemo, useState as useState3 } from "react";
669
+ import { ArrowUp, ArrowDown, ArrowsUpDown } from "@mirror-physics/fractal-icons";
670
+ import { jsx as jsx12, jsxs as jsxs9 } from "react/jsx-runtime";
671
+ function SortableTable({
672
+ columns,
673
+ data,
674
+ emptyMessage,
675
+ header,
676
+ size = "md",
677
+ className,
678
+ defaultSortKey = void 0,
679
+ defaultSortDirection = null,
680
+ sortKey: controlledKey,
681
+ sortDirection: controlledDirection,
682
+ onSortChange,
683
+ highlightRow
684
+ }) {
685
+ const [internalKey, setInternalKey] = useState3(defaultSortKey ?? null);
686
+ const [internalDirection, setInternalDirection] = useState3(defaultSortDirection);
687
+ const isControlled = controlledKey !== void 0 && controlledDirection !== void 0;
688
+ const activeKey = isControlled ? controlledKey : internalKey;
689
+ const activeDirection = isControlled ? controlledDirection : internalDirection;
690
+ const sorted = useMemo(() => {
691
+ if (!activeKey || !activeDirection) return data;
692
+ const col = columns.find((c) => c.key === activeKey);
693
+ if (!col) return data;
694
+ const accessor = col.sortAccessor ?? ((row) => {
695
+ const value = row[activeKey];
696
+ return typeof value === "string" || typeof value === "number" ? value : null;
697
+ });
698
+ const copy = [...data];
699
+ copy.sort((a, b) => {
700
+ const av = accessor(a);
701
+ const bv = accessor(b);
702
+ if (av == null && bv == null) return 0;
703
+ if (av == null) return 1;
704
+ if (bv == null) return -1;
705
+ if (typeof av === "number" && typeof bv === "number") {
706
+ return activeDirection === "asc" ? av - bv : bv - av;
707
+ }
708
+ const as = String(av).toLowerCase();
709
+ const bs = String(bv).toLowerCase();
710
+ if (as < bs) return activeDirection === "asc" ? -1 : 1;
711
+ if (as > bs) return activeDirection === "asc" ? 1 : -1;
712
+ return 0;
713
+ });
714
+ return copy;
715
+ }, [data, columns, activeKey, activeDirection]);
716
+ const handleSortClick = (key) => {
717
+ let nextDirection;
718
+ if (activeKey !== key) {
719
+ nextDirection = "asc";
720
+ } else if (activeDirection === "asc") {
721
+ nextDirection = "desc";
722
+ } else if (activeDirection === "desc") {
723
+ nextDirection = null;
724
+ } else {
725
+ nextDirection = "asc";
726
+ }
727
+ const nextKey = nextDirection === null ? null : key;
728
+ if (isControlled) {
729
+ onSortChange?.(nextKey, nextDirection);
730
+ } else {
731
+ setInternalKey(nextKey);
732
+ setInternalDirection(nextDirection);
733
+ onSortChange?.(nextKey, nextDirection);
734
+ }
735
+ };
736
+ return /* @__PURE__ */ jsx12("div", { className: cn("fractal-sortable-table", `fractal-sortable-table--${size}`, className), children: /* @__PURE__ */ jsxs9("div", { className: "fractal-sortable-table-surface", children: [
737
+ header && /* @__PURE__ */ jsx12("div", { className: "fractal-sortable-table-header", children: header }),
738
+ sorted.length === 0 ? /* @__PURE__ */ jsx12("div", { className: "fractal-sortable-table-empty", children: emptyMessage || "No data" }) : /* @__PURE__ */ jsxs9("table", { className: "fractal-sortable-table-table", children: [
739
+ /* @__PURE__ */ jsx12("thead", { children: /* @__PURE__ */ jsx12("tr", { className: "fractal-sortable-table-thead-row", children: columns.map((col) => {
740
+ const isActive = activeKey === col.key && activeDirection !== null;
741
+ if (!col.sortable) {
742
+ return /* @__PURE__ */ jsx12(
743
+ "th",
744
+ {
745
+ className: "fractal-sortable-table-th",
746
+ style: { width: col.width },
747
+ children: col.header
748
+ },
749
+ col.key
750
+ );
751
+ }
752
+ return /* @__PURE__ */ jsx12(
753
+ "th",
754
+ {
755
+ className: "fractal-sortable-table-th fractal-sortable-table-th--sortable",
756
+ style: { width: col.width },
757
+ children: /* @__PURE__ */ jsxs9(
758
+ "button",
759
+ {
760
+ type: "button",
761
+ className: cn(
762
+ "fractal-sortable-table-sort-button",
763
+ isActive && "fractal-sortable-table-sort-button--active"
764
+ ),
765
+ onClick: () => handleSortClick(col.key),
766
+ "aria-sort": isActive ? activeDirection === "asc" ? "ascending" : "descending" : "none",
767
+ children: [
768
+ /* @__PURE__ */ jsx12("span", { children: col.header }),
769
+ /* @__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 }) })
770
+ ]
771
+ }
772
+ )
773
+ },
774
+ col.key
775
+ );
776
+ }) }) }),
777
+ /* @__PURE__ */ jsx12("tbody", { children: sorted.map((row, rowIndex) => {
778
+ const highlighted = highlightRow?.(row, rowIndex) ?? false;
779
+ return /* @__PURE__ */ jsx12(
780
+ "tr",
781
+ {
782
+ className: cn(
783
+ "fractal-sortable-table-row",
784
+ highlighted && "fractal-sortable-table-row--highlighted"
785
+ ),
786
+ children: columns.map((col) => /* @__PURE__ */ jsx12(
787
+ "td",
788
+ {
789
+ className: "fractal-sortable-table-td",
790
+ style: { width: col.width, textAlign: col.align ?? "left" },
791
+ children: col.render(row, rowIndex)
792
+ },
793
+ col.key
794
+ ))
795
+ },
796
+ rowIndex
797
+ );
798
+ }) })
799
+ ] })
800
+ ] }) });
801
+ }
802
+
803
+ // src/components/Tabs/Tabs.tsx
804
+ import { useId, useRef as useRef3, useState as useState4 } from "react";
805
+ import { jsx as jsx13, jsxs as jsxs10 } from "react/jsx-runtime";
806
+ function Tabs({
807
+ items,
808
+ defaultValue,
809
+ value: controlledValue,
810
+ onValueChange,
811
+ className,
812
+ ariaLabel
813
+ }) {
814
+ const reactId = useId();
815
+ const [internalValue, setInternalValue] = useState4(
816
+ defaultValue ?? items.find((i) => !i.disabled)?.id ?? items[0]?.id ?? ""
817
+ );
818
+ const isControlled = controlledValue !== void 0;
819
+ const activeId = isControlled ? controlledValue : internalValue;
820
+ const tabRefs = useRef3({});
821
+ const selectTab = (id) => {
822
+ if (isControlled) {
823
+ onValueChange?.(id);
824
+ } else {
825
+ setInternalValue(id);
826
+ onValueChange?.(id);
827
+ }
828
+ };
829
+ const focusable = items.filter((i) => !i.disabled);
830
+ const onKeyDown = (event) => {
831
+ const currentIndex = focusable.findIndex((i) => i.id === activeId);
832
+ if (currentIndex === -1) return;
833
+ let nextIndex = currentIndex;
834
+ switch (event.key) {
835
+ case "ArrowRight":
836
+ nextIndex = (currentIndex + 1) % focusable.length;
837
+ break;
838
+ case "ArrowLeft":
839
+ nextIndex = (currentIndex - 1 + focusable.length) % focusable.length;
840
+ break;
841
+ case "Home":
842
+ nextIndex = 0;
843
+ break;
844
+ case "End":
845
+ nextIndex = focusable.length - 1;
846
+ break;
847
+ default:
848
+ return;
849
+ }
850
+ event.preventDefault();
851
+ const nextId2 = focusable[nextIndex].id;
852
+ selectTab(nextId2);
853
+ requestAnimationFrame(() => tabRefs.current[nextId2]?.focus());
854
+ };
855
+ return /* @__PURE__ */ jsxs10("div", { className: cn("fractal-tabs", className), children: [
856
+ /* @__PURE__ */ jsx13("div", { role: "tablist", "aria-label": ariaLabel, className: "fractal-tabs-list", children: items.map((item) => {
857
+ const isActive = item.id === activeId;
858
+ const tabId = `${reactId}-tab-${item.id}`;
859
+ const panelId = `${reactId}-panel-${item.id}`;
860
+ return /* @__PURE__ */ jsx13(
861
+ "button",
862
+ {
863
+ ref: (el) => {
864
+ tabRefs.current[item.id] = el;
865
+ },
866
+ id: tabId,
867
+ role: "tab",
868
+ type: "button",
869
+ "aria-selected": isActive,
870
+ "aria-controls": panelId,
871
+ tabIndex: isActive ? 0 : -1,
872
+ disabled: item.disabled,
873
+ className: cn(
874
+ "fractal-tabs-trigger",
875
+ isActive && "fractal-tabs-trigger--active",
876
+ item.disabled && "fractal-tabs-trigger--disabled"
877
+ ),
878
+ onClick: () => !item.disabled && selectTab(item.id),
879
+ onKeyDown,
880
+ children: item.label
881
+ },
882
+ item.id
883
+ );
884
+ }) }),
885
+ items.map((item) => {
886
+ const tabId = `${reactId}-tab-${item.id}`;
887
+ const panelId = `${reactId}-panel-${item.id}`;
888
+ const isActive = item.id === activeId;
889
+ return /* @__PURE__ */ jsx13(
890
+ "div",
891
+ {
892
+ id: panelId,
893
+ role: "tabpanel",
894
+ "aria-labelledby": tabId,
895
+ hidden: !isActive,
896
+ className: "fractal-tabs-panel",
897
+ children: isActive && item.content
898
+ },
899
+ item.id
900
+ );
901
+ })
902
+ ] });
903
+ }
904
+
905
+ // src/components/Disclosure/Disclosure.tsx
906
+ import { useId as useId2, useState as useState5 } from "react";
907
+ import { ChevronRight, ChevronDown as ChevronDown2 } from "@mirror-physics/fractal-icons";
908
+ import { jsx as jsx14, jsxs as jsxs11 } from "react/jsx-runtime";
909
+ function Disclosure({
910
+ title,
911
+ meta,
912
+ children,
913
+ defaultOpen = false,
914
+ open: controlledOpen,
915
+ onOpenChange,
916
+ className,
917
+ variant = "card"
918
+ }) {
919
+ const reactId = useId2();
920
+ const headerId = `${reactId}-header`;
921
+ const panelId = `${reactId}-panel`;
922
+ const [internalOpen, setInternalOpen] = useState5(defaultOpen);
923
+ const isControlled = controlledOpen !== void 0;
924
+ const isOpen = isControlled ? controlledOpen : internalOpen;
925
+ const toggle = () => {
926
+ const next = !isOpen;
927
+ if (isControlled) {
928
+ onOpenChange?.(next);
929
+ } else {
930
+ setInternalOpen(next);
931
+ onOpenChange?.(next);
932
+ }
933
+ };
934
+ return /* @__PURE__ */ jsxs11(
935
+ "div",
936
+ {
937
+ className: cn(
938
+ "fractal-disclosure",
939
+ variant === "card" && "fractal-disclosure--card",
940
+ isOpen && "fractal-disclosure--open",
941
+ className
942
+ ),
943
+ children: [
944
+ /* @__PURE__ */ jsxs11(
945
+ "button",
946
+ {
947
+ type: "button",
948
+ id: headerId,
949
+ className: "fractal-disclosure-trigger",
950
+ "aria-expanded": isOpen,
951
+ "aria-controls": panelId,
952
+ onClick: toggle,
953
+ children: [
954
+ /* @__PURE__ */ jsx14("span", { className: "fractal-disclosure-icon", "aria-hidden": "true", children: isOpen ? /* @__PURE__ */ jsx14(ChevronDown2, { size: 16 }) : /* @__PURE__ */ jsx14(ChevronRight, { size: 16 }) }),
955
+ /* @__PURE__ */ jsx14("span", { className: "fractal-disclosure-title", children: title }),
956
+ meta && /* @__PURE__ */ jsx14("span", { className: "fractal-disclosure-meta", children: meta })
957
+ ]
958
+ }
959
+ ),
960
+ /* @__PURE__ */ jsx14(
961
+ "div",
962
+ {
963
+ id: panelId,
964
+ role: "region",
965
+ "aria-labelledby": headerId,
966
+ className: "fractal-disclosure-panel",
967
+ hidden: !isOpen,
968
+ children: /* @__PURE__ */ jsx14("div", { className: "fractal-disclosure-panel-inner", children })
969
+ }
970
+ )
971
+ ]
972
+ }
973
+ );
974
+ }
975
+
976
+ // src/components/Sidebar/Sidebar.tsx
977
+ import * as React10 from "react";
978
+ import { jsx as jsx15, jsxs as jsxs12 } from "react/jsx-runtime";
979
+ var Sidebar = React10.forwardRef(
980
+ ({ className, width, style, ...props }, ref) => /* @__PURE__ */ jsx15(
981
+ "aside",
982
+ {
983
+ ref,
984
+ className: cn("fractal-sidebar", className),
985
+ style: { ...style, ...width ? { "--fractal-sidebar-width": width } : {} },
986
+ ...props
987
+ }
988
+ )
989
+ );
990
+ Sidebar.displayName = "Sidebar";
991
+ var SidebarHeader = React10.forwardRef(
992
+ ({ className, ...props }, ref) => /* @__PURE__ */ jsx15("div", { ref, className: cn("fractal-sidebar__header", className), ...props })
993
+ );
994
+ SidebarHeader.displayName = "SidebarHeader";
995
+ var SidebarBrand = React10.forwardRef(
996
+ ({ className, type = "button", ...props }, ref) => /* @__PURE__ */ jsx15("button", { ref, type, className: cn("fractal-sidebar__brand", className), ...props })
997
+ );
998
+ SidebarBrand.displayName = "SidebarBrand";
999
+ var SidebarNav = React10.forwardRef(
1000
+ ({ className, ...props }, ref) => /* @__PURE__ */ jsx15("nav", { ref, className: cn("fractal-sidebar__nav", className), ...props })
1001
+ );
1002
+ SidebarNav.displayName = "SidebarNav";
1003
+ var SidebarSection = React10.forwardRef(
1004
+ ({ className, ...props }, ref) => /* @__PURE__ */ jsx15("div", { ref, className: cn("fractal-sidebar__section", className), ...props })
1005
+ );
1006
+ SidebarSection.displayName = "SidebarSection";
1007
+ var SidebarNavItem = React10.forwardRef(
1008
+ ({ active = false, icon, endAdornment, className, type = "button", children, ...props }, ref) => /* @__PURE__ */ jsxs12(
1009
+ "button",
1010
+ {
1011
+ ref,
1012
+ type,
1013
+ className: cn("fractal-sidebar__nav-item", className),
1014
+ "data-active": active || void 0,
1015
+ "aria-current": active ? "page" : void 0,
1016
+ ...props,
1017
+ children: [
1018
+ icon && /* @__PURE__ */ jsx15("span", { className: "fractal-sidebar__nav-item-icon", "aria-hidden": "true", children: icon }),
1019
+ /* @__PURE__ */ jsx15("span", { className: "fractal-sidebar__nav-item-label", children }),
1020
+ endAdornment && /* @__PURE__ */ jsx15("span", { className: "fractal-sidebar__nav-item-end", children: endAdornment })
1021
+ ]
1022
+ }
1023
+ )
1024
+ );
1025
+ SidebarNavItem.displayName = "SidebarNavItem";
1026
+ var SidebarFooter = React10.forwardRef(
1027
+ ({ className, ...props }, ref) => /* @__PURE__ */ jsx15("div", { ref, className: cn("fractal-sidebar__footer", className), ...props })
1028
+ );
1029
+ SidebarFooter.displayName = "SidebarFooter";
1030
+ export {
1031
+ Alert,
1032
+ AlertDescription,
1033
+ AlertTitle,
1034
+ Button,
1035
+ Card,
1036
+ CardContent,
1037
+ CardDescription,
1038
+ CardFooter,
1039
+ CardHeader,
1040
+ CardTitle,
1041
+ Chip,
1042
+ DataTable,
1043
+ Disclosure,
1044
+ Dropdown,
1045
+ FeatureCard,
1046
+ FolderCard,
1047
+ Input,
1048
+ Label,
1049
+ Link,
1050
+ Modal,
1051
+ PromptCard,
1052
+ PromptGrid,
1053
+ Sidebar,
1054
+ SidebarBrand,
1055
+ SidebarFooter,
1056
+ SidebarHeader,
1057
+ SidebarNav,
1058
+ SidebarNavItem,
1059
+ SidebarSection,
1060
+ SortableTable,
1061
+ Tabs,
1062
+ Toggle,
1063
+ cn,
1064
+ useEscapeDismiss
1065
+ };
1066
+ //# sourceMappingURL=index.js.map