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