@nimblebrain/synapse 0.15.0 → 0.17.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.
Files changed (59) hide show
  1. package/README.md +61 -111
  2. package/dist/{chunk-SN7YXLAV.cjs → chunk-EHLQWCYV.cjs} +6 -6
  3. package/dist/{chunk-SN7YXLAV.cjs.map → chunk-EHLQWCYV.cjs.map} +1 -1
  4. package/dist/{chunk-ZMNYUGSF.cjs → chunk-FDMRJZKT.cjs} +576 -662
  5. package/dist/chunk-FDMRJZKT.cjs.map +1 -0
  6. package/dist/{chunk-E7OOOYVR.js → chunk-SRQNQ26I.js} +3 -3
  7. package/dist/{chunk-E7OOOYVR.js.map → chunk-SRQNQ26I.js.map} +1 -1
  8. package/dist/{chunk-RYWW6EHB.cjs → chunk-TXJMGQYF.cjs} +3 -5
  9. package/dist/chunk-TXJMGQYF.cjs.map +1 -0
  10. package/dist/{chunk-J5ICAHK2.js → chunk-UPDBDXQB.js} +4 -5
  11. package/dist/chunk-UPDBDXQB.js.map +1 -0
  12. package/dist/{chunk-GNTQJSQX.js → chunk-VYR6V47N.js} +573 -663
  13. package/dist/chunk-VYR6V47N.js.map +1 -0
  14. package/dist/codegen/cli.cjs +1 -1
  15. package/dist/codegen/cli.js +1 -1
  16. package/dist/codegen/index.d.cts +1 -1
  17. package/dist/codegen/index.d.ts +1 -1
  18. package/dist/connect.iife.global.js +28 -28
  19. package/dist/{detect-C_qmYsh9.d.cts → detect-BHYg26_d.d.cts} +1 -1
  20. package/dist/{detect-Dfd0upoz.d.ts → detect-Bf8Q_0dK.d.ts} +1 -1
  21. package/dist/host/index.cjs +14 -14
  22. package/dist/host/index.d.cts +3 -3
  23. package/dist/host/index.d.ts +3 -3
  24. package/dist/host/index.js +2 -2
  25. package/dist/index.cjs +28 -87
  26. package/dist/index.cjs.map +1 -1
  27. package/dist/index.d.cts +58 -26
  28. package/dist/index.d.ts +58 -26
  29. package/dist/index.js +3 -81
  30. package/dist/index.js.map +1 -1
  31. package/dist/react/index.cjs +81 -140
  32. package/dist/react/index.cjs.map +1 -1
  33. package/dist/react/index.d.cts +66 -82
  34. package/dist/react/index.d.ts +66 -82
  35. package/dist/react/index.js +82 -136
  36. package/dist/react/index.js.map +1 -1
  37. package/dist/{server-ERRDGEZ7.js → server-5N76YCWC.js} +3 -3
  38. package/dist/{server-ERRDGEZ7.js.map → server-5N76YCWC.js.map} +1 -1
  39. package/dist/{server-P54IYX2G.cjs → server-QIYIJHD5.cjs} +3 -3
  40. package/dist/{server-P54IYX2G.cjs.map → server-QIYIJHD5.cjs.map} +1 -1
  41. package/dist/synapse-runtime.iife.global.js +28 -28
  42. package/dist/synapse-ui.iife.global.js +2 -2
  43. package/dist/{types-BSZasM8q.d.cts → types-uEO4VFJ2.d.cts} +87 -166
  44. package/dist/{types-BSZasM8q.d.ts → types-uEO4VFJ2.d.ts} +87 -166
  45. package/dist/ui/index.cjs +403 -91
  46. package/dist/ui/index.cjs.map +1 -1
  47. package/dist/ui/index.d.cts +115 -3
  48. package/dist/ui/index.d.ts +115 -3
  49. package/dist/ui/index.js +401 -93
  50. package/dist/ui/index.js.map +1 -1
  51. package/dist/vite/index.cjs +1 -1
  52. package/dist/vite/index.cjs.map +1 -1
  53. package/dist/vite/index.js +1 -1
  54. package/dist/vite/index.js.map +1 -1
  55. package/package.json +1 -1
  56. package/dist/chunk-GNTQJSQX.js.map +0 -1
  57. package/dist/chunk-J5ICAHK2.js.map +0 -1
  58. package/dist/chunk-RYWW6EHB.cjs.map +0 -1
  59. package/dist/chunk-ZMNYUGSF.cjs.map +0 -1
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
@@ -160,6 +160,81 @@ function Badge({ tone = "neutral", style, children, ...rest }) {
160
160
  }
161
161
  );
162
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
+ }
163
238
  var STYLE_ID = "nb-synapse-button";
164
239
  var RULES = `
165
240
  .nb-btn {
@@ -260,6 +335,41 @@ function TextLink({ tone = "muted", style, className, children, ...rest }) {
260
335
  }
261
336
  );
262
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
+ }
263
373
  var STYLE_ID2 = "nb-synapse-card";
264
374
  var RULES2 = `
265
375
  .nb-card--interactive {
@@ -608,75 +718,6 @@ function Divider({ orientation = "horizontal", style, ...rest }) {
608
718
  }
609
719
  );
610
720
  }
611
- var TONE2 = {
612
- default: tokens.fg,
613
- muted: tokens.fgMuted,
614
- faint: tokens.fgFaint,
615
- accent: tokens.accent,
616
- danger: tokens.danger,
617
- success: tokens.success
618
- };
619
- var WEIGHT = {
620
- normal: tokens.weightNormal,
621
- medium: tokens.weightMedium,
622
- semibold: tokens.weightSemibold,
623
- bold: tokens.weightBold
624
- };
625
- function Text({
626
- size = "base",
627
- weight = "normal",
628
- tone = "default",
629
- mono = false,
630
- truncate = false,
631
- as: As = "span",
632
- style,
633
- children,
634
- ...rest
635
- }) {
636
- const truncation = truncate ? { overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" } : {};
637
- return /* @__PURE__ */ jsx(
638
- As,
639
- {
640
- style: {
641
- margin: 0,
642
- fontFamily: mono ? tokens.fontMono : tokens.fontSans,
643
- fontWeight: WEIGHT[weight],
644
- color: TONE2[tone],
645
- ...textStyle(size),
646
- ...truncation,
647
- ...style
648
- },
649
- ...rest,
650
- children
651
- }
652
- );
653
- }
654
- function Heading({
655
- size = "md",
656
- weight = "semibold",
657
- tone = "default",
658
- as: As = "h2",
659
- style,
660
- children,
661
- ...rest
662
- }) {
663
- return /* @__PURE__ */ jsx(
664
- As,
665
- {
666
- style: {
667
- margin: 0,
668
- fontFamily: tokens.fontHeading,
669
- fontWeight: WEIGHT[weight],
670
- color: TONE2[tone],
671
- letterSpacing: "-0.015em",
672
- ...headingStyle(size),
673
- ...style
674
- },
675
- ...rest,
676
- children
677
- }
678
- );
679
- }
680
721
  function EmptyState({ icon, title, description, action, style, ...rest }) {
681
722
  return /* @__PURE__ */ jsxs(
682
723
  Stack,
@@ -798,6 +839,81 @@ function ListRow({
798
839
  }
799
840
  );
800
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
+ }
801
917
  function Pagination({ page, pageCount, onChange, style, ...rest }) {
802
918
  const total = Math.max(1, pageCount);
803
919
  const atStart = page <= 1;
@@ -829,8 +945,8 @@ function Pagination({ page, pageCount, onChange, style, ...rest }) {
829
945
  }
830
946
  );
831
947
  }
832
- var STYLE_ID5 = "nb-synapse-prose";
833
- var RULES5 = `
948
+ var STYLE_ID6 = "nb-synapse-prose";
949
+ var RULES6 = `
834
950
  .nb-prose {
835
951
  font-family: ${tokens.fontSans};
836
952
  font-size: ${tokens.textBaseSize};
@@ -855,15 +971,15 @@ var RULES5 = `
855
971
  .nb-prose strong { font-weight: ${tokens.weightSemibold}; }
856
972
  `;
857
973
  function Prose({ html, children, className, ...rest }) {
858
- ensureStyle(STYLE_ID5, RULES5);
974
+ ensureStyle(STYLE_ID6, RULES6);
859
975
  const cls = `nb-prose ${className ?? ""}`.trim();
860
976
  if (html !== void 0) {
861
977
  return /* @__PURE__ */ jsx("div", { className: cls, dangerouslySetInnerHTML: { __html: html }, ...rest });
862
978
  }
863
979
  return /* @__PURE__ */ jsx("div", { className: cls, ...rest, children });
864
980
  }
865
- var STYLE_ID6 = "nb-synapse-searchfield";
866
- var RULES6 = `
981
+ var STYLE_ID7 = "nb-synapse-searchfield";
982
+ var RULES7 = `
867
983
  .nb-search { font-family: var(--nb-search-font); color: var(--nb-search-fg); outline: none; }
868
984
  .nb-search::placeholder { color: var(--nb-search-placeholder); }
869
985
  .nb-search--underline {
@@ -893,7 +1009,7 @@ function SearchField({
893
1009
  type = "search",
894
1010
  ...rest
895
1011
  }) {
896
- ensureStyle(STYLE_ID6, RULES6);
1012
+ ensureStyle(STYLE_ID7, RULES7);
897
1013
  const fieldStyle = {
898
1014
  "--nb-search-font": tokens.fontSans,
899
1015
  "--nb-search-fg": tokens.fg,
@@ -916,8 +1032,8 @@ function SearchField({
916
1032
  }
917
1033
  );
918
1034
  }
919
- var STYLE_ID7 = "nb-synapse-segmented";
920
- var RULES7 = `
1035
+ var STYLE_ID8 = "nb-synapse-segmented";
1036
+ var RULES8 = `
921
1037
  .nb-seg {
922
1038
  display: inline-flex;
923
1039
  gap: 2px;
@@ -952,7 +1068,7 @@ function SegmentedControl({
952
1068
  className,
953
1069
  ...rest
954
1070
  }) {
955
- ensureStyle(STYLE_ID7, RULES7);
1071
+ ensureStyle(STYLE_ID8, RULES8);
956
1072
  const trackStyle = {
957
1073
  "--nb-seg-radius": tokens.radiusSm,
958
1074
  "--nb-seg-track": tokens.bgSubtle,
@@ -981,14 +1097,14 @@ function SegmentedControl({
981
1097
  )) })
982
1098
  );
983
1099
  }
984
- var STYLE_ID8 = "nb-synapse-spinner";
985
- var RULES8 = `
1100
+ var STYLE_ID9 = "nb-synapse-spinner";
1101
+ var RULES9 = `
986
1102
  @keyframes nb-spin { to { transform: rotate(360deg); } }
987
1103
  .nb-spinner { animation: nb-spin 0.7s linear infinite; }
988
1104
  @media (prefers-reduced-motion: reduce) { .nb-spinner { animation-duration: 1.6s; } }
989
1105
  `;
990
1106
  function Spinner({ size = 16, color, style, className, ...rest }) {
991
- ensureStyle(STYLE_ID8, RULES8);
1107
+ ensureStyle(STYLE_ID9, RULES9);
992
1108
  const spinnerStyle = {
993
1109
  display: "inline-block",
994
1110
  flexShrink: 0,
@@ -1017,7 +1133,7 @@ var COLOR = {
1017
1133
  failed: tokens.danger,
1018
1134
  idle: tokens.fgFaint
1019
1135
  };
1020
- var STYLE_ID9 = "nb-synapse-statusdot";
1136
+ var STYLE_ID10 = "nb-synapse-statusdot";
1021
1137
  var KEYFRAMES = `
1022
1138
  @keyframes nb-statusdot-pulse {
1023
1139
  0% { box-shadow: 0 0 0 0 var(--nb-statusdot-ring); transform: scale(1); }
@@ -1039,7 +1155,7 @@ function StatusDot({
1039
1155
  className,
1040
1156
  ...rest
1041
1157
  }) {
1042
- ensureStyle(STYLE_ID9, KEYFRAMES);
1158
+ ensureStyle(STYLE_ID10, KEYFRAMES);
1043
1159
  const dotColor = color ?? COLOR[status];
1044
1160
  const working = status === "working";
1045
1161
  const dotStyle = {
@@ -1062,8 +1178,8 @@ function StatusDot({
1062
1178
  }
1063
1179
  );
1064
1180
  }
1065
- var STYLE_ID10 = "nb-synapse-table";
1066
- var RULES9 = `
1181
+ var STYLE_ID11 = "nb-synapse-table";
1182
+ var RULES10 = `
1067
1183
  .nb-table { width: 100%; border-collapse: collapse; font-family: var(--nb-table-font); }
1068
1184
  .nb-table th, .nb-table td {
1069
1185
  padding: 0.55rem 0.75rem;
@@ -1088,11 +1204,12 @@ function Table({
1088
1204
  rowKey,
1089
1205
  onRowClick,
1090
1206
  empty,
1207
+ minWidth,
1091
1208
  style,
1092
1209
  className,
1093
1210
  ...rest
1094
1211
  }) {
1095
- ensureStyle(STYLE_ID10, RULES9);
1212
+ ensureStyle(STYLE_ID11, RULES10);
1096
1213
  const vars = {
1097
1214
  "--nb-table-font": tokens.fontSans,
1098
1215
  "--nb-table-fg": tokens.fg,
@@ -1108,8 +1225,14 @@ function Table({
1108
1225
  if (data.length === 0 && empty !== void 0) {
1109
1226
  return /* @__PURE__ */ jsx(Fragment, { children: empty });
1110
1227
  }
1111
- const tableStyle = { ...vars, ...style };
1112
- 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: [
1113
1236
  /* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsx("tr", { children: columns.map((col) => /* @__PURE__ */ jsx(
1114
1237
  "th",
1115
1238
  {
@@ -1139,6 +1262,141 @@ function Table({
1139
1262
  );
1140
1263
  }) })
1141
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
+ );
1142
1400
  }
1143
1401
  var READING_MAX = 760;
1144
1402
  var ContentWidthCtx = createContext("full");
@@ -1284,14 +1542,64 @@ function Back({ children, style }) {
1284
1542
  return /* @__PURE__ */ jsx(TextLink, { onClick: () => onBack?.(), style: { marginBottom: "0.5rem", ...style }, children: children ?? "\u2190 Back" });
1285
1543
  }
1286
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
+ }
1287
1595
  var SidebarCtx = createContext(null);
1288
1596
  function useSidebar() {
1289
1597
  const ctx = useContext(SidebarCtx);
1290
1598
  if (!ctx) throw new Error("useSidebar must be used within <SidebarLayout>");
1291
1599
  return ctx;
1292
1600
  }
1293
- var STYLE_ID11 = "nb-synapse-sidebarlayout";
1294
- var RULES10 = `
1601
+ var STYLE_ID13 = "nb-synapse-sidebarlayout";
1602
+ var RULES12 = `
1295
1603
  .nb-sidebar-drawer { transition: transform 220ms cubic-bezier(0.2, 0, 0, 1); }
1296
1604
  .nb-sidebar-scrim { animation: nb-sidebar-fade 180ms ease; }
1297
1605
  @keyframes nb-sidebar-fade { from { opacity: 0; } to { opacity: 1; } }
@@ -1341,7 +1649,7 @@ function SidebarLayoutRoot({
1341
1649
  }
1342
1650
  function Sidebar({ style, children, ...rest }) {
1343
1651
  const { collapsed, mode, side, width, open, setOpen } = useSidebar();
1344
- ensureStyle(STYLE_ID11, RULES10);
1652
+ ensureStyle(STYLE_ID13, RULES12);
1345
1653
  if (!collapsed) {
1346
1654
  const border = `${tokens.borderWidth} solid ${tokens.border}`;
1347
1655
  return /* @__PURE__ */ jsx(
@@ -1466,6 +1774,6 @@ function Trigger({ children, style, onClick, ...rest }) {
1466
1774
  }
1467
1775
  var SidebarLayout = Object.assign(SidebarLayoutRoot, { Sidebar, Main, Trigger });
1468
1776
 
1469
- 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 };
1470
1778
  //# sourceMappingURL=index.js.map
1471
1779
  //# sourceMappingURL=index.js.map