@nimblebrain/synapse 0.7.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1366 @@
1
+ import { createContext, useRef, useEffect, useContext, useState, useLayoutEffect } from 'react';
2
+ import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
3
+
4
+ // src/ui/components/Avatar.tsx
5
+
6
+ // src/ui/tokens.ts
7
+ var tokens = {
8
+ // ── Surfaces ──
9
+ bg: "var(--color-background-primary, #ffffff)",
10
+ bgRaised: "var(--color-background-secondary, #fafafa)",
11
+ bgSubtle: "var(--color-background-tertiary, #f3f4f6)",
12
+ // ── Text ──
13
+ fg: "var(--color-text-primary, #111827)",
14
+ fgMuted: "var(--color-text-secondary, #6b7280)",
15
+ fgFaint: "var(--color-text-tertiary, #9ca3af)",
16
+ accent: "var(--color-text-accent, #2563eb)",
17
+ accentFg: "var(--nb-color-accent-foreground, #ffffff)",
18
+ // ── Border / ring ──
19
+ border: "var(--color-border-primary, #e5e7eb)",
20
+ borderStrong: "var(--color-border-secondary, #d1d5db)",
21
+ ring: "var(--color-ring-primary, #2563eb)",
22
+ // ── Status / brand semantics ──
23
+ danger: "var(--nb-color-danger, #dc2626)",
24
+ success: "var(--nb-color-success, #059669)",
25
+ warning: "var(--nb-color-warning, #f59e0b)",
26
+ warm: "var(--nb-color-warm, #d4620a)",
27
+ warmLight: "var(--nb-color-warm-light, #fef5ee)",
28
+ processing: "var(--nb-color-processing, #7c3aed)",
29
+ processingLight: "var(--nb-color-processing-light, #f3eeff)",
30
+ infoLight: "var(--nb-color-info-light, #eef4ff)",
31
+ // ── Typography ──
32
+ fontSans: "var(--font-sans, system-ui, -apple-system, BlinkMacSystemFont, sans-serif)",
33
+ fontMono: "var(--font-mono, ui-monospace, SFMono-Regular, Menlo, monospace)",
34
+ fontHeading: "var(--nb-font-heading, Georgia, 'Times New Roman', serif)",
35
+ weightNormal: "var(--font-weight-normal, 400)",
36
+ weightMedium: "var(--font-weight-medium, 500)",
37
+ weightSemibold: "var(--font-weight-semibold, 600)",
38
+ weightBold: "var(--font-weight-bold, 700)",
39
+ // ── Type scale (size + line-height pairs) ──
40
+ textXsSize: "var(--font-text-xs-size, 0.75rem)",
41
+ textXsLine: "var(--font-text-xs-line-height, 1rem)",
42
+ textSmSize: "var(--font-text-sm-size, 0.875rem)",
43
+ textSmLine: "var(--font-text-sm-line-height, 1.25rem)",
44
+ textBaseSize: "var(--font-text-base-size, 1rem)",
45
+ textBaseLine: "var(--font-text-base-line-height, 1.5rem)",
46
+ textLgSize: "var(--font-text-lg-size, 1.125rem)",
47
+ textLgLine: "var(--font-text-lg-line-height, 1.75rem)",
48
+ headingSmSize: "var(--font-heading-sm-size, 1.25rem)",
49
+ headingSmLine: "var(--font-heading-sm-line-height, 1.75rem)",
50
+ headingMdSize: "var(--font-heading-md-size, 1.5rem)",
51
+ headingMdLine: "var(--font-heading-md-line-height, 2rem)",
52
+ headingLgSize: "var(--font-heading-lg-size, 2rem)",
53
+ headingLgLine: "var(--font-heading-lg-line-height, 2.5rem)",
54
+ // ── Radius / border width ──
55
+ radiusXs: "var(--border-radius-xs, 0.25rem)",
56
+ radiusSm: "var(--border-radius-sm, 0.5rem)",
57
+ radiusMd: "var(--border-radius-md, 0.75rem)",
58
+ radiusLg: "var(--border-radius-lg, 1rem)",
59
+ radiusXl: "var(--border-radius-xl, 1.5rem)",
60
+ borderWidth: "var(--border-width-regular, 1px)",
61
+ // ── Shadows ──
62
+ shadowHairline: "var(--shadow-hairline, 0 0 0 1px rgba(0,0,0,0.06))",
63
+ shadowSm: "var(--shadow-sm, 0 1px 2px rgba(0,0,0,0.05))",
64
+ shadowMd: "var(--shadow-md, 0 4px 6px -1px rgba(0,0,0,0.1))",
65
+ shadowLg: "var(--shadow-lg, 0 10px 15px -3px rgba(0,0,0,0.1))"
66
+ };
67
+ var TEXT_SCALE = {
68
+ xs: { fontSize: tokens.textXsSize, lineHeight: tokens.textXsLine },
69
+ sm: { fontSize: tokens.textSmSize, lineHeight: tokens.textSmLine },
70
+ base: { fontSize: tokens.textBaseSize, lineHeight: tokens.textBaseLine },
71
+ lg: { fontSize: tokens.textLgSize, lineHeight: tokens.textLgLine }
72
+ };
73
+ var HEADING_SCALE = {
74
+ sm: { fontSize: tokens.headingSmSize, lineHeight: tokens.headingSmLine },
75
+ md: { fontSize: tokens.headingMdSize, lineHeight: tokens.headingMdLine },
76
+ lg: { fontSize: tokens.headingLgSize, lineHeight: tokens.headingLgLine }
77
+ };
78
+ function textStyle(size) {
79
+ return TEXT_SCALE[size];
80
+ }
81
+ function headingStyle(size) {
82
+ return HEADING_SCALE[size];
83
+ }
84
+ function initialsOf(name) {
85
+ const parts = name.trim().split(/\s+/).filter(Boolean);
86
+ if (parts.length === 0) return "?";
87
+ const first = parts[0] ?? "";
88
+ if (parts.length === 1) return first.charAt(0).toUpperCase();
89
+ const last = parts[parts.length - 1] ?? "";
90
+ return (first.charAt(0) + last.charAt(0)).toUpperCase();
91
+ }
92
+ function Avatar({ name, src, size = 28, style, ...rest }) {
93
+ const [failed, setFailed] = useState(false);
94
+ useEffect(() => setFailed(false), [src]);
95
+ const showImage = Boolean(src) && !failed;
96
+ return /* @__PURE__ */ jsx(
97
+ "span",
98
+ {
99
+ style: {
100
+ display: "inline-flex",
101
+ alignItems: "center",
102
+ justifyContent: "center",
103
+ flexShrink: 0,
104
+ width: size,
105
+ height: size,
106
+ borderRadius: "50%",
107
+ overflow: "hidden",
108
+ background: tokens.bgSubtle,
109
+ color: tokens.fgMuted,
110
+ fontFamily: tokens.fontSans,
111
+ fontSize: Math.round(size * 0.4),
112
+ fontWeight: tokens.weightSemibold,
113
+ userSelect: "none",
114
+ ...style
115
+ },
116
+ ...rest,
117
+ children: showImage ? /* @__PURE__ */ jsx(
118
+ "img",
119
+ {
120
+ src,
121
+ alt: name,
122
+ width: size,
123
+ height: size,
124
+ onError: () => setFailed(true),
125
+ style: { width: "100%", height: "100%", objectFit: "cover" }
126
+ }
127
+ ) : initialsOf(name)
128
+ }
129
+ );
130
+ }
131
+ var TONE = {
132
+ neutral: { bg: tokens.bgSubtle, fg: tokens.fgMuted },
133
+ accent: { bg: tokens.infoLight, fg: tokens.accent },
134
+ success: { bg: `color-mix(in oklab, ${tokens.success} 14%, transparent)`, fg: tokens.success },
135
+ warning: { bg: `color-mix(in oklab, ${tokens.warning} 14%, transparent)`, fg: tokens.warning },
136
+ danger: { bg: `color-mix(in oklab, ${tokens.danger} 14%, transparent)`, fg: tokens.danger },
137
+ processing: { bg: tokens.processingLight, fg: tokens.processing },
138
+ warm: { bg: tokens.warmLight, fg: tokens.warm }
139
+ };
140
+ function Badge({ tone = "neutral", style, children, ...rest }) {
141
+ const { bg, fg } = TONE[tone];
142
+ return /* @__PURE__ */ jsx(
143
+ "span",
144
+ {
145
+ style: {
146
+ display: "inline-flex",
147
+ alignItems: "center",
148
+ gap: "0.25rem",
149
+ padding: "0.1rem 0.45rem",
150
+ borderRadius: tokens.radiusXs,
151
+ background: bg,
152
+ color: fg,
153
+ fontFamily: tokens.fontSans,
154
+ fontSize: tokens.textXsSize,
155
+ lineHeight: tokens.textXsLine,
156
+ fontWeight: tokens.weightSemibold,
157
+ letterSpacing: "0.02em",
158
+ textTransform: "uppercase",
159
+ whiteSpace: "nowrap",
160
+ ...style
161
+ },
162
+ ...rest,
163
+ children
164
+ }
165
+ );
166
+ }
167
+
168
+ // src/ui/internal/inject-style.ts
169
+ function ensureStyle(id, css) {
170
+ if (typeof document === "undefined") return;
171
+ if (document.getElementById(id)) return;
172
+ const style = document.createElement("style");
173
+ style.id = id;
174
+ style.textContent = css;
175
+ document.head.appendChild(style);
176
+ }
177
+ var STYLE_ID = "nb-synapse-button";
178
+ var RULES = `
179
+ .nb-btn {
180
+ --nb-btn-bg: transparent;
181
+ --nb-btn-bg-hover: transparent;
182
+ display: inline-flex;
183
+ align-items: center;
184
+ justify-content: center;
185
+ gap: 0.4rem;
186
+ border: var(--nb-btn-border, none);
187
+ border-radius: var(--nb-btn-radius);
188
+ background: var(--nb-btn-bg);
189
+ color: var(--nb-btn-fg);
190
+ font-family: var(--nb-btn-font);
191
+ font-weight: var(--nb-btn-weight);
192
+ cursor: pointer;
193
+ transition: background 130ms ease, color 130ms ease, opacity 130ms ease;
194
+ white-space: nowrap;
195
+ }
196
+ .nb-btn:hover { background: var(--nb-btn-bg-hover); }
197
+ .nb-btn:active { transform: translateY(0.5px); }
198
+ .nb-btn:disabled { opacity: 0.5; cursor: default; }
199
+ .nb-btn:disabled:hover { background: var(--nb-btn-bg); }
200
+ .nb-textlink {
201
+ border: none;
202
+ background: none;
203
+ padding: 0;
204
+ cursor: pointer;
205
+ font-family: var(--nb-tl-font);
206
+ color: var(--nb-tl-fg);
207
+ transition: color 130ms ease, opacity 130ms ease;
208
+ }
209
+ .nb-textlink:hover { color: var(--nb-tl-fg-hover); }
210
+ .nb-textlink:disabled { opacity: 0.45; cursor: default; }
211
+ `;
212
+ var SIZE_PAD = {
213
+ sm: "0.3rem 0.6rem",
214
+ md: "0.45rem 0.85rem"
215
+ };
216
+ function Button({
217
+ variant = "primary",
218
+ size = "md",
219
+ style,
220
+ className,
221
+ children,
222
+ ...rest
223
+ }) {
224
+ ensureStyle(STYLE_ID, RULES);
225
+ const vars = {
226
+ "--nb-btn-radius": tokens.radiusSm,
227
+ "--nb-btn-font": tokens.fontSans,
228
+ "--nb-btn-weight": tokens.weightMedium
229
+ };
230
+ if (variant === "primary") {
231
+ vars["--nb-btn-bg"] = tokens.accent;
232
+ vars["--nb-btn-bg-hover"] = `color-mix(in oklab, ${tokens.accent} 88%, black)`;
233
+ vars["--nb-btn-fg"] = tokens.accentFg;
234
+ } else if (variant === "secondary") {
235
+ vars["--nb-btn-bg"] = tokens.bgSubtle;
236
+ vars["--nb-btn-bg-hover"] = tokens.bgRaised;
237
+ vars["--nb-btn-fg"] = tokens.fg;
238
+ vars["--nb-btn-border"] = `${tokens.borderWidth} solid ${tokens.border}`;
239
+ } else {
240
+ vars["--nb-btn-bg"] = "transparent";
241
+ vars["--nb-btn-bg-hover"] = tokens.bgSubtle;
242
+ vars["--nb-btn-fg"] = tokens.fg;
243
+ }
244
+ const btnStyle = {
245
+ ...vars,
246
+ padding: SIZE_PAD[size],
247
+ fontSize: size === "sm" ? tokens.textSmSize : tokens.textBaseSize,
248
+ lineHeight: size === "sm" ? tokens.textSmLine : tokens.textBaseLine,
249
+ ...style
250
+ };
251
+ return /* @__PURE__ */ jsx("button", { type: "button", className: `nb-btn ${className ?? ""}`.trim(), style: btnStyle, ...rest, children });
252
+ }
253
+ function TextLink({ tone = "muted", style, className, children, ...rest }) {
254
+ ensureStyle(STYLE_ID, RULES);
255
+ const resting = tone === "danger" ? tokens.danger : tone === "default" ? tokens.fg : tokens.fgMuted;
256
+ const hover = tone === "danger" ? tokens.danger : tokens.accent;
257
+ const linkStyle = {
258
+ "--nb-tl-font": tokens.fontSans,
259
+ "--nb-tl-fg": resting,
260
+ "--nb-tl-fg-hover": hover,
261
+ fontSize: tokens.textXsSize,
262
+ lineHeight: tokens.textXsLine,
263
+ letterSpacing: "0.02em",
264
+ ...style
265
+ };
266
+ return /* @__PURE__ */ jsx(
267
+ "button",
268
+ {
269
+ type: "button",
270
+ className: `nb-textlink ${className ?? ""}`.trim(),
271
+ style: linkStyle,
272
+ ...rest,
273
+ children
274
+ }
275
+ );
276
+ }
277
+ var STYLE_ID2 = "nb-synapse-card";
278
+ var RULES2 = `
279
+ .nb-card--interactive {
280
+ cursor: pointer;
281
+ transition: box-shadow 140ms ease, border-color 140ms ease;
282
+ }
283
+ .nb-card--interactive:hover {
284
+ box-shadow: var(--nb-card-shadow-hover);
285
+ border-color: var(--nb-card-border-hover);
286
+ }
287
+ `;
288
+ function Card({
289
+ padding = "0.875rem",
290
+ interactive = false,
291
+ style,
292
+ className,
293
+ children,
294
+ ...rest
295
+ }) {
296
+ if (interactive) ensureStyle(STYLE_ID2, RULES2);
297
+ const cardStyle = {
298
+ background: tokens.bgRaised,
299
+ border: `${tokens.borderWidth} solid ${tokens.border}`,
300
+ borderRadius: tokens.radiusMd,
301
+ padding,
302
+ boxShadow: tokens.shadowSm,
303
+ "--nb-card-shadow-hover": tokens.shadowMd,
304
+ "--nb-card-border-hover": tokens.borderStrong,
305
+ ...style
306
+ };
307
+ return /* @__PURE__ */ jsx(
308
+ "div",
309
+ {
310
+ className: interactive ? `nb-card--interactive ${className ?? ""}`.trim() : className,
311
+ style: cardStyle,
312
+ ...rest,
313
+ children
314
+ }
315
+ );
316
+ }
317
+ var STYLE_ID3 = "nb-synapse-drawer";
318
+ var RULES3 = `
319
+ .nb-drawer {
320
+ margin: 0; padding: 0; border: none; max-width: 92%;
321
+ height: 100%; max-height: 100%; display: flex; flex-direction: column;
322
+ }
323
+ .nb-drawer--right { margin-left: auto; }
324
+ .nb-drawer--left { margin-right: auto; }
325
+ .nb-drawer--right[open] { animation: nb-drawer-in-right 240ms cubic-bezier(0.2, 0, 0, 1); }
326
+ .nb-drawer--left[open] { animation: nb-drawer-in-left 240ms cubic-bezier(0.2, 0, 0, 1); }
327
+ .nb-drawer::backdrop { background: rgba(0, 0, 0, 0.32); }
328
+ .nb-drawer[open]::backdrop { animation: nb-drawer-fade 200ms ease; }
329
+ @keyframes nb-drawer-in-right { from { transform: translateX(100%); } }
330
+ @keyframes nb-drawer-in-left { from { transform: translateX(-100%); } }
331
+ @keyframes nb-drawer-fade { from { opacity: 0; } }
332
+ `;
333
+ function DrawerRoot({
334
+ open,
335
+ onClose,
336
+ onEscape,
337
+ side = "right",
338
+ width = 480,
339
+ style,
340
+ children,
341
+ ...rest
342
+ }) {
343
+ ensureStyle(STYLE_ID3, RULES3);
344
+ const dialogRef = useRef(null);
345
+ useEffect(() => {
346
+ const dialog = dialogRef.current;
347
+ if (!dialog || typeof dialog.showModal !== "function") return;
348
+ if (open && !dialog.open) dialog.showModal();
349
+ else if (!open && dialog.open) dialog.close();
350
+ }, [open]);
351
+ const panelStyle = {
352
+ width,
353
+ background: tokens.bg,
354
+ color: tokens.fg,
355
+ boxShadow: tokens.shadowLg,
356
+ ...side === "right" ? { borderLeft: `${tokens.borderWidth} solid ${tokens.border}` } : { borderRight: `${tokens.borderWidth} solid ${tokens.border}` },
357
+ ...style
358
+ };
359
+ return (
360
+ // biome-ignore lint/a11y/useKeyWithClickEvents: backdrop click is a mouse convenience; the dialog's Escape (onCancel) is the keyboard dismissal path.
361
+ /* @__PURE__ */ jsx(
362
+ "dialog",
363
+ {
364
+ ref: dialogRef,
365
+ className: `nb-drawer nb-drawer--${side}`,
366
+ style: panelStyle,
367
+ onCancel: (e) => {
368
+ e.preventDefault();
369
+ (onEscape ?? onClose)();
370
+ },
371
+ onClick: (e) => {
372
+ if (e.target === dialogRef.current) onClose();
373
+ },
374
+ ...rest,
375
+ children
376
+ }
377
+ )
378
+ );
379
+ }
380
+ function Header({ onClose, style, children, ...rest }) {
381
+ return /* @__PURE__ */ jsxs(
382
+ "header",
383
+ {
384
+ style: {
385
+ flexShrink: 0,
386
+ display: "flex",
387
+ alignItems: "center",
388
+ gap: "0.75rem",
389
+ padding: "0.875rem 1rem",
390
+ borderBottom: `${tokens.borderWidth} solid ${tokens.border}`,
391
+ fontFamily: tokens.fontSans,
392
+ fontWeight: tokens.weightSemibold,
393
+ color: tokens.fg,
394
+ ...style
395
+ },
396
+ ...rest,
397
+ children: [
398
+ /* @__PURE__ */ jsx("div", { style: { flex: 1, minWidth: 0 }, children }),
399
+ onClose ? /* @__PURE__ */ jsx(CloseButton, { onClose }) : null
400
+ ]
401
+ }
402
+ );
403
+ }
404
+ function CloseButton({ onClose }) {
405
+ return /* @__PURE__ */ jsx(
406
+ "button",
407
+ {
408
+ type: "button",
409
+ "aria-label": "Close",
410
+ onClick: onClose,
411
+ style: {
412
+ flexShrink: 0,
413
+ display: "inline-flex",
414
+ alignItems: "center",
415
+ justifyContent: "center",
416
+ width: 28,
417
+ height: 28,
418
+ border: "none",
419
+ background: "transparent",
420
+ color: tokens.fgMuted,
421
+ cursor: "pointer",
422
+ borderRadius: tokens.radiusSm
423
+ },
424
+ children: /* @__PURE__ */ jsx("svg", { width: "16", height: "16", viewBox: "0 0 16 16", "aria-hidden": "true", children: /* @__PURE__ */ jsx(
425
+ "path",
426
+ {
427
+ d: "M4 4l8 8M12 4l-8 8",
428
+ stroke: "currentColor",
429
+ strokeWidth: "1.5",
430
+ strokeLinecap: "round"
431
+ }
432
+ ) })
433
+ }
434
+ );
435
+ }
436
+ function Body({ style, children, ...rest }) {
437
+ return /* @__PURE__ */ jsx("div", { style: { flex: 1, minHeight: 0, overflowY: "auto", padding: "1rem", ...style }, ...rest, children });
438
+ }
439
+ function Footer({ style, children, ...rest }) {
440
+ return /* @__PURE__ */ jsx(
441
+ "footer",
442
+ {
443
+ style: {
444
+ flexShrink: 0,
445
+ padding: "0.75rem 1rem",
446
+ borderTop: `${tokens.borderWidth} solid ${tokens.border}`,
447
+ ...style
448
+ },
449
+ ...rest,
450
+ children
451
+ }
452
+ );
453
+ }
454
+ var Drawer = Object.assign(DrawerRoot, { Header, Body, Footer });
455
+ var ALIGN = {
456
+ start: "flex-start",
457
+ center: "center",
458
+ end: "flex-end",
459
+ stretch: "stretch",
460
+ baseline: "baseline"
461
+ };
462
+ var JUSTIFY = {
463
+ start: "flex-start",
464
+ center: "center",
465
+ end: "flex-end",
466
+ between: "space-between",
467
+ around: "space-around"
468
+ };
469
+ function flexStyle(direction, { gap, align, justify, wrap }) {
470
+ return {
471
+ display: "flex",
472
+ flexDirection: direction,
473
+ gap,
474
+ alignItems: align ? ALIGN[align] : void 0,
475
+ justifyContent: justify ? JUSTIFY[justify] : void 0,
476
+ flexWrap: wrap ? "wrap" : void 0
477
+ };
478
+ }
479
+ function Stack({ gap, align, justify, wrap, style, children, ...rest }) {
480
+ return /* @__PURE__ */ jsx("div", { style: { ...flexStyle("column", { gap, align, justify, wrap }), ...style }, ...rest, children });
481
+ }
482
+ function Inline({
483
+ gap,
484
+ align = "center",
485
+ justify,
486
+ wrap,
487
+ style,
488
+ children,
489
+ ...rest
490
+ }) {
491
+ return /* @__PURE__ */ jsx("div", { style: { ...flexStyle("row", { gap, align, justify, wrap }), ...style }, ...rest, children });
492
+ }
493
+ function Spacer({ style, ...rest }) {
494
+ return /* @__PURE__ */ jsx("div", { style: { flex: 1, ...style }, ...rest });
495
+ }
496
+ function Divider({ orientation = "horizontal", style, ...rest }) {
497
+ const isVertical = orientation === "vertical";
498
+ return /* @__PURE__ */ jsx(
499
+ "div",
500
+ {
501
+ "aria-hidden": "true",
502
+ style: {
503
+ flexShrink: 0,
504
+ background: tokens.border,
505
+ ...isVertical ? { width: tokens.borderWidth, alignSelf: "stretch" } : { height: tokens.borderWidth, width: "100%" },
506
+ ...style
507
+ },
508
+ ...rest
509
+ }
510
+ );
511
+ }
512
+ var TONE2 = {
513
+ default: tokens.fg,
514
+ muted: tokens.fgMuted,
515
+ faint: tokens.fgFaint,
516
+ accent: tokens.accent,
517
+ danger: tokens.danger,
518
+ success: tokens.success
519
+ };
520
+ var WEIGHT = {
521
+ normal: tokens.weightNormal,
522
+ medium: tokens.weightMedium,
523
+ semibold: tokens.weightSemibold,
524
+ bold: tokens.weightBold
525
+ };
526
+ function Text({
527
+ size = "base",
528
+ weight = "normal",
529
+ tone = "default",
530
+ mono = false,
531
+ truncate = false,
532
+ as: As = "span",
533
+ style,
534
+ children,
535
+ ...rest
536
+ }) {
537
+ const truncation = truncate ? { overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" } : {};
538
+ return /* @__PURE__ */ jsx(
539
+ As,
540
+ {
541
+ style: {
542
+ margin: 0,
543
+ fontFamily: mono ? tokens.fontMono : tokens.fontSans,
544
+ fontWeight: WEIGHT[weight],
545
+ color: TONE2[tone],
546
+ ...textStyle(size),
547
+ ...truncation,
548
+ ...style
549
+ },
550
+ ...rest,
551
+ children
552
+ }
553
+ );
554
+ }
555
+ function Heading({
556
+ size = "md",
557
+ weight = "semibold",
558
+ tone = "default",
559
+ as: As = "h2",
560
+ style,
561
+ children,
562
+ ...rest
563
+ }) {
564
+ return /* @__PURE__ */ jsx(
565
+ As,
566
+ {
567
+ style: {
568
+ margin: 0,
569
+ fontFamily: tokens.fontHeading,
570
+ fontWeight: WEIGHT[weight],
571
+ color: TONE2[tone],
572
+ letterSpacing: "-0.015em",
573
+ ...headingStyle(size),
574
+ ...style
575
+ },
576
+ ...rest,
577
+ children
578
+ }
579
+ );
580
+ }
581
+ function EmptyState({ icon, title, description, action, style, ...rest }) {
582
+ return /* @__PURE__ */ jsxs(
583
+ Stack,
584
+ {
585
+ align: "center",
586
+ gap: "0.5rem",
587
+ style: {
588
+ textAlign: "center",
589
+ padding: "4rem 1.5rem",
590
+ color: tokens.fgMuted,
591
+ ...style
592
+ },
593
+ ...rest,
594
+ children: [
595
+ icon ? /* @__PURE__ */ jsx("div", { style: { color: tokens.fgFaint, marginBottom: "0.25rem" }, children: icon }) : null,
596
+ title ? /* @__PURE__ */ jsx(Heading, { size: "sm", children: title }) : null,
597
+ description ? /* @__PURE__ */ jsx(Text, { size: "sm", tone: "muted", style: { maxWidth: 360 }, children: description }) : null,
598
+ action ? /* @__PURE__ */ jsx("div", { style: { marginTop: "0.75rem" }, children: action }) : null
599
+ ]
600
+ }
601
+ );
602
+ }
603
+ var STYLE_ID4 = "nb-synapse-listrow";
604
+ var RULES4 = `
605
+ .nb-listrow { transition: background 140ms ease; }
606
+ .nb-listrow--interactive { cursor: pointer; }
607
+ .nb-listrow--interactive:hover { background: var(--nb-listrow-hover); }
608
+ `;
609
+ function ListRow({
610
+ title,
611
+ meta,
612
+ leading,
613
+ trailing,
614
+ interactive = false,
615
+ style,
616
+ className,
617
+ ...rest
618
+ }) {
619
+ ensureStyle(STYLE_ID4, RULES4);
620
+ const rowStyle = {
621
+ "--nb-listrow-hover": tokens.bgSubtle,
622
+ display: "grid",
623
+ gridTemplateColumns: `${leading ? "auto " : ""}minmax(0, 1fr)${trailing ? " auto" : ""}`,
624
+ columnGap: "0.75rem",
625
+ alignItems: "baseline",
626
+ padding: "0.65rem 0.75rem",
627
+ borderRadius: tokens.radiusSm,
628
+ ...style
629
+ };
630
+ const interactiveProps = interactive ? {
631
+ role: "button",
632
+ tabIndex: 0,
633
+ onKeyDown: (e) => {
634
+ if (e.key === "Enter" || e.key === " ") {
635
+ e.preventDefault();
636
+ e.currentTarget.click();
637
+ }
638
+ }
639
+ } : {};
640
+ return /* @__PURE__ */ jsxs(
641
+ "div",
642
+ {
643
+ className: `nb-listrow ${interactive ? "nb-listrow--interactive" : ""} ${className ?? ""}`.trim(),
644
+ style: rowStyle,
645
+ ...interactiveProps,
646
+ ...rest,
647
+ children: [
648
+ leading ? /* @__PURE__ */ jsx("div", { style: { alignSelf: "center", display: "flex" }, children: leading }) : null,
649
+ /* @__PURE__ */ jsxs("div", { style: { minWidth: 0 }, children: [
650
+ /* @__PURE__ */ jsx(
651
+ "div",
652
+ {
653
+ style: {
654
+ fontFamily: tokens.fontSans,
655
+ fontSize: tokens.textBaseSize,
656
+ lineHeight: tokens.textBaseLine,
657
+ fontWeight: tokens.weightMedium,
658
+ color: tokens.fg,
659
+ overflow: "hidden",
660
+ textOverflow: "ellipsis",
661
+ whiteSpace: "nowrap"
662
+ },
663
+ children: title
664
+ }
665
+ ),
666
+ meta ? /* @__PURE__ */ jsx(
667
+ "div",
668
+ {
669
+ style: {
670
+ marginTop: "0.15rem",
671
+ fontFamily: tokens.fontSans,
672
+ fontSize: tokens.textXsSize,
673
+ lineHeight: tokens.textXsLine,
674
+ color: tokens.fgMuted,
675
+ overflow: "hidden",
676
+ textOverflow: "ellipsis",
677
+ whiteSpace: "nowrap"
678
+ },
679
+ children: meta
680
+ }
681
+ ) : null
682
+ ] }),
683
+ trailing ? /* @__PURE__ */ jsx(
684
+ "div",
685
+ {
686
+ style: {
687
+ alignSelf: "center",
688
+ display: "inline-flex",
689
+ alignItems: "center",
690
+ gap: "0.5rem",
691
+ fontSize: tokens.textXsSize,
692
+ color: tokens.fgMuted,
693
+ whiteSpace: "nowrap"
694
+ },
695
+ children: trailing
696
+ }
697
+ ) : null
698
+ ]
699
+ }
700
+ );
701
+ }
702
+ function Pagination({ page, pageCount, onChange, style, ...rest }) {
703
+ const total = Math.max(1, pageCount);
704
+ const atStart = page <= 1;
705
+ const atEnd = page >= total;
706
+ return /* @__PURE__ */ jsxs(
707
+ Inline,
708
+ {
709
+ justify: "between",
710
+ style: {
711
+ fontFamily: tokens.fontSans,
712
+ fontSize: tokens.textXsSize,
713
+ color: tokens.fgMuted,
714
+ letterSpacing: "0.02em",
715
+ ...style
716
+ },
717
+ ...rest,
718
+ children: [
719
+ /* @__PURE__ */ jsxs("span", { style: { fontVariantNumeric: "tabular-nums" }, children: [
720
+ "Page ",
721
+ page,
722
+ " of ",
723
+ total
724
+ ] }),
725
+ /* @__PURE__ */ jsxs(Inline, { gap: "1rem", children: [
726
+ /* @__PURE__ */ jsx(TextLink, { disabled: atStart, onClick: () => onChange(page - 1), children: "\u2190 Prev" }),
727
+ /* @__PURE__ */ jsx(TextLink, { disabled: atEnd, onClick: () => onChange(page + 1), children: "Next \u2192" })
728
+ ] })
729
+ ]
730
+ }
731
+ );
732
+ }
733
+ var STYLE_ID5 = "nb-synapse-prose";
734
+ var RULES5 = `
735
+ .nb-prose {
736
+ font-family: ${tokens.fontSans};
737
+ font-size: ${tokens.textBaseSize};
738
+ line-height: 1.7;
739
+ color: ${tokens.fg};
740
+ }
741
+ .nb-prose > :first-child { margin-top: 0; }
742
+ .nb-prose > :last-child { margin-bottom: 0; }
743
+ .nb-prose h1, .nb-prose h2 { font-family: ${tokens.fontHeading}; letter-spacing: -0.015em; color: ${tokens.fg}; }
744
+ .nb-prose h1 { font-size: ${tokens.headingMdSize}; line-height: ${tokens.headingMdLine}; margin: 2rem 0 0.75rem; }
745
+ .nb-prose h2 { font-size: ${tokens.headingSmSize}; line-height: ${tokens.headingSmLine}; margin: 1.75rem 0 0.5rem; }
746
+ .nb-prose h3 { font-size: ${tokens.textBaseSize}; font-weight: ${tokens.weightSemibold}; margin: 1.5rem 0 0.35rem; color: ${tokens.fg}; }
747
+ .nb-prose p { margin: 0 0 1rem; }
748
+ .nb-prose ul, .nb-prose ol { margin: 0 0 1rem; padding-left: 1.25rem; }
749
+ .nb-prose li { margin: 0.25rem 0; }
750
+ .nb-prose a { color: ${tokens.accent}; text-decoration: underline; text-underline-offset: 2px; }
751
+ .nb-prose blockquote { margin: 0 0 1rem; padding-left: 1rem; border-left: 2px solid ${tokens.accent}; color: ${tokens.fgMuted}; font-style: italic; }
752
+ .nb-prose code { font-family: ${tokens.fontMono}; font-size: 0.88em; padding: 0.08rem 0.32rem; border-radius: ${tokens.radiusXs}; background: ${tokens.bgSubtle}; }
753
+ .nb-prose pre { font-family: ${tokens.fontMono}; font-size: ${tokens.textSmSize}; line-height: 1.55; margin: 0 0 1.25rem; padding: 0.875rem 1rem; background: ${tokens.bgSubtle}; border-radius: ${tokens.radiusSm}; overflow-x: auto; }
754
+ .nb-prose pre code { padding: 0; background: none; }
755
+ .nb-prose hr { border: none; border-top: 1px solid ${tokens.border}; margin: 2rem 0; }
756
+ .nb-prose strong { font-weight: ${tokens.weightSemibold}; }
757
+ `;
758
+ function Prose({ html, children, className, ...rest }) {
759
+ ensureStyle(STYLE_ID5, RULES5);
760
+ const cls = `nb-prose ${className ?? ""}`.trim();
761
+ if (html !== void 0) {
762
+ return /* @__PURE__ */ jsx("div", { className: cls, dangerouslySetInnerHTML: { __html: html }, ...rest });
763
+ }
764
+ return /* @__PURE__ */ jsx("div", { className: cls, ...rest, children });
765
+ }
766
+ var STYLE_ID6 = "nb-synapse-searchfield";
767
+ var RULES6 = `
768
+ .nb-search { font-family: var(--nb-search-font); color: var(--nb-search-fg); outline: none; }
769
+ .nb-search::placeholder { color: var(--nb-search-placeholder); }
770
+ .nb-search--underline {
771
+ border: none;
772
+ border-bottom: 1px solid transparent;
773
+ background: transparent;
774
+ padding: 0.3rem 0.1rem;
775
+ transition: border-color 160ms ease;
776
+ }
777
+ .nb-search--underline:focus { border-bottom-color: var(--nb-search-accent); }
778
+ .nb-search--boxed {
779
+ border: 1px solid var(--nb-search-border);
780
+ border-radius: var(--nb-search-radius);
781
+ background: var(--nb-search-bg);
782
+ padding: 0.4rem 0.6rem;
783
+ transition: border-color 160ms ease, box-shadow 160ms ease;
784
+ }
785
+ .nb-search--boxed:focus {
786
+ border-color: var(--nb-search-accent);
787
+ box-shadow: 0 0 0 3px color-mix(in oklab, var(--nb-search-accent) 18%, transparent);
788
+ }
789
+ `;
790
+ function SearchField({
791
+ variant = "underline",
792
+ style,
793
+ className,
794
+ type = "search",
795
+ ...rest
796
+ }) {
797
+ ensureStyle(STYLE_ID6, RULES6);
798
+ const fieldStyle = {
799
+ "--nb-search-font": tokens.fontSans,
800
+ "--nb-search-fg": tokens.fg,
801
+ "--nb-search-placeholder": tokens.fgFaint,
802
+ "--nb-search-accent": tokens.accent,
803
+ "--nb-search-border": tokens.border,
804
+ "--nb-search-bg": tokens.bgSubtle,
805
+ "--nb-search-radius": tokens.radiusSm,
806
+ fontSize: tokens.textSmSize,
807
+ lineHeight: tokens.textSmLine,
808
+ ...style
809
+ };
810
+ return /* @__PURE__ */ jsx(
811
+ "input",
812
+ {
813
+ type,
814
+ className: `nb-search nb-search--${variant} ${className ?? ""}`.trim(),
815
+ style: fieldStyle,
816
+ ...rest
817
+ }
818
+ );
819
+ }
820
+ var STYLE_ID7 = "nb-synapse-segmented";
821
+ var RULES7 = `
822
+ .nb-seg {
823
+ display: inline-flex;
824
+ gap: 2px;
825
+ padding: 2px;
826
+ border-radius: var(--nb-seg-radius);
827
+ background: var(--nb-seg-track);
828
+ }
829
+ .nb-seg__btn {
830
+ border: none;
831
+ background: transparent;
832
+ color: var(--nb-seg-fg);
833
+ font-family: var(--nb-seg-font);
834
+ font-weight: var(--nb-seg-weight);
835
+ padding: 0.25rem 0.7rem;
836
+ border-radius: calc(var(--nb-seg-radius) - 2px);
837
+ cursor: pointer;
838
+ transition: color 130ms ease, background 130ms ease;
839
+ white-space: nowrap;
840
+ }
841
+ .nb-seg__btn:hover:not([aria-pressed="true"]) { color: var(--nb-seg-fg-strong); }
842
+ .nb-seg__btn[aria-pressed="true"] {
843
+ background: var(--nb-seg-active-bg);
844
+ color: var(--nb-seg-fg-strong);
845
+ box-shadow: var(--nb-seg-active-shadow);
846
+ }
847
+ `;
848
+ function SegmentedControl({
849
+ options,
850
+ value,
851
+ onChange,
852
+ style,
853
+ className,
854
+ ...rest
855
+ }) {
856
+ ensureStyle(STYLE_ID7, RULES7);
857
+ const trackStyle = {
858
+ "--nb-seg-radius": tokens.radiusSm,
859
+ "--nb-seg-track": tokens.bgSubtle,
860
+ "--nb-seg-fg": tokens.fgMuted,
861
+ "--nb-seg-fg-strong": tokens.fg,
862
+ "--nb-seg-font": tokens.fontSans,
863
+ "--nb-seg-weight": tokens.weightMedium,
864
+ "--nb-seg-active-bg": tokens.bgRaised,
865
+ "--nb-seg-active-shadow": tokens.shadowSm,
866
+ fontSize: tokens.textSmSize,
867
+ lineHeight: tokens.textSmLine,
868
+ ...style
869
+ };
870
+ return (
871
+ // biome-ignore lint/a11y/useSemanticElements: role="group" with aria-label is the correct grouping for a set of toggle buttons; <fieldset> would impose form semantics this control doesn't have.
872
+ /* @__PURE__ */ jsx("div", { className: `nb-seg ${className ?? ""}`.trim(), role: "group", style: trackStyle, ...rest, children: options.map((opt) => /* @__PURE__ */ jsx(
873
+ "button",
874
+ {
875
+ type: "button",
876
+ className: "nb-seg__btn",
877
+ "aria-pressed": opt.value === value,
878
+ onClick: () => onChange(opt.value),
879
+ children: opt.label
880
+ },
881
+ opt.value
882
+ )) })
883
+ );
884
+ }
885
+ var STYLE_ID8 = "nb-synapse-spinner";
886
+ var RULES8 = `
887
+ @keyframes nb-spin { to { transform: rotate(360deg); } }
888
+ .nb-spinner { animation: nb-spin 0.7s linear infinite; }
889
+ @media (prefers-reduced-motion: reduce) { .nb-spinner { animation-duration: 1.6s; } }
890
+ `;
891
+ function Spinner({ size = 16, color, style, className, ...rest }) {
892
+ ensureStyle(STYLE_ID8, RULES8);
893
+ const spinnerStyle = {
894
+ display: "inline-block",
895
+ flexShrink: 0,
896
+ width: size,
897
+ height: size,
898
+ borderRadius: "50%",
899
+ border: `2px solid ${tokens.border}`,
900
+ borderTopColor: color ?? tokens.accent,
901
+ boxSizing: "border-box",
902
+ ...style
903
+ };
904
+ return /* @__PURE__ */ jsx(
905
+ "span",
906
+ {
907
+ className: `nb-spinner ${className ?? ""}`.trim(),
908
+ role: "status",
909
+ "aria-label": "Loading",
910
+ style: spinnerStyle,
911
+ ...rest
912
+ }
913
+ );
914
+ }
915
+ var COLOR = {
916
+ working: tokens.accent,
917
+ completed: tokens.success,
918
+ failed: tokens.danger,
919
+ idle: tokens.fgFaint
920
+ };
921
+ var STYLE_ID9 = "nb-synapse-statusdot";
922
+ var KEYFRAMES = `
923
+ @keyframes nb-statusdot-pulse {
924
+ 0% { box-shadow: 0 0 0 0 var(--nb-statusdot-ring); transform: scale(1); }
925
+ 70% { box-shadow: 0 0 0 7px transparent; transform: scale(1.15); }
926
+ 100% { box-shadow: 0 0 0 0 transparent; transform: scale(1); }
927
+ }
928
+ .nb-statusdot--working {
929
+ animation: nb-statusdot-pulse 1.6s cubic-bezier(0.4, 0, 0.6, 1) infinite;
930
+ }
931
+ @media (prefers-reduced-motion: reduce) {
932
+ .nb-statusdot--working { animation: none; box-shadow: 0 0 0 2px var(--nb-statusdot-ring); }
933
+ }
934
+ `;
935
+ function StatusDot({
936
+ status = "idle",
937
+ color,
938
+ size = 8,
939
+ style,
940
+ className,
941
+ ...rest
942
+ }) {
943
+ ensureStyle(STYLE_ID9, KEYFRAMES);
944
+ const dotColor = color ?? COLOR[status];
945
+ const working = status === "working";
946
+ const dotStyle = {
947
+ display: "inline-block",
948
+ flexShrink: 0,
949
+ width: size,
950
+ height: size,
951
+ borderRadius: "50%",
952
+ background: dotColor,
953
+ // Ring color for the pulse keyframe — a faded copy of the dot color.
954
+ "--nb-statusdot-ring": `color-mix(in oklab, ${dotColor} 45%, transparent)`,
955
+ ...style
956
+ };
957
+ return /* @__PURE__ */ jsx(
958
+ "span",
959
+ {
960
+ className: working ? `nb-statusdot--working ${className ?? ""}`.trim() : className,
961
+ style: dotStyle,
962
+ ...rest
963
+ }
964
+ );
965
+ }
966
+ var STYLE_ID10 = "nb-synapse-table";
967
+ var RULES9 = `
968
+ .nb-table { width: 100%; border-collapse: collapse; font-family: var(--nb-table-font); }
969
+ .nb-table th, .nb-table td {
970
+ padding: 0.55rem 0.75rem;
971
+ border-bottom: var(--nb-table-border);
972
+ font-size: var(--nb-table-size); line-height: var(--nb-table-line);
973
+ }
974
+ .nb-table thead th {
975
+ position: sticky; top: 0; z-index: 1;
976
+ background: var(--nb-table-head-bg);
977
+ color: var(--nb-table-muted);
978
+ font-weight: var(--nb-table-semibold);
979
+ font-size: var(--nb-table-xs); text-align: left;
980
+ text-transform: uppercase; letter-spacing: 0.04em;
981
+ }
982
+ .nb-table tbody td { color: var(--nb-table-fg); }
983
+ .nb-table tbody tr.nb-table__row--clickable { cursor: pointer; }
984
+ .nb-table tbody tr.nb-table__row--clickable:hover { background: var(--nb-table-hover); }
985
+ `;
986
+ function Table({
987
+ data,
988
+ columns,
989
+ rowKey,
990
+ onRowClick,
991
+ empty,
992
+ style,
993
+ className,
994
+ ...rest
995
+ }) {
996
+ ensureStyle(STYLE_ID10, RULES9);
997
+ const vars = {
998
+ "--nb-table-font": tokens.fontSans,
999
+ "--nb-table-fg": tokens.fg,
1000
+ "--nb-table-muted": tokens.fgMuted,
1001
+ "--nb-table-head-bg": tokens.bg,
1002
+ "--nb-table-hover": tokens.bgSubtle,
1003
+ "--nb-table-border": `${tokens.borderWidth} solid ${tokens.border}`,
1004
+ "--nb-table-size": tokens.textSmSize,
1005
+ "--nb-table-line": tokens.textSmLine,
1006
+ "--nb-table-xs": tokens.textXsSize,
1007
+ "--nb-table-semibold": tokens.weightSemibold
1008
+ };
1009
+ if (data.length === 0 && empty !== void 0) {
1010
+ return /* @__PURE__ */ jsx(Fragment, { children: empty });
1011
+ }
1012
+ const tableStyle = { ...vars, ...style };
1013
+ return /* @__PURE__ */ jsxs("table", { className: `nb-table ${className ?? ""}`.trim(), style: tableStyle, ...rest, children: [
1014
+ /* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsx("tr", { children: columns.map((col) => /* @__PURE__ */ jsx(
1015
+ "th",
1016
+ {
1017
+ scope: "col",
1018
+ style: { width: col.width, textAlign: col.align ?? "left" },
1019
+ children: col.header
1020
+ },
1021
+ col.key
1022
+ )) }) }),
1023
+ /* @__PURE__ */ jsx("tbody", { children: data.map((row, i) => {
1024
+ const clickable = Boolean(onRowClick);
1025
+ return /* @__PURE__ */ jsx(
1026
+ "tr",
1027
+ {
1028
+ className: clickable ? "nb-table__row--clickable" : void 0,
1029
+ tabIndex: clickable ? 0 : void 0,
1030
+ onClick: clickable ? () => onRowClick?.(row, i) : void 0,
1031
+ onKeyDown: clickable ? (e) => {
1032
+ if (e.key === "Enter" || e.key === " ") {
1033
+ e.preventDefault();
1034
+ onRowClick?.(row, i);
1035
+ }
1036
+ } : void 0,
1037
+ children: columns.map((col) => /* @__PURE__ */ jsx("td", { style: { textAlign: col.align ?? "left" }, children: col.render(row, i) }, col.key))
1038
+ },
1039
+ rowKey(row, i)
1040
+ );
1041
+ }) })
1042
+ ] });
1043
+ }
1044
+ var READING_MAX = 760;
1045
+ var ContentWidthCtx = createContext("full");
1046
+ function columnStyle(width) {
1047
+ return width === "reading" ? { maxWidth: READING_MAX, marginInline: "auto", width: "100%" } : {};
1048
+ }
1049
+ function AppFrameRoot({ contentWidth = "full", style, children, ...rest }) {
1050
+ return /* @__PURE__ */ jsx(ContentWidthCtx.Provider, { value: contentWidth, children: /* @__PURE__ */ jsx(
1051
+ "div",
1052
+ {
1053
+ style: {
1054
+ display: "flex",
1055
+ flexDirection: "column",
1056
+ height: "100%",
1057
+ minHeight: 0,
1058
+ background: tokens.bg,
1059
+ color: tokens.fg,
1060
+ fontFamily: tokens.fontSans,
1061
+ ...style
1062
+ },
1063
+ ...rest,
1064
+ children
1065
+ }
1066
+ ) });
1067
+ }
1068
+ function Header2({ style, children, ...rest }) {
1069
+ const width = useContext(ContentWidthCtx);
1070
+ return /* @__PURE__ */ jsx("header", { style: { flexShrink: 0, padding: "1.25rem 1.5rem 0.75rem", ...style }, ...rest, children: /* @__PURE__ */ jsx("div", { style: columnStyle(width), children }) });
1071
+ }
1072
+ function Body2({ bleed = false, style, children, ...rest }) {
1073
+ const width = useContext(ContentWidthCtx);
1074
+ if (bleed) {
1075
+ return /* @__PURE__ */ jsx(
1076
+ "div",
1077
+ {
1078
+ style: {
1079
+ flex: 1,
1080
+ minHeight: 0,
1081
+ display: "flex",
1082
+ flexDirection: "column",
1083
+ overflow: "hidden",
1084
+ ...style
1085
+ },
1086
+ ...rest,
1087
+ children
1088
+ }
1089
+ );
1090
+ }
1091
+ return /* @__PURE__ */ jsx("div", { style: { flex: 1, minHeight: 0, overflowY: "auto", ...style }, ...rest, children: /* @__PURE__ */ jsx("div", { style: { ...columnStyle(width), padding: "0.75rem 1.5rem 1.5rem" }, children }) });
1092
+ }
1093
+ function Footer2({ style, children, ...rest }) {
1094
+ const width = useContext(ContentWidthCtx);
1095
+ return /* @__PURE__ */ jsx(
1096
+ "footer",
1097
+ {
1098
+ style: {
1099
+ flexShrink: 0,
1100
+ padding: "0.75rem 1.5rem",
1101
+ borderTop: `${tokens.borderWidth} solid ${tokens.border}`,
1102
+ ...style
1103
+ },
1104
+ ...rest,
1105
+ children: /* @__PURE__ */ jsx("div", { style: columnStyle(width), children })
1106
+ }
1107
+ );
1108
+ }
1109
+ var AppFrame = Object.assign(AppFrameRoot, { Header: Header2, Body: Body2, Footer: Footer2 });
1110
+ function useBreakpoint(breakpoint) {
1111
+ const ref = useRef(null);
1112
+ const [width, setWidth] = useState(null);
1113
+ useLayoutEffect(() => {
1114
+ const el = ref.current;
1115
+ if (!el) return;
1116
+ setWidth(el.getBoundingClientRect().width);
1117
+ if (typeof ResizeObserver === "undefined") return;
1118
+ const ro = new ResizeObserver((entries) => {
1119
+ const entry = entries[0];
1120
+ const w = entry?.borderBoxSize?.[0]?.inlineSize ?? entry?.contentRect.width;
1121
+ if (typeof w === "number") setWidth(w);
1122
+ });
1123
+ ro.observe(el);
1124
+ return () => ro.disconnect();
1125
+ }, []);
1126
+ return { ref, isNarrow: width !== null && width < breakpoint };
1127
+ }
1128
+ var ListDetailCtx = createContext(null);
1129
+ function useListDetail() {
1130
+ const ctx = useContext(ListDetailCtx);
1131
+ if (!ctx) throw new Error("useListDetail must be used within <ListDetailLayout>");
1132
+ return ctx;
1133
+ }
1134
+ function ListDetailLayoutRoot({
1135
+ selected = false,
1136
+ onBack,
1137
+ listWidth = 320,
1138
+ breakpoint = 720,
1139
+ style,
1140
+ children,
1141
+ ...rest
1142
+ }) {
1143
+ const { ref, isNarrow } = useBreakpoint(breakpoint);
1144
+ return /* @__PURE__ */ jsx(ListDetailCtx.Provider, { value: { collapsed: isNarrow, selected, listWidth, onBack }, children: /* @__PURE__ */ jsx(
1145
+ "div",
1146
+ {
1147
+ ref,
1148
+ style: { display: "flex", flexDirection: "row", height: "100%", minHeight: 0, ...style },
1149
+ ...rest,
1150
+ children
1151
+ }
1152
+ ) });
1153
+ }
1154
+ function List({ style, children, ...rest }) {
1155
+ const { collapsed, selected, listWidth } = useListDetail();
1156
+ if (collapsed && selected) return null;
1157
+ const border = `${tokens.borderWidth} solid ${tokens.border}`;
1158
+ return /* @__PURE__ */ jsx(
1159
+ "section",
1160
+ {
1161
+ style: {
1162
+ overflowY: "auto",
1163
+ ...collapsed ? { flex: 1, minWidth: 0 } : { width: listWidth, flexShrink: 0, borderRight: border },
1164
+ ...style
1165
+ },
1166
+ ...rest,
1167
+ children
1168
+ }
1169
+ );
1170
+ }
1171
+ function Detail({ style, children, ...rest }) {
1172
+ const { collapsed, selected } = useListDetail();
1173
+ if (collapsed && !selected) return null;
1174
+ return /* @__PURE__ */ jsx("div", { style: { flex: 1, minWidth: 0, minHeight: 0, overflowY: "auto", ...style }, ...rest, children });
1175
+ }
1176
+ function Back({ children, style }) {
1177
+ const { collapsed, selected, onBack } = useListDetail();
1178
+ if (!collapsed || !selected) return null;
1179
+ return /* @__PURE__ */ jsx(TextLink, { onClick: () => onBack?.(), style: { marginBottom: "0.5rem", ...style }, children: children ?? "\u2190 Back" });
1180
+ }
1181
+ var ListDetailLayout = Object.assign(ListDetailLayoutRoot, { List, Detail, Back });
1182
+ var SidebarCtx = createContext(null);
1183
+ function useSidebar() {
1184
+ const ctx = useContext(SidebarCtx);
1185
+ if (!ctx) throw new Error("useSidebar must be used within <SidebarLayout>");
1186
+ return ctx;
1187
+ }
1188
+ var STYLE_ID11 = "nb-synapse-sidebarlayout";
1189
+ var RULES10 = `
1190
+ .nb-sidebar-drawer { transition: transform 220ms cubic-bezier(0.2, 0, 0, 1); }
1191
+ .nb-sidebar-scrim { animation: nb-sidebar-fade 180ms ease; }
1192
+ @keyframes nb-sidebar-fade { from { opacity: 0; } to { opacity: 1; } }
1193
+ @media (prefers-reduced-motion: reduce) {
1194
+ .nb-sidebar-drawer { transition: none; }
1195
+ .nb-sidebar-scrim { animation: none; }
1196
+ }
1197
+ `;
1198
+ function SidebarLayoutRoot({
1199
+ side = "left",
1200
+ width = 240,
1201
+ breakpoint = 640,
1202
+ collapseMode = "reflow",
1203
+ style,
1204
+ children,
1205
+ ...rest
1206
+ }) {
1207
+ const { ref, isNarrow } = useBreakpoint(breakpoint);
1208
+ const [open, setOpen] = useState(false);
1209
+ useEffect(() => {
1210
+ if (!open) return;
1211
+ const onKey = (e) => {
1212
+ if (e.key === "Escape") setOpen(false);
1213
+ };
1214
+ window.addEventListener("keydown", onKey);
1215
+ return () => window.removeEventListener("keydown", onKey);
1216
+ }, [open]);
1217
+ useEffect(() => {
1218
+ if (!isNarrow && open) setOpen(false);
1219
+ }, [isNarrow, open]);
1220
+ const reflowed = isNarrow && collapseMode === "reflow";
1221
+ const rootStyle = {
1222
+ position: "relative",
1223
+ display: "flex",
1224
+ flexDirection: reflowed ? "column" : side === "right" ? "row-reverse" : "row",
1225
+ height: "100%",
1226
+ minHeight: 0,
1227
+ ...style
1228
+ };
1229
+ return /* @__PURE__ */ jsx(
1230
+ SidebarCtx.Provider,
1231
+ {
1232
+ value: { collapsed: isNarrow, mode: collapseMode, side, width, open, setOpen },
1233
+ children: /* @__PURE__ */ jsx("div", { ref, style: rootStyle, ...rest, children })
1234
+ }
1235
+ );
1236
+ }
1237
+ function Sidebar({ style, children, ...rest }) {
1238
+ const { collapsed, mode, side, width, open, setOpen } = useSidebar();
1239
+ ensureStyle(STYLE_ID11, RULES10);
1240
+ if (!collapsed) {
1241
+ const border = `${tokens.borderWidth} solid ${tokens.border}`;
1242
+ return /* @__PURE__ */ jsx(
1243
+ "aside",
1244
+ {
1245
+ style: {
1246
+ width,
1247
+ flexShrink: 0,
1248
+ overflowY: "auto",
1249
+ display: "flex",
1250
+ flexDirection: "column",
1251
+ gap: "0.125rem",
1252
+ padding: "0.5rem 0.5rem",
1253
+ ...side === "right" ? { borderLeft: border } : { borderRight: border },
1254
+ ...style
1255
+ },
1256
+ ...rest,
1257
+ children
1258
+ }
1259
+ );
1260
+ }
1261
+ if (mode === "reflow") {
1262
+ return /* @__PURE__ */ jsx(
1263
+ "aside",
1264
+ {
1265
+ style: {
1266
+ display: "flex",
1267
+ flexDirection: "row",
1268
+ alignItems: "center",
1269
+ gap: "0.25rem",
1270
+ width: "100%",
1271
+ overflowX: "auto",
1272
+ padding: "0.5rem 0.75rem",
1273
+ borderBottom: `${tokens.borderWidth} solid ${tokens.border}`,
1274
+ ...style
1275
+ },
1276
+ ...rest,
1277
+ children
1278
+ }
1279
+ );
1280
+ }
1281
+ const closed = side === "right" ? "translateX(100%)" : "translateX(-100%)";
1282
+ const drawerStyle = {
1283
+ position: "absolute",
1284
+ top: 0,
1285
+ bottom: 0,
1286
+ [side]: 0,
1287
+ width,
1288
+ maxWidth: "85%",
1289
+ overflowY: "auto",
1290
+ background: tokens.bgRaised,
1291
+ boxShadow: tokens.shadowLg,
1292
+ transform: open ? "translateX(0)" : closed,
1293
+ zIndex: 11,
1294
+ ...side === "right" ? { borderLeft: `${tokens.borderWidth} solid ${tokens.border}` } : { borderRight: `${tokens.borderWidth} solid ${tokens.border}` },
1295
+ ...style
1296
+ };
1297
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
1298
+ open ? /* @__PURE__ */ jsx(
1299
+ "button",
1300
+ {
1301
+ type: "button",
1302
+ "aria-label": "Close sidebar",
1303
+ className: "nb-sidebar-scrim",
1304
+ onClick: () => setOpen(false),
1305
+ style: {
1306
+ position: "absolute",
1307
+ inset: 0,
1308
+ border: "none",
1309
+ padding: 0,
1310
+ cursor: "pointer",
1311
+ background: "rgba(0,0,0,0.32)",
1312
+ zIndex: 10
1313
+ }
1314
+ }
1315
+ ) : null,
1316
+ /* @__PURE__ */ jsx("aside", { className: "nb-sidebar-drawer", inert: !open, style: drawerStyle, ...rest, children })
1317
+ ] });
1318
+ }
1319
+ function Main({ style, children, ...rest }) {
1320
+ return /* @__PURE__ */ jsx("div", { style: { flex: 1, minWidth: 0, minHeight: 0, overflowY: "auto", ...style }, ...rest, children });
1321
+ }
1322
+ function Trigger({ children, style, onClick, ...rest }) {
1323
+ const ctx = useContext(SidebarCtx);
1324
+ if (!ctx?.collapsed || ctx.mode !== "drawer") return null;
1325
+ const { open, setOpen } = ctx;
1326
+ return /* @__PURE__ */ jsx(
1327
+ "button",
1328
+ {
1329
+ type: "button",
1330
+ "aria-label": "Toggle sidebar",
1331
+ "aria-expanded": open,
1332
+ onClick: (e) => {
1333
+ setOpen(!open);
1334
+ onClick?.(e);
1335
+ },
1336
+ style: {
1337
+ display: "inline-flex",
1338
+ alignItems: "center",
1339
+ justifyContent: "center",
1340
+ width: 32,
1341
+ height: 32,
1342
+ border: "none",
1343
+ background: "transparent",
1344
+ color: tokens.fg,
1345
+ cursor: "pointer",
1346
+ borderRadius: tokens.radiusSm,
1347
+ ...style
1348
+ },
1349
+ ...rest,
1350
+ children: children ?? /* @__PURE__ */ jsx("svg", { width: "18", height: "18", viewBox: "0 0 18 18", "aria-hidden": "true", children: /* @__PURE__ */ jsx(
1351
+ "path",
1352
+ {
1353
+ d: "M2 4.5h14M2 9h14M2 13.5h14",
1354
+ stroke: "currentColor",
1355
+ strokeWidth: "1.5",
1356
+ strokeLinecap: "round"
1357
+ }
1358
+ ) })
1359
+ }
1360
+ );
1361
+ }
1362
+ var SidebarLayout = Object.assign(SidebarLayoutRoot, { Sidebar, Main, Trigger });
1363
+
1364
+ export { AppFrame, Avatar, Badge, Button, Card, Divider, Drawer, EmptyState, Heading, Inline, ListDetailLayout, ListRow, Pagination, Prose, SearchField, SegmentedControl, SidebarLayout, Spacer, Spinner, Stack, StatusDot, Table, Text, TextLink, headingStyle, textStyle, tokens, useBreakpoint, useListDetail, useSidebar };
1365
+ //# sourceMappingURL=index.js.map
1366
+ //# sourceMappingURL=index.js.map