@nimblebrain/synapse 0.14.1 → 0.16.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.
package/dist/ui/index.cjs CHANGED
@@ -24,15 +24,13 @@ var tokens = {
24
24
  danger: "var(--nb-color-danger, #dc2626)",
25
25
  success: "var(--nb-color-success, #059669)",
26
26
  warning: "var(--nb-color-warning, #f59e0b)",
27
- warm: "var(--nb-color-warm, #d4620a)",
28
- warmLight: "var(--nb-color-warm-light, #fef5ee)",
29
27
  processing: "var(--nb-color-processing, #7c3aed)",
30
28
  processingLight: "var(--nb-color-processing-light, #f3eeff)",
31
29
  infoLight: "var(--nb-color-info-light, #eef4ff)",
32
30
  // ── Typography ──
33
31
  fontSans: "var(--font-sans, system-ui, -apple-system, BlinkMacSystemFont, sans-serif)",
34
32
  fontMono: "var(--font-mono, ui-monospace, SFMono-Regular, Menlo, monospace)",
35
- fontHeading: "var(--nb-font-heading, Georgia, 'Times New Roman', serif)",
33
+ fontHeading: "var(--nb-font-heading, system-ui, -apple-system, BlinkMacSystemFont, sans-serif)",
36
34
  weightNormal: "var(--font-weight-normal, 400)",
37
35
  weightMedium: "var(--font-weight-medium, 500)",
38
36
  weightSemibold: "var(--font-weight-semibold, 600)",
@@ -135,8 +133,7 @@ var TONE = {
135
133
  success: { bg: `color-mix(in oklab, ${tokens.success} 14%, transparent)`, fg: tokens.success },
136
134
  warning: { bg: `color-mix(in oklab, ${tokens.warning} 14%, transparent)`, fg: tokens.warning },
137
135
  danger: { bg: `color-mix(in oklab, ${tokens.danger} 14%, transparent)`, fg: tokens.danger },
138
- processing: { bg: tokens.processingLight, fg: tokens.processing },
139
- warm: { bg: tokens.warmLight, fg: tokens.warm }
136
+ processing: { bg: tokens.processingLight, fg: tokens.processing }
140
137
  };
141
138
  function Badge({ tone = "neutral", style, children, ...rest }) {
142
139
  const { bg, fg } = TONE[tone];
@@ -165,6 +162,81 @@ function Badge({ tone = "neutral", style, children, ...rest }) {
165
162
  }
166
163
  );
167
164
  }
165
+ var TONE2 = {
166
+ default: tokens.fg,
167
+ muted: tokens.fgMuted,
168
+ faint: tokens.fgFaint,
169
+ accent: tokens.accent,
170
+ danger: tokens.danger,
171
+ success: tokens.success
172
+ };
173
+ var WEIGHT = {
174
+ normal: tokens.weightNormal,
175
+ medium: tokens.weightMedium,
176
+ semibold: tokens.weightSemibold,
177
+ bold: tokens.weightBold
178
+ };
179
+ function Text({
180
+ size = "base",
181
+ weight = "normal",
182
+ tone = "default",
183
+ mono = false,
184
+ truncate = false,
185
+ as: As = "span",
186
+ style,
187
+ children,
188
+ ...rest
189
+ }) {
190
+ const truncation = truncate ? {
191
+ display: "block",
192
+ minWidth: 0,
193
+ overflow: "hidden",
194
+ textOverflow: "ellipsis",
195
+ whiteSpace: "nowrap"
196
+ } : {};
197
+ return /* @__PURE__ */ jsxRuntime.jsx(
198
+ As,
199
+ {
200
+ style: {
201
+ margin: 0,
202
+ fontFamily: mono ? tokens.fontMono : tokens.fontSans,
203
+ fontWeight: WEIGHT[weight],
204
+ color: TONE2[tone],
205
+ ...textStyle(size),
206
+ ...truncation,
207
+ ...style
208
+ },
209
+ ...rest,
210
+ children
211
+ }
212
+ );
213
+ }
214
+ function Heading({
215
+ size = "md",
216
+ weight = "semibold",
217
+ tone = "default",
218
+ as: As = "h2",
219
+ style,
220
+ children,
221
+ ...rest
222
+ }) {
223
+ return /* @__PURE__ */ jsxRuntime.jsx(
224
+ As,
225
+ {
226
+ style: {
227
+ margin: 0,
228
+ fontFamily: tokens.fontHeading,
229
+ fontWeight: WEIGHT[weight],
230
+ color: TONE2[tone],
231
+ letterSpacing: "-0.015em",
232
+ ...headingStyle(size),
233
+ ...style
234
+ },
235
+ ...rest,
236
+ children
237
+ }
238
+ );
239
+ }
168
240
  var STYLE_ID = "nb-synapse-button";
169
241
  var RULES = `
170
242
  .nb-btn {
@@ -265,6 +337,41 @@ function TextLink({ tone = "muted", style, className, children, ...rest }) {
265
337
  }
266
338
  );
267
339
  }
340
+ function Breadcrumb({ crumbs, separator = "\u203A", style, ...rest }) {
341
+ return /* @__PURE__ */ jsxRuntime.jsx(
342
+ "nav",
343
+ {
344
+ "aria-label": "Breadcrumb",
345
+ style: {
346
+ display: "flex",
347
+ alignItems: "center",
348
+ gap: "0.4rem",
349
+ flexWrap: "wrap",
350
+ minWidth: 0,
351
+ ...style
352
+ },
353
+ ...rest,
354
+ children: crumbs.map((crumb, i) => {
355
+ const last = i === crumbs.length - 1;
356
+ return (
357
+ // biome-ignore lint/suspicious/noArrayIndexKey: a crumb's POSITION is its identity — "the second level" is what the step means, and two levels can legitimately carry the same label. There is no id to key on, and a trail that changed shape SHOULD remount rather than reconcile a step into a different depth.
358
+ /* @__PURE__ */ jsxRuntime.jsxs(react.Fragment, { children: [
359
+ last || !crumb.onClick ? /* @__PURE__ */ jsxRuntime.jsx(
360
+ Text,
361
+ {
362
+ size: "xs",
363
+ tone: last ? "muted" : "default",
364
+ "aria-current": last ? "page" : void 0,
365
+ children: crumb.label
366
+ }
367
+ ) : /* @__PURE__ */ jsxRuntime.jsx(TextLink, { onClick: crumb.onClick, style: { fontSize: tokens.textXsSize }, children: crumb.label }),
368
+ last ? null : /* @__PURE__ */ jsxRuntime.jsx(Text, { size: "xs", tone: "faint", "aria-hidden": "true", children: separator })
369
+ ] }, `${crumb.label}:${i}`)
370
+ );
371
+ })
372
+ }
373
+ );
374
+ }
268
375
  var STYLE_ID2 = "nb-synapse-card";
269
376
  var RULES2 = `
270
377
  .nb-card--interactive {
@@ -613,75 +720,6 @@ function Divider({ orientation = "horizontal", style, ...rest }) {
613
720
  }
614
721
  );
615
722
  }
616
- var TONE2 = {
617
- default: tokens.fg,
618
- muted: tokens.fgMuted,
619
- faint: tokens.fgFaint,
620
- accent: tokens.accent,
621
- danger: tokens.danger,
622
- success: tokens.success
623
- };
624
- var WEIGHT = {
625
- normal: tokens.weightNormal,
626
- medium: tokens.weightMedium,
627
- semibold: tokens.weightSemibold,
628
- bold: tokens.weightBold
629
- };
630
- function Text({
631
- size = "base",
632
- weight = "normal",
633
- tone = "default",
634
- mono = false,
635
- truncate = false,
636
- as: As = "span",
637
- style,
638
- children,
639
- ...rest
640
- }) {
641
- const truncation = truncate ? { overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" } : {};
642
- return /* @__PURE__ */ jsxRuntime.jsx(
643
- As,
644
- {
645
- style: {
646
- margin: 0,
647
- fontFamily: mono ? tokens.fontMono : tokens.fontSans,
648
- fontWeight: WEIGHT[weight],
649
- color: TONE2[tone],
650
- ...textStyle(size),
651
- ...truncation,
652
- ...style
653
- },
654
- ...rest,
655
- children
656
- }
657
- );
658
- }
659
- function Heading({
660
- size = "md",
661
- weight = "semibold",
662
- tone = "default",
663
- as: As = "h2",
664
- style,
665
- children,
666
- ...rest
667
- }) {
668
- return /* @__PURE__ */ jsxRuntime.jsx(
669
- As,
670
- {
671
- style: {
672
- margin: 0,
673
- fontFamily: tokens.fontHeading,
674
- fontWeight: WEIGHT[weight],
675
- color: TONE2[tone],
676
- letterSpacing: "-0.015em",
677
- ...headingStyle(size),
678
- ...style
679
- },
680
- ...rest,
681
- children
682
- }
683
- );
684
- }
685
723
  function EmptyState({ icon, title, description, action, style, ...rest }) {
686
724
  return /* @__PURE__ */ jsxRuntime.jsxs(
687
725
  Stack,
@@ -803,6 +841,81 @@ function ListRow({
803
841
  }
804
842
  );
805
843
  }
844
+ var STYLE_ID5 = "nb-synapse-page-header";
845
+ var RULES5 = `
846
+ .nb-page-header__desc {
847
+ display: -webkit-box;
848
+ -webkit-box-orient: vertical;
849
+ -webkit-line-clamp: var(--nb-ph-desc-lines, 2);
850
+ line-clamp: var(--nb-ph-desc-lines, 2);
851
+ overflow: hidden;
852
+ }
853
+ `;
854
+ function PageHeader({
855
+ crumbs,
856
+ title,
857
+ status,
858
+ actions,
859
+ description,
860
+ descriptionLines = 2,
861
+ style,
862
+ ...rest
863
+ }) {
864
+ chunkRX6XCLFF_cjs.ensureStyle(STYLE_ID5, RULES5);
865
+ const descVars = descriptionLines > 0 ? { "--nb-ph-desc-lines": String(descriptionLines) } : {};
866
+ return /* @__PURE__ */ jsxRuntime.jsxs(
867
+ "header",
868
+ {
869
+ style: { display: "flex", flexDirection: "column", gap: "0.5rem", minWidth: 0, ...style },
870
+ ...rest,
871
+ children: [
872
+ crumbs && crumbs.length > 0 ? /* @__PURE__ */ jsxRuntime.jsx(Breadcrumb, { crumbs }) : null,
873
+ /* @__PURE__ */ jsxRuntime.jsxs(
874
+ "div",
875
+ {
876
+ style: {
877
+ display: "flex",
878
+ alignItems: "center",
879
+ justifyContent: "space-between",
880
+ gap: "1rem",
881
+ flexWrap: "wrap",
882
+ minWidth: 0
883
+ },
884
+ children: [
885
+ /* @__PURE__ */ jsxRuntime.jsxs(
886
+ "div",
887
+ {
888
+ style: {
889
+ display: "flex",
890
+ alignItems: "center",
891
+ gap: "0.6rem",
892
+ flexWrap: "wrap",
893
+ minWidth: 0
894
+ },
895
+ children: [
896
+ /* @__PURE__ */ jsxRuntime.jsx(Heading, { size: "md", children: title }),
897
+ status
898
+ ]
899
+ }
900
+ ),
901
+ actions ? /* @__PURE__ */ jsxRuntime.jsx("div", { style: { display: "flex", alignItems: "center", gap: "0.5rem", flexShrink: 0 }, children: actions }) : null
902
+ ]
903
+ }
904
+ ),
905
+ description ? /* @__PURE__ */ jsxRuntime.jsx(
906
+ Text,
907
+ {
908
+ size: "sm",
909
+ tone: "muted",
910
+ className: descriptionLines > 0 ? "nb-page-header__desc" : void 0,
911
+ style: { maxWidth: "72ch", ...descVars },
912
+ children: description
913
+ }
914
+ ) : null
915
+ ]
916
+ }
917
+ );
918
+ }
806
919
  function Pagination({ page, pageCount, onChange, style, ...rest }) {
807
920
  const total = Math.max(1, pageCount);
808
921
  const atStart = page <= 1;
@@ -834,8 +947,8 @@ function Pagination({ page, pageCount, onChange, style, ...rest }) {
834
947
  }
835
948
  );
836
949
  }
837
- var STYLE_ID5 = "nb-synapse-prose";
838
- var RULES5 = `
950
+ var STYLE_ID6 = "nb-synapse-prose";
951
+ var RULES6 = `
839
952
  .nb-prose {
840
953
  font-family: ${tokens.fontSans};
841
954
  font-size: ${tokens.textBaseSize};
@@ -860,15 +973,15 @@ var RULES5 = `
860
973
  .nb-prose strong { font-weight: ${tokens.weightSemibold}; }
861
974
  `;
862
975
  function Prose({ html, children, className, ...rest }) {
863
- chunkRX6XCLFF_cjs.ensureStyle(STYLE_ID5, RULES5);
976
+ chunkRX6XCLFF_cjs.ensureStyle(STYLE_ID6, RULES6);
864
977
  const cls = `nb-prose ${className ?? ""}`.trim();
865
978
  if (html !== void 0) {
866
979
  return /* @__PURE__ */ jsxRuntime.jsx("div", { className: cls, dangerouslySetInnerHTML: { __html: html }, ...rest });
867
980
  }
868
981
  return /* @__PURE__ */ jsxRuntime.jsx("div", { className: cls, ...rest, children });
869
982
  }
870
- var STYLE_ID6 = "nb-synapse-searchfield";
871
- var RULES6 = `
983
+ var STYLE_ID7 = "nb-synapse-searchfield";
984
+ var RULES7 = `
872
985
  .nb-search { font-family: var(--nb-search-font); color: var(--nb-search-fg); outline: none; }
873
986
  .nb-search::placeholder { color: var(--nb-search-placeholder); }
874
987
  .nb-search--underline {
@@ -898,7 +1011,7 @@ function SearchField({
898
1011
  type = "search",
899
1012
  ...rest
900
1013
  }) {
901
- chunkRX6XCLFF_cjs.ensureStyle(STYLE_ID6, RULES6);
1014
+ chunkRX6XCLFF_cjs.ensureStyle(STYLE_ID7, RULES7);
902
1015
  const fieldStyle = {
903
1016
  "--nb-search-font": tokens.fontSans,
904
1017
  "--nb-search-fg": tokens.fg,
@@ -921,8 +1034,8 @@ function SearchField({
921
1034
  }
922
1035
  );
923
1036
  }
924
- var STYLE_ID7 = "nb-synapse-segmented";
925
- var RULES7 = `
1037
+ var STYLE_ID8 = "nb-synapse-segmented";
1038
+ var RULES8 = `
926
1039
  .nb-seg {
927
1040
  display: inline-flex;
928
1041
  gap: 2px;
@@ -957,7 +1070,7 @@ function SegmentedControl({
957
1070
  className,
958
1071
  ...rest
959
1072
  }) {
960
- chunkRX6XCLFF_cjs.ensureStyle(STYLE_ID7, RULES7);
1073
+ chunkRX6XCLFF_cjs.ensureStyle(STYLE_ID8, RULES8);
961
1074
  const trackStyle = {
962
1075
  "--nb-seg-radius": tokens.radiusSm,
963
1076
  "--nb-seg-track": tokens.bgSubtle,
@@ -986,14 +1099,14 @@ function SegmentedControl({
986
1099
  )) })
987
1100
  );
988
1101
  }
989
- var STYLE_ID8 = "nb-synapse-spinner";
990
- var RULES8 = `
1102
+ var STYLE_ID9 = "nb-synapse-spinner";
1103
+ var RULES9 = `
991
1104
  @keyframes nb-spin { to { transform: rotate(360deg); } }
992
1105
  .nb-spinner { animation: nb-spin 0.7s linear infinite; }
993
1106
  @media (prefers-reduced-motion: reduce) { .nb-spinner { animation-duration: 1.6s; } }
994
1107
  `;
995
1108
  function Spinner({ size = 16, color, style, className, ...rest }) {
996
- chunkRX6XCLFF_cjs.ensureStyle(STYLE_ID8, RULES8);
1109
+ chunkRX6XCLFF_cjs.ensureStyle(STYLE_ID9, RULES9);
997
1110
  const spinnerStyle = {
998
1111
  display: "inline-block",
999
1112
  flexShrink: 0,
@@ -1022,7 +1135,7 @@ var COLOR = {
1022
1135
  failed: tokens.danger,
1023
1136
  idle: tokens.fgFaint
1024
1137
  };
1025
- var STYLE_ID9 = "nb-synapse-statusdot";
1138
+ var STYLE_ID10 = "nb-synapse-statusdot";
1026
1139
  var KEYFRAMES = `
1027
1140
  @keyframes nb-statusdot-pulse {
1028
1141
  0% { box-shadow: 0 0 0 0 var(--nb-statusdot-ring); transform: scale(1); }
@@ -1044,7 +1157,7 @@ function StatusDot({
1044
1157
  className,
1045
1158
  ...rest
1046
1159
  }) {
1047
- chunkRX6XCLFF_cjs.ensureStyle(STYLE_ID9, KEYFRAMES);
1160
+ chunkRX6XCLFF_cjs.ensureStyle(STYLE_ID10, KEYFRAMES);
1048
1161
  const dotColor = color ?? COLOR[status];
1049
1162
  const working = status === "working";
1050
1163
  const dotStyle = {
@@ -1067,8 +1180,8 @@ function StatusDot({
1067
1180
  }
1068
1181
  );
1069
1182
  }
1070
- var STYLE_ID10 = "nb-synapse-table";
1071
- var RULES9 = `
1183
+ var STYLE_ID11 = "nb-synapse-table";
1184
+ var RULES10 = `
1072
1185
  .nb-table { width: 100%; border-collapse: collapse; font-family: var(--nb-table-font); }
1073
1186
  .nb-table th, .nb-table td {
1074
1187
  padding: 0.55rem 0.75rem;
@@ -1093,11 +1206,12 @@ function Table({
1093
1206
  rowKey,
1094
1207
  onRowClick,
1095
1208
  empty,
1209
+ minWidth,
1096
1210
  style,
1097
1211
  className,
1098
1212
  ...rest
1099
1213
  }) {
1100
- chunkRX6XCLFF_cjs.ensureStyle(STYLE_ID10, RULES9);
1214
+ chunkRX6XCLFF_cjs.ensureStyle(STYLE_ID11, RULES10);
1101
1215
  const vars = {
1102
1216
  "--nb-table-font": tokens.fontSans,
1103
1217
  "--nb-table-fg": tokens.fg,
@@ -1113,8 +1227,14 @@ function Table({
1113
1227
  if (data.length === 0 && empty !== void 0) {
1114
1228
  return /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: empty });
1115
1229
  }
1116
- const tableStyle = { ...vars, ...style };
1117
- return /* @__PURE__ */ jsxRuntime.jsxs("table", { className: `nb-table ${className ?? ""}`.trim(), style: tableStyle, ...rest, children: [
1230
+ const fixed = columns.some((col) => col.width !== void 0);
1231
+ const tableStyle = {
1232
+ ...vars,
1233
+ ...fixed ? { tableLayout: "fixed" } : {},
1234
+ ...minWidth !== void 0 ? { minWidth } : {},
1235
+ ...style
1236
+ };
1237
+ const table = /* @__PURE__ */ jsxRuntime.jsxs("table", { className: `nb-table ${className ?? ""}`.trim(), style: tableStyle, ...rest, children: [
1118
1238
  /* @__PURE__ */ jsxRuntime.jsx("thead", { children: /* @__PURE__ */ jsxRuntime.jsx("tr", { children: columns.map((col) => /* @__PURE__ */ jsxRuntime.jsx(
1119
1239
  "th",
1120
1240
  {
@@ -1144,6 +1264,141 @@ function Table({
1144
1264
  );
1145
1265
  }) })
1146
1266
  ] });
1267
+ if (minWidth === void 0) return table;
1268
+ const label = rest["aria-label"];
1269
+ const named = label ? { role: "region", "aria-label": label } : {};
1270
+ return /* @__PURE__ */ jsxRuntime.jsx(
1271
+ "div",
1272
+ {
1273
+ tabIndex: 0,
1274
+ ...named,
1275
+ style: { overflowX: "auto", maxWidth: "100%" },
1276
+ children: table
1277
+ }
1278
+ );
1279
+ }
1280
+ var STYLE_ID12 = "nb-synapse-tabs";
1281
+ var RULES11 = `
1282
+ .nb-tabs {
1283
+ display: flex;
1284
+ align-items: stretch;
1285
+ gap: var(--nb-tabs-gap);
1286
+ border-bottom: var(--nb-tabs-rule);
1287
+ overflow-x: auto;
1288
+ scrollbar-width: none;
1289
+ }
1290
+ .nb-tabs::-webkit-scrollbar { display: none; }
1291
+ .nb-tabs__tab {
1292
+ border: none;
1293
+ background: transparent;
1294
+ color: var(--nb-tabs-fg);
1295
+ font-family: var(--nb-tabs-font);
1296
+ font-weight: var(--nb-tabs-weight);
1297
+ font-size: var(--nb-tabs-size);
1298
+ line-height: var(--nb-tabs-line);
1299
+ padding: 0.4rem 0.15rem 0.55rem;
1300
+ cursor: pointer;
1301
+ white-space: nowrap;
1302
+ position: relative;
1303
+ transition: color 130ms ease;
1304
+ }
1305
+ .nb-tabs__tab::after {
1306
+ content: "";
1307
+ position: absolute;
1308
+ /* bottom 0, not -1px. overflow-x auto makes the computed overflow-y auto as well, and
1309
+ clipping happens at the PADDING box - so a negative offset put 1px of the 2px accent
1310
+ outside it: the underline rendered half-height and left 1px of phantom overflow. */
1311
+ left: 0; right: 0; bottom: 0;
1312
+ height: 2px;
1313
+ border-radius: 2px 2px 0 0;
1314
+ background: transparent;
1315
+ transition: background 130ms ease;
1316
+ }
1317
+ .nb-tabs__tab:hover { color: var(--nb-tabs-fg-strong); }
1318
+ .nb-tabs__tab[aria-selected="true"] {
1319
+ color: var(--nb-tabs-fg-strong);
1320
+ font-weight: var(--nb-tabs-weight-active);
1321
+ }
1322
+ .nb-tabs__tab[aria-selected="true"]::after { background: var(--nb-tabs-accent); }
1323
+ .nb-tabs__tab:focus-visible {
1324
+ outline: 2px solid var(--nb-tabs-ring);
1325
+ outline-offset: -2px;
1326
+ border-radius: var(--nb-tabs-radius);
1327
+ }
1328
+ `;
1329
+ function Tabs({
1330
+ tabs,
1331
+ value,
1332
+ onChange,
1333
+ label,
1334
+ style,
1335
+ className,
1336
+ onKeyDown: callerKeyDown,
1337
+ ...rest
1338
+ }) {
1339
+ chunkRX6XCLFF_cjs.ensureStyle(STYLE_ID12, RULES11);
1340
+ const barRef = react.useRef(null);
1341
+ const barStyle = {
1342
+ "--nb-tabs-gap": "1.25rem",
1343
+ "--nb-tabs-rule": `${tokens.borderWidth} solid ${tokens.border}`,
1344
+ "--nb-tabs-fg": tokens.fgMuted,
1345
+ "--nb-tabs-fg-strong": tokens.fg,
1346
+ "--nb-tabs-accent": tokens.accent,
1347
+ "--nb-tabs-ring": tokens.ring,
1348
+ "--nb-tabs-font": tokens.fontSans,
1349
+ "--nb-tabs-weight": tokens.weightMedium,
1350
+ "--nb-tabs-weight-active": tokens.weightSemibold,
1351
+ "--nb-tabs-size": tokens.textSmSize,
1352
+ "--nb-tabs-line": tokens.textSmLine,
1353
+ "--nb-tabs-radius": tokens.radiusXs,
1354
+ ...style
1355
+ };
1356
+ const onKeyDown = (e) => {
1357
+ callerKeyDown?.(e);
1358
+ if (e.defaultPrevented) return;
1359
+ const at = tabs.findIndex((t) => t.value === value);
1360
+ if (at < 0) return;
1361
+ let to;
1362
+ if (e.key === "ArrowRight") to = (at + 1) % tabs.length;
1363
+ else if (e.key === "ArrowLeft") to = (at - 1 + tabs.length) % tabs.length;
1364
+ else if (e.key === "Home") to = 0;
1365
+ else if (e.key === "End") to = tabs.length - 1;
1366
+ else return;
1367
+ e.preventDefault();
1368
+ if (to !== at) onChange(tabs[to].value);
1369
+ barRef.current?.querySelectorAll('[role="tab"]')[to]?.focus();
1370
+ };
1371
+ return /* @__PURE__ */ jsxRuntime.jsx(
1372
+ "div",
1373
+ {
1374
+ ...rest,
1375
+ ref: barRef,
1376
+ role: "tablist",
1377
+ "aria-label": label,
1378
+ className: `nb-tabs ${className ?? ""}`.trim(),
1379
+ style: barStyle,
1380
+ onKeyDown,
1381
+ children: tabs.map((tab) => {
1382
+ const selected = tab.value === value;
1383
+ return /* @__PURE__ */ jsxRuntime.jsxs(
1384
+ "button",
1385
+ {
1386
+ type: "button",
1387
+ role: "tab",
1388
+ "aria-selected": selected,
1389
+ tabIndex: selected ? 0 : -1,
1390
+ className: "nb-tabs__tab",
1391
+ onClick: () => onChange(tab.value),
1392
+ children: [
1393
+ tab.label,
1394
+ tab.count !== void 0 ? /* @__PURE__ */ jsxRuntime.jsx("span", { style: { marginLeft: "0.35rem", color: tokens.fgFaint }, children: tab.count }) : null
1395
+ ]
1396
+ },
1397
+ tab.value
1398
+ );
1399
+ })
1400
+ }
1401
+ );
1147
1402
  }
1148
1403
  var READING_MAX = 760;
1149
1404
  var ContentWidthCtx = react.createContext("full");
@@ -1289,14 +1544,64 @@ function Back({ children, style }) {
1289
1544
  return /* @__PURE__ */ jsxRuntime.jsx(TextLink, { onClick: () => onBack?.(), style: { marginBottom: "0.5rem", ...style }, children: children ?? "\u2190 Back" });
1290
1545
  }
1291
1546
  var ListDetailLayout = Object.assign(ListDetailLayoutRoot, { List, Detail, Back });
1547
+ function PageLayout({
1548
+ crumbs,
1549
+ title,
1550
+ status,
1551
+ actions,
1552
+ description,
1553
+ descriptionLines,
1554
+ tabs,
1555
+ toolbar,
1556
+ children,
1557
+ style,
1558
+ ...rest
1559
+ }) {
1560
+ return /* @__PURE__ */ jsxRuntime.jsxs(
1561
+ "div",
1562
+ {
1563
+ style: { display: "flex", flexDirection: "column", gap: "1.1rem", minWidth: 0, ...style },
1564
+ ...rest,
1565
+ children: [
1566
+ /* @__PURE__ */ jsxRuntime.jsx(
1567
+ PageHeader,
1568
+ {
1569
+ crumbs,
1570
+ title,
1571
+ status,
1572
+ actions,
1573
+ description,
1574
+ descriptionLines
1575
+ }
1576
+ ),
1577
+ tabs,
1578
+ toolbar ? /* @__PURE__ */ jsxRuntime.jsx(
1579
+ "div",
1580
+ {
1581
+ style: {
1582
+ display: "flex",
1583
+ alignItems: "center",
1584
+ gap: "0.5rem",
1585
+ flexWrap: "wrap",
1586
+ minWidth: 0,
1587
+ marginBottom: "-0.35rem"
1588
+ },
1589
+ children: toolbar
1590
+ }
1591
+ ) : null,
1592
+ children
1593
+ ]
1594
+ }
1595
+ );
1596
+ }
1292
1597
  var SidebarCtx = react.createContext(null);
1293
1598
  function useSidebar() {
1294
1599
  const ctx = react.useContext(SidebarCtx);
1295
1600
  if (!ctx) throw new Error("useSidebar must be used within <SidebarLayout>");
1296
1601
  return ctx;
1297
1602
  }
1298
- var STYLE_ID11 = "nb-synapse-sidebarlayout";
1299
- var RULES10 = `
1603
+ var STYLE_ID13 = "nb-synapse-sidebarlayout";
1604
+ var RULES12 = `
1300
1605
  .nb-sidebar-drawer { transition: transform 220ms cubic-bezier(0.2, 0, 0, 1); }
1301
1606
  .nb-sidebar-scrim { animation: nb-sidebar-fade 180ms ease; }
1302
1607
  @keyframes nb-sidebar-fade { from { opacity: 0; } to { opacity: 1; } }
@@ -1346,7 +1651,7 @@ function SidebarLayoutRoot({
1346
1651
  }
1347
1652
  function Sidebar({ style, children, ...rest }) {
1348
1653
  const { collapsed, mode, side, width, open, setOpen } = useSidebar();
1349
- chunkRX6XCLFF_cjs.ensureStyle(STYLE_ID11, RULES10);
1654
+ chunkRX6XCLFF_cjs.ensureStyle(STYLE_ID13, RULES12);
1350
1655
  if (!collapsed) {
1351
1656
  const border = `${tokens.borderWidth} solid ${tokens.border}`;
1352
1657
  return /* @__PURE__ */ jsxRuntime.jsx(
@@ -1474,6 +1779,7 @@ var SidebarLayout = Object.assign(SidebarLayoutRoot, { Sidebar, Main, Trigger })
1474
1779
  exports.AppFrame = AppFrame;
1475
1780
  exports.Avatar = Avatar;
1476
1781
  exports.Badge = Badge;
1782
+ exports.Breadcrumb = Breadcrumb;
1477
1783
  exports.Button = Button;
1478
1784
  exports.Card = Card;
1479
1785
  exports.Divider = Divider;
@@ -1483,6 +1789,8 @@ exports.Heading = Heading;
1483
1789
  exports.Inline = Inline;
1484
1790
  exports.ListDetailLayout = ListDetailLayout;
1485
1791
  exports.ListRow = ListRow;
1792
+ exports.PageHeader = PageHeader;
1793
+ exports.PageLayout = PageLayout;
1486
1794
  exports.Pagination = Pagination;
1487
1795
  exports.Prose = Prose;
1488
1796
  exports.SearchField = SearchField;
@@ -1493,6 +1801,7 @@ exports.Spinner = Spinner;
1493
1801
  exports.Stack = Stack;
1494
1802
  exports.StatusDot = StatusDot;
1495
1803
  exports.Table = Table;
1804
+ exports.Tabs = Tabs;
1496
1805
  exports.Text = Text;
1497
1806
  exports.TextLink = TextLink;
1498
1807
  exports.headingStyle = headingStyle;