@designa8/vibedesign-stripe-deepseek-85ea 0.2.1 → 0.2.2

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/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @designa8/vibedesign-stripe-deepseek-85ea
2
2
 
3
- Complete design system generated by Atoms Design, version **0.2.1**.
3
+ Complete design system generated by Atoms Design, version **0.2.2**.
4
4
  Tokens, atoms, molecules, organisms, and templates — import from the package root;
5
5
  never hardcode visual values in consumer code.
6
6
 
package/dist/index.cjs CHANGED
@@ -14669,12 +14669,59 @@ var glyph3 = (name, size = 16, strokeWidth = 1.75) => /* @__PURE__ */ (0, import
14669
14669
  style: { color: "inherit" }
14670
14670
  }
14671
14671
  );
14672
+ var hasBadge = (badge) => {
14673
+ if (badge === void 0 || badge === null) {
14674
+ return false;
14675
+ }
14676
+ if (typeof badge === "number") {
14677
+ return Number.isFinite(badge) && badge !== 0;
14678
+ }
14679
+ const trimmed = badge.trim();
14680
+ return trimmed.length > 0 && trimmed !== "0";
14681
+ };
14682
+ var badgeText = (badge) => typeof badge === "number" ? badge > 99 ? "99+" : String(badge) : badge.trim();
14683
+ var rowAccessibleName = (label, badge) => {
14684
+ if (!hasBadge(badge)) {
14685
+ return label;
14686
+ }
14687
+ return typeof badge === "number" ? `${label}, ${badge} pending` : `${label}, ${badgeText(badge)}`;
14688
+ };
14689
+ var RailBadge = ({
14690
+ badge,
14691
+ active
14692
+ }) => /* @__PURE__ */ (0, import_jsx_runtime74.jsx)(
14693
+ "span",
14694
+ {
14695
+ "aria-hidden": "true",
14696
+ "data-active": active || void 0,
14697
+ className: cn65(
14698
+ "ml-auto flex h-4 min-w-4 shrink-0 items-center justify-center rounded-full",
14699
+ "bg-primary px-xxs text-caption text-on-primary",
14700
+ "data-[active=true]:bg-on-primary data-[active=true]:text-primary"
14701
+ ),
14702
+ children: badgeText(badge)
14703
+ }
14704
+ );
14705
+ var railGlyph = (item, folded) => /* @__PURE__ */ (0, import_jsx_runtime74.jsxs)("span", { className: "relative inline-flex shrink-0 items-center", children: [
14706
+ glyph3(item.icon, 16, 1.75),
14707
+ folded && hasBadge(item.badge) ? /* @__PURE__ */ (0, import_jsx_runtime74.jsx)(
14708
+ "span",
14709
+ {
14710
+ "aria-hidden": "true",
14711
+ className: cn65(
14712
+ "pointer-events-none absolute -right-xxs -top-xxs size-2 rounded-full",
14713
+ "bg-primary",
14714
+ "group-data-[active=true]:bg-on-primary"
14715
+ )
14716
+ }
14717
+ ) : null
14718
+ ] });
14672
14719
  var RowLabel = ({
14673
14720
  folded,
14674
14721
  children
14675
14722
  }) => folded ? /* @__PURE__ */ (0, import_jsx_runtime74.jsx)("span", { className: "sr-only", children }) : /* @__PURE__ */ (0, import_jsx_runtime74.jsx)("span", { className: "truncate", children });
14676
14723
  var railRowClass = (active, folded) => cn65(
14677
- "flex w-full cursor-pointer items-center rounded-lg border-0 bg-transparent",
14724
+ "group flex w-full cursor-pointer items-center rounded-lg border-0 bg-transparent",
14678
14725
  "text-body-md transition duration-150 ease-out",
14679
14726
  "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-focus-blue",
14680
14727
  "focus-visible:ring-inset",
@@ -15010,6 +15057,7 @@ var SidebarDesktop = React74.forwardRef(
15010
15057
  className: "flex flex-col gap-xxs",
15011
15058
  children: navItems.map((item) => {
15012
15059
  const active = item.id === activeId;
15060
+ const badged = hasBadge(item.badge);
15013
15061
  if (!item.children?.length) {
15014
15062
  return /* @__PURE__ */ (0, import_jsx_runtime74.jsxs)(
15015
15063
  "a",
@@ -15017,11 +15065,13 @@ var SidebarDesktop = React74.forwardRef(
15017
15065
  href: item.href ?? `#${item.id}`,
15018
15066
  onClick: navigate({ id: item.id, href: item.href }),
15019
15067
  "aria-current": active ? "page" : void 0,
15020
- "aria-label": isFolded ? item.label : void 0,
15068
+ "data-active": active,
15069
+ "aria-label": isFolded || badged ? rowAccessibleName(item.label, item.badge) : void 0,
15021
15070
  className: railRowClass(active, isFolded),
15022
15071
  children: [
15023
- glyph3(item.icon, 16, 1.75),
15024
- /* @__PURE__ */ (0, import_jsx_runtime74.jsx)(RowLabel, { folded: isFolded, children: item.label })
15072
+ railGlyph(item, isFolded),
15073
+ /* @__PURE__ */ (0, import_jsx_runtime74.jsx)(RowLabel, { folded: isFolded, children: item.label }),
15074
+ !isFolded && badged ? /* @__PURE__ */ (0, import_jsx_runtime74.jsx)(RailBadge, { badge: item.badge, active }) : null
15025
15075
  ]
15026
15076
  },
15027
15077
  item.id
@@ -15037,22 +15087,32 @@ var SidebarDesktop = React74.forwardRef(
15037
15087
  size: "rail",
15038
15088
  "aria-expanded": expanded,
15039
15089
  "aria-controls": subId,
15040
- "aria-label": isFolded ? item.label : void 0,
15090
+ "aria-label": isFolded || badged ? rowAccessibleName(item.label, item.badge) : void 0,
15041
15091
  "data-active": active,
15042
15092
  "data-folded": isFolded,
15043
15093
  onClick: () => toggleExpanded(item.id),
15044
- leadingIcon: glyph3(item.icon, 16, 1.75),
15045
- trailingIcon: isFolded ? void 0 : /* @__PURE__ */ (0, import_jsx_runtime74.jsx)(
15046
- "span",
15047
- {
15048
- "aria-hidden": "true",
15049
- className: cn65(
15050
- "flex shrink-0 items-center transition-transform duration-200 ease-out",
15051
- expanded ? "rotate-180" : "rotate-0"
15052
- ),
15053
- children: glyph3("chevron-down", 16, 2)
15054
- }
15055
- ),
15094
+ className: "group",
15095
+ leadingIcon: railGlyph(item, isFolded),
15096
+ trailingIcon: isFolded ? void 0 : /* @__PURE__ */ (0, import_jsx_runtime74.jsxs)("span", { className: "flex items-center gap-xs", children: [
15097
+ badged ? /* @__PURE__ */ (0, import_jsx_runtime74.jsx)(
15098
+ RailBadge,
15099
+ {
15100
+ badge: item.badge,
15101
+ active
15102
+ }
15103
+ ) : null,
15104
+ /* @__PURE__ */ (0, import_jsx_runtime74.jsx)(
15105
+ "span",
15106
+ {
15107
+ "aria-hidden": "true",
15108
+ className: cn65(
15109
+ "flex shrink-0 items-center transition-transform duration-200 ease-out",
15110
+ expanded ? "rotate-180" : "rotate-0"
15111
+ ),
15112
+ children: glyph3("chevron-down", 16, 2)
15113
+ }
15114
+ )
15115
+ ] }),
15056
15116
  children: /* @__PURE__ */ (0, import_jsx_runtime74.jsx)(RowLabel, { folded: isFolded, children: item.label })
15057
15117
  }
15058
15118
  ),
@@ -15105,17 +15165,20 @@ var SidebarDesktop = React74.forwardRef(
15105
15165
  ),
15106
15166
  /* @__PURE__ */ (0, import_jsx_runtime74.jsx)("nav", { "aria-label": "Secondary", className: "mt-lg flex flex-col gap-xxs", children: secondaryNav.map((item) => {
15107
15167
  const active = item.id === secondaryActiveId;
15168
+ const badged = hasBadge(item.badge);
15108
15169
  return /* @__PURE__ */ (0, import_jsx_runtime74.jsxs)(
15109
15170
  "a",
15110
15171
  {
15111
15172
  href: item.href ?? `#${item.id}`,
15112
15173
  onClick: navigate({ id: item.id, href: item.href }),
15113
15174
  "aria-current": active ? "page" : void 0,
15114
- "aria-label": isFolded ? item.label : void 0,
15175
+ "data-active": active,
15176
+ "aria-label": isFolded || badged ? rowAccessibleName(item.label, item.badge) : void 0,
15115
15177
  className: railRowClass(active, isFolded),
15116
15178
  children: [
15117
- glyph3(item.icon, 16, 1.75),
15118
- /* @__PURE__ */ (0, import_jsx_runtime74.jsx)(RowLabel, { folded: isFolded, children: item.label })
15179
+ railGlyph(item, isFolded),
15180
+ /* @__PURE__ */ (0, import_jsx_runtime74.jsx)(RowLabel, { folded: isFolded, children: item.label }),
15181
+ !isFolded && badged ? /* @__PURE__ */ (0, import_jsx_runtime74.jsx)(RailBadge, { badge: item.badge, active }) : null
15119
15182
  ]
15120
15183
  },
15121
15184
  item.id
@@ -15242,6 +15305,39 @@ var SAMPLE_SECONDARY2 = [
15242
15305
  ];
15243
15306
  var SAMPLE_PLAN2 = { name: "Pro", price: "$2,400.00 / month", href: "#plan" };
15244
15307
  var glyph4 = (name, size = 18, strokeWidth = 1.75) => /* @__PURE__ */ (0, import_jsx_runtime75.jsx)(Icon, { name, size, strokeWidth, style: { color: "inherit" } });
15308
+ var hasBadge2 = (badge) => {
15309
+ if (badge === void 0 || badge === null) {
15310
+ return false;
15311
+ }
15312
+ if (typeof badge === "number") {
15313
+ return Number.isFinite(badge) && badge !== 0;
15314
+ }
15315
+ const trimmed = badge.trim();
15316
+ return trimmed.length > 0 && trimmed !== "0";
15317
+ };
15318
+ var badgeText2 = (badge) => typeof badge === "number" ? badge > 99 ? "99+" : String(badge) : badge.trim();
15319
+ var rowAccessibleName2 = (label, badge) => {
15320
+ if (!hasBadge2(badge)) {
15321
+ return label;
15322
+ }
15323
+ return typeof badge === "number" ? `${label}, ${badge} pending` : `${label}, ${badgeText2(badge)}`;
15324
+ };
15325
+ var NavBadge = ({
15326
+ badge,
15327
+ active
15328
+ }) => /* @__PURE__ */ (0, import_jsx_runtime75.jsx)(
15329
+ "span",
15330
+ {
15331
+ "aria-hidden": "true",
15332
+ "data-active": active || void 0,
15333
+ className: cn66(
15334
+ "ml-auto flex h-4 min-w-4 shrink-0 items-center justify-center rounded-full",
15335
+ "bg-primary px-xxs text-caption text-on-primary",
15336
+ "data-[active=true]:bg-on-primary data-[active=true]:text-primary"
15337
+ ),
15338
+ children: badgeText2(badge)
15339
+ }
15340
+ );
15245
15341
  var rowClass = (active) => cn66(
15246
15342
  "flex min-h-10 w-full cursor-pointer items-center gap-md rounded-lg px-md py-sm",
15247
15343
  "text-body-md transition duration-150 ease-out",
@@ -15554,6 +15650,7 @@ var SidebarMobile = React75.forwardRef(
15554
15650
  ] }),
15555
15651
  /* @__PURE__ */ (0, import_jsx_runtime75.jsx)("nav", { "aria-label": "Primary", className: "mt-md flex flex-col gap-xxs", children: navItems.map((item) => {
15556
15652
  const active = item.id === activeId;
15653
+ const badged = hasBadge2(item.badge);
15557
15654
  if (!item.children?.length) {
15558
15655
  return /* @__PURE__ */ (0, import_jsx_runtime75.jsxs)(
15559
15656
  "a",
@@ -15561,10 +15658,18 @@ var SidebarMobile = React75.forwardRef(
15561
15658
  href: item.href ?? `#${item.id}`,
15562
15659
  onClick: navigate({ id: item.id, href: item.href }),
15563
15660
  "aria-current": active ? "page" : void 0,
15661
+ "aria-label": badged ? rowAccessibleName2(item.label, item.badge) : void 0,
15564
15662
  className: rowClass(active),
15565
15663
  children: [
15566
15664
  glyph4(item.icon, 18, 1.75),
15567
- /* @__PURE__ */ (0, import_jsx_runtime75.jsx)("span", { className: "truncate", children: item.label })
15665
+ /* @__PURE__ */ (0, import_jsx_runtime75.jsx)("span", { className: "truncate", children: item.label }),
15666
+ badged ? /* @__PURE__ */ (0, import_jsx_runtime75.jsx)(
15667
+ NavBadge,
15668
+ {
15669
+ badge: item.badge,
15670
+ active
15671
+ }
15672
+ ) : null
15568
15673
  ]
15569
15674
  },
15570
15675
  item.id
@@ -15580,20 +15685,31 @@ var SidebarMobile = React75.forwardRef(
15580
15685
  size: "rail",
15581
15686
  "aria-expanded": expanded,
15582
15687
  "aria-controls": subId,
15688
+ "aria-label": badged ? rowAccessibleName2(item.label, item.badge) : void 0,
15583
15689
  "data-active": active,
15584
15690
  onClick: () => toggleExpanded(item.id),
15585
15691
  className: "px-md",
15586
15692
  leadingIcon: glyph4(item.icon, 18, 1.75),
15587
- trailingIcon: /* @__PURE__ */ (0, import_jsx_runtime75.jsx)(
15588
- "span",
15589
- {
15590
- className: cn66(
15591
- "flex shrink-0 items-center transition-transform duration-200 ease-out",
15592
- expanded ? "rotate-180" : "rotate-0"
15593
- ),
15594
- children: glyph4("chevron-down", 18, 2)
15595
- }
15596
- ),
15693
+ trailingIcon: /* @__PURE__ */ (0, import_jsx_runtime75.jsxs)("span", { className: "flex items-center gap-xs", children: [
15694
+ badged ? /* @__PURE__ */ (0, import_jsx_runtime75.jsx)(
15695
+ NavBadge,
15696
+ {
15697
+ badge: item.badge,
15698
+ active
15699
+ }
15700
+ ) : null,
15701
+ /* @__PURE__ */ (0, import_jsx_runtime75.jsx)(
15702
+ "span",
15703
+ {
15704
+ "aria-hidden": "true",
15705
+ className: cn66(
15706
+ "flex shrink-0 items-center transition-transform duration-200 ease-out",
15707
+ expanded ? "rotate-180" : "rotate-0"
15708
+ ),
15709
+ children: glyph4("chevron-down", 18, 2)
15710
+ }
15711
+ )
15712
+ ] }),
15597
15713
  children: /* @__PURE__ */ (0, import_jsx_runtime75.jsx)("span", { className: "truncate text-left", children: item.label })
15598
15714
  }
15599
15715
  ),
@@ -15649,16 +15765,25 @@ var SidebarMobile = React75.forwardRef(
15649
15765
  /* @__PURE__ */ (0, import_jsx_runtime75.jsx)(Divider, { className: "my-md" }),
15650
15766
  /* @__PURE__ */ (0, import_jsx_runtime75.jsx)("nav", { "aria-label": "Secondary", className: "flex flex-col gap-xxs", children: secondaryNav.map((item) => {
15651
15767
  const active = item.id === secondaryActiveId;
15768
+ const badged = hasBadge2(item.badge);
15652
15769
  return /* @__PURE__ */ (0, import_jsx_runtime75.jsxs)(
15653
15770
  "a",
15654
15771
  {
15655
15772
  href: item.href ?? `#${item.id}`,
15656
15773
  onClick: navigate({ id: item.id, href: item.href }),
15657
15774
  "aria-current": active ? "page" : void 0,
15775
+ "aria-label": badged ? rowAccessibleName2(item.label, item.badge) : void 0,
15658
15776
  className: rowClass(active),
15659
15777
  children: [
15660
15778
  glyph4(item.icon, 18, 1.75),
15661
- /* @__PURE__ */ (0, import_jsx_runtime75.jsx)("span", { className: "truncate", children: item.label })
15779
+ /* @__PURE__ */ (0, import_jsx_runtime75.jsx)("span", { className: "truncate", children: item.label }),
15780
+ badged ? /* @__PURE__ */ (0, import_jsx_runtime75.jsx)(
15781
+ NavBadge,
15782
+ {
15783
+ badge: item.badge,
15784
+ active
15785
+ }
15786
+ ) : null
15662
15787
  ]
15663
15788
  },
15664
15789
  item.id
package/dist/index.js CHANGED
@@ -14532,12 +14532,59 @@ var glyph3 = (name, size = 16, strokeWidth = 1.75) => /* @__PURE__ */ jsx74(
14532
14532
  style: { color: "inherit" }
14533
14533
  }
14534
14534
  );
14535
+ var hasBadge = (badge) => {
14536
+ if (badge === void 0 || badge === null) {
14537
+ return false;
14538
+ }
14539
+ if (typeof badge === "number") {
14540
+ return Number.isFinite(badge) && badge !== 0;
14541
+ }
14542
+ const trimmed = badge.trim();
14543
+ return trimmed.length > 0 && trimmed !== "0";
14544
+ };
14545
+ var badgeText = (badge) => typeof badge === "number" ? badge > 99 ? "99+" : String(badge) : badge.trim();
14546
+ var rowAccessibleName = (label, badge) => {
14547
+ if (!hasBadge(badge)) {
14548
+ return label;
14549
+ }
14550
+ return typeof badge === "number" ? `${label}, ${badge} pending` : `${label}, ${badgeText(badge)}`;
14551
+ };
14552
+ var RailBadge = ({
14553
+ badge,
14554
+ active
14555
+ }) => /* @__PURE__ */ jsx74(
14556
+ "span",
14557
+ {
14558
+ "aria-hidden": "true",
14559
+ "data-active": active || void 0,
14560
+ className: cn65(
14561
+ "ml-auto flex h-4 min-w-4 shrink-0 items-center justify-center rounded-full",
14562
+ "bg-primary px-xxs text-caption text-on-primary",
14563
+ "data-[active=true]:bg-on-primary data-[active=true]:text-primary"
14564
+ ),
14565
+ children: badgeText(badge)
14566
+ }
14567
+ );
14568
+ var railGlyph = (item, folded) => /* @__PURE__ */ jsxs71("span", { className: "relative inline-flex shrink-0 items-center", children: [
14569
+ glyph3(item.icon, 16, 1.75),
14570
+ folded && hasBadge(item.badge) ? /* @__PURE__ */ jsx74(
14571
+ "span",
14572
+ {
14573
+ "aria-hidden": "true",
14574
+ className: cn65(
14575
+ "pointer-events-none absolute -right-xxs -top-xxs size-2 rounded-full",
14576
+ "bg-primary",
14577
+ "group-data-[active=true]:bg-on-primary"
14578
+ )
14579
+ }
14580
+ ) : null
14581
+ ] });
14535
14582
  var RowLabel = ({
14536
14583
  folded,
14537
14584
  children
14538
14585
  }) => folded ? /* @__PURE__ */ jsx74("span", { className: "sr-only", children }) : /* @__PURE__ */ jsx74("span", { className: "truncate", children });
14539
14586
  var railRowClass = (active, folded) => cn65(
14540
- "flex w-full cursor-pointer items-center rounded-lg border-0 bg-transparent",
14587
+ "group flex w-full cursor-pointer items-center rounded-lg border-0 bg-transparent",
14541
14588
  "text-body-md transition duration-150 ease-out",
14542
14589
  "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-focus-blue",
14543
14590
  "focus-visible:ring-inset",
@@ -14873,6 +14920,7 @@ var SidebarDesktop = React74.forwardRef(
14873
14920
  className: "flex flex-col gap-xxs",
14874
14921
  children: navItems.map((item) => {
14875
14922
  const active = item.id === activeId;
14923
+ const badged = hasBadge(item.badge);
14876
14924
  if (!item.children?.length) {
14877
14925
  return /* @__PURE__ */ jsxs71(
14878
14926
  "a",
@@ -14880,11 +14928,13 @@ var SidebarDesktop = React74.forwardRef(
14880
14928
  href: item.href ?? `#${item.id}`,
14881
14929
  onClick: navigate({ id: item.id, href: item.href }),
14882
14930
  "aria-current": active ? "page" : void 0,
14883
- "aria-label": isFolded ? item.label : void 0,
14931
+ "data-active": active,
14932
+ "aria-label": isFolded || badged ? rowAccessibleName(item.label, item.badge) : void 0,
14884
14933
  className: railRowClass(active, isFolded),
14885
14934
  children: [
14886
- glyph3(item.icon, 16, 1.75),
14887
- /* @__PURE__ */ jsx74(RowLabel, { folded: isFolded, children: item.label })
14935
+ railGlyph(item, isFolded),
14936
+ /* @__PURE__ */ jsx74(RowLabel, { folded: isFolded, children: item.label }),
14937
+ !isFolded && badged ? /* @__PURE__ */ jsx74(RailBadge, { badge: item.badge, active }) : null
14888
14938
  ]
14889
14939
  },
14890
14940
  item.id
@@ -14900,22 +14950,32 @@ var SidebarDesktop = React74.forwardRef(
14900
14950
  size: "rail",
14901
14951
  "aria-expanded": expanded,
14902
14952
  "aria-controls": subId,
14903
- "aria-label": isFolded ? item.label : void 0,
14953
+ "aria-label": isFolded || badged ? rowAccessibleName(item.label, item.badge) : void 0,
14904
14954
  "data-active": active,
14905
14955
  "data-folded": isFolded,
14906
14956
  onClick: () => toggleExpanded(item.id),
14907
- leadingIcon: glyph3(item.icon, 16, 1.75),
14908
- trailingIcon: isFolded ? void 0 : /* @__PURE__ */ jsx74(
14909
- "span",
14910
- {
14911
- "aria-hidden": "true",
14912
- className: cn65(
14913
- "flex shrink-0 items-center transition-transform duration-200 ease-out",
14914
- expanded ? "rotate-180" : "rotate-0"
14915
- ),
14916
- children: glyph3("chevron-down", 16, 2)
14917
- }
14918
- ),
14957
+ className: "group",
14958
+ leadingIcon: railGlyph(item, isFolded),
14959
+ trailingIcon: isFolded ? void 0 : /* @__PURE__ */ jsxs71("span", { className: "flex items-center gap-xs", children: [
14960
+ badged ? /* @__PURE__ */ jsx74(
14961
+ RailBadge,
14962
+ {
14963
+ badge: item.badge,
14964
+ active
14965
+ }
14966
+ ) : null,
14967
+ /* @__PURE__ */ jsx74(
14968
+ "span",
14969
+ {
14970
+ "aria-hidden": "true",
14971
+ className: cn65(
14972
+ "flex shrink-0 items-center transition-transform duration-200 ease-out",
14973
+ expanded ? "rotate-180" : "rotate-0"
14974
+ ),
14975
+ children: glyph3("chevron-down", 16, 2)
14976
+ }
14977
+ )
14978
+ ] }),
14919
14979
  children: /* @__PURE__ */ jsx74(RowLabel, { folded: isFolded, children: item.label })
14920
14980
  }
14921
14981
  ),
@@ -14968,17 +15028,20 @@ var SidebarDesktop = React74.forwardRef(
14968
15028
  ),
14969
15029
  /* @__PURE__ */ jsx74("nav", { "aria-label": "Secondary", className: "mt-lg flex flex-col gap-xxs", children: secondaryNav.map((item) => {
14970
15030
  const active = item.id === secondaryActiveId;
15031
+ const badged = hasBadge(item.badge);
14971
15032
  return /* @__PURE__ */ jsxs71(
14972
15033
  "a",
14973
15034
  {
14974
15035
  href: item.href ?? `#${item.id}`,
14975
15036
  onClick: navigate({ id: item.id, href: item.href }),
14976
15037
  "aria-current": active ? "page" : void 0,
14977
- "aria-label": isFolded ? item.label : void 0,
15038
+ "data-active": active,
15039
+ "aria-label": isFolded || badged ? rowAccessibleName(item.label, item.badge) : void 0,
14978
15040
  className: railRowClass(active, isFolded),
14979
15041
  children: [
14980
- glyph3(item.icon, 16, 1.75),
14981
- /* @__PURE__ */ jsx74(RowLabel, { folded: isFolded, children: item.label })
15042
+ railGlyph(item, isFolded),
15043
+ /* @__PURE__ */ jsx74(RowLabel, { folded: isFolded, children: item.label }),
15044
+ !isFolded && badged ? /* @__PURE__ */ jsx74(RailBadge, { badge: item.badge, active }) : null
14982
15045
  ]
14983
15046
  },
14984
15047
  item.id
@@ -15105,6 +15168,39 @@ var SAMPLE_SECONDARY2 = [
15105
15168
  ];
15106
15169
  var SAMPLE_PLAN2 = { name: "Pro", price: "$2,400.00 / month", href: "#plan" };
15107
15170
  var glyph4 = (name, size = 18, strokeWidth = 1.75) => /* @__PURE__ */ jsx75(Icon, { name, size, strokeWidth, style: { color: "inherit" } });
15171
+ var hasBadge2 = (badge) => {
15172
+ if (badge === void 0 || badge === null) {
15173
+ return false;
15174
+ }
15175
+ if (typeof badge === "number") {
15176
+ return Number.isFinite(badge) && badge !== 0;
15177
+ }
15178
+ const trimmed = badge.trim();
15179
+ return trimmed.length > 0 && trimmed !== "0";
15180
+ };
15181
+ var badgeText2 = (badge) => typeof badge === "number" ? badge > 99 ? "99+" : String(badge) : badge.trim();
15182
+ var rowAccessibleName2 = (label, badge) => {
15183
+ if (!hasBadge2(badge)) {
15184
+ return label;
15185
+ }
15186
+ return typeof badge === "number" ? `${label}, ${badge} pending` : `${label}, ${badgeText2(badge)}`;
15187
+ };
15188
+ var NavBadge = ({
15189
+ badge,
15190
+ active
15191
+ }) => /* @__PURE__ */ jsx75(
15192
+ "span",
15193
+ {
15194
+ "aria-hidden": "true",
15195
+ "data-active": active || void 0,
15196
+ className: cn66(
15197
+ "ml-auto flex h-4 min-w-4 shrink-0 items-center justify-center rounded-full",
15198
+ "bg-primary px-xxs text-caption text-on-primary",
15199
+ "data-[active=true]:bg-on-primary data-[active=true]:text-primary"
15200
+ ),
15201
+ children: badgeText2(badge)
15202
+ }
15203
+ );
15108
15204
  var rowClass = (active) => cn66(
15109
15205
  "flex min-h-10 w-full cursor-pointer items-center gap-md rounded-lg px-md py-sm",
15110
15206
  "text-body-md transition duration-150 ease-out",
@@ -15417,6 +15513,7 @@ var SidebarMobile = React75.forwardRef(
15417
15513
  ] }),
15418
15514
  /* @__PURE__ */ jsx75("nav", { "aria-label": "Primary", className: "mt-md flex flex-col gap-xxs", children: navItems.map((item) => {
15419
15515
  const active = item.id === activeId;
15516
+ const badged = hasBadge2(item.badge);
15420
15517
  if (!item.children?.length) {
15421
15518
  return /* @__PURE__ */ jsxs72(
15422
15519
  "a",
@@ -15424,10 +15521,18 @@ var SidebarMobile = React75.forwardRef(
15424
15521
  href: item.href ?? `#${item.id}`,
15425
15522
  onClick: navigate({ id: item.id, href: item.href }),
15426
15523
  "aria-current": active ? "page" : void 0,
15524
+ "aria-label": badged ? rowAccessibleName2(item.label, item.badge) : void 0,
15427
15525
  className: rowClass(active),
15428
15526
  children: [
15429
15527
  glyph4(item.icon, 18, 1.75),
15430
- /* @__PURE__ */ jsx75("span", { className: "truncate", children: item.label })
15528
+ /* @__PURE__ */ jsx75("span", { className: "truncate", children: item.label }),
15529
+ badged ? /* @__PURE__ */ jsx75(
15530
+ NavBadge,
15531
+ {
15532
+ badge: item.badge,
15533
+ active
15534
+ }
15535
+ ) : null
15431
15536
  ]
15432
15537
  },
15433
15538
  item.id
@@ -15443,20 +15548,31 @@ var SidebarMobile = React75.forwardRef(
15443
15548
  size: "rail",
15444
15549
  "aria-expanded": expanded,
15445
15550
  "aria-controls": subId,
15551
+ "aria-label": badged ? rowAccessibleName2(item.label, item.badge) : void 0,
15446
15552
  "data-active": active,
15447
15553
  onClick: () => toggleExpanded(item.id),
15448
15554
  className: "px-md",
15449
15555
  leadingIcon: glyph4(item.icon, 18, 1.75),
15450
- trailingIcon: /* @__PURE__ */ jsx75(
15451
- "span",
15452
- {
15453
- className: cn66(
15454
- "flex shrink-0 items-center transition-transform duration-200 ease-out",
15455
- expanded ? "rotate-180" : "rotate-0"
15456
- ),
15457
- children: glyph4("chevron-down", 18, 2)
15458
- }
15459
- ),
15556
+ trailingIcon: /* @__PURE__ */ jsxs72("span", { className: "flex items-center gap-xs", children: [
15557
+ badged ? /* @__PURE__ */ jsx75(
15558
+ NavBadge,
15559
+ {
15560
+ badge: item.badge,
15561
+ active
15562
+ }
15563
+ ) : null,
15564
+ /* @__PURE__ */ jsx75(
15565
+ "span",
15566
+ {
15567
+ "aria-hidden": "true",
15568
+ className: cn66(
15569
+ "flex shrink-0 items-center transition-transform duration-200 ease-out",
15570
+ expanded ? "rotate-180" : "rotate-0"
15571
+ ),
15572
+ children: glyph4("chevron-down", 18, 2)
15573
+ }
15574
+ )
15575
+ ] }),
15460
15576
  children: /* @__PURE__ */ jsx75("span", { className: "truncate text-left", children: item.label })
15461
15577
  }
15462
15578
  ),
@@ -15512,16 +15628,25 @@ var SidebarMobile = React75.forwardRef(
15512
15628
  /* @__PURE__ */ jsx75(Divider, { className: "my-md" }),
15513
15629
  /* @__PURE__ */ jsx75("nav", { "aria-label": "Secondary", className: "flex flex-col gap-xxs", children: secondaryNav.map((item) => {
15514
15630
  const active = item.id === secondaryActiveId;
15631
+ const badged = hasBadge2(item.badge);
15515
15632
  return /* @__PURE__ */ jsxs72(
15516
15633
  "a",
15517
15634
  {
15518
15635
  href: item.href ?? `#${item.id}`,
15519
15636
  onClick: navigate({ id: item.id, href: item.href }),
15520
15637
  "aria-current": active ? "page" : void 0,
15638
+ "aria-label": badged ? rowAccessibleName2(item.label, item.badge) : void 0,
15521
15639
  className: rowClass(active),
15522
15640
  children: [
15523
15641
  glyph4(item.icon, 18, 1.75),
15524
- /* @__PURE__ */ jsx75("span", { className: "truncate", children: item.label })
15642
+ /* @__PURE__ */ jsx75("span", { className: "truncate", children: item.label }),
15643
+ badged ? /* @__PURE__ */ jsx75(
15644
+ NavBadge,
15645
+ {
15646
+ badge: item.badge,
15647
+ active
15648
+ }
15649
+ ) : null
15525
15650
  ]
15526
15651
  },
15527
15652
  item.id
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@designa8/vibedesign-stripe-deepseek-85ea",
3
- "version": "0.2.1",
3
+ "version": "0.2.2",
4
4
  "description": "Complete design system \"My Project stripe-deepseek\" generated by Atoms Design.",
5
5
  "main": "dist/index.cjs",
6
6
  "module": "dist/index.js",
@@ -2,30 +2,35 @@ import * as React from "react";
2
2
  import { Avatar } from "../Avatar";
3
3
  import { Button } from "../Button";
4
4
  import { Icon } from "../Icon";
5
- import { SidebarDesktop, type SidebarDesktopNavItem } from "../SidebarDesktop";
6
- import { SidebarMobile } from "../SidebarMobile";
5
+ import { type SidebarDesktopNavItem } from "../SidebarDesktop";
7
6
  import { Sidebar, type SidebarNavTarget } from "./Sidebar";
8
7
 
9
- const ITEMS: SidebarDesktopNavItem[] = [
10
- { id: "payments", label: "Payments", icon: "credit-card", href: "/payments" },
8
+ /** Nav fixtures with the count wired to state, so the pill really hides at 0. */
9
+ const buildItems = (pending: number): SidebarDesktopNavItem[] => [
10
+ {
11
+ id: "payments",
12
+ label: "Tasks",
13
+ icon: "credit-card",
14
+ href: "/tasks",
15
+ badge: pending,
16
+ },
11
17
  { id: "balances", label: "Balances", icon: "layers", href: "/balances" },
12
18
  {
13
19
  id: "payouts",
14
- label: "Payouts",
20
+ label: "Approvals",
15
21
  icon: "arrow-left-right",
22
+ badge: pending,
16
23
  children: [
17
- { id: "scheduled", label: "Scheduled payouts", href: "/payouts/scheduled" },
18
- { id: "payout-accounts", label: "Payout accounts", href: "/payouts/accounts" },
19
- { id: "payout-limits", label: "Payout limits", href: "/payouts/limits" },
24
+ { id: "scheduled", label: "Scheduled payouts", href: "/approvals/scheduled" },
25
+ { id: "payout-accounts", label: "Payout accounts", href: "/approvals/accounts" },
20
26
  ],
21
27
  },
22
28
  { id: "developers", label: "Developers", icon: "code", href: "/developers" },
23
- { id: "api-logs", label: "API logs", icon: "file", href: "/api-logs" },
24
29
  ];
25
30
 
26
31
  const SECONDARY_ITEMS: SidebarDesktopNavItem[] = [
27
32
  { id: "settings", label: "Settings", icon: "settings", href: "/settings" },
28
- { id: "team", label: "Team", icon: "group", href: "/team" },
33
+ { id: "team", label: "Team", icon: "group", href: "/team", badge: "New" },
29
34
  ];
30
35
 
31
36
  const ACCOUNTS = [
@@ -35,27 +40,11 @@ const ACCOUNTS = [
35
40
 
36
41
  const PROFILE = { name: "Priya Shah", email: "priya@acme.com" };
37
42
 
38
- /** A tiny content pane so the routed shell reads as a page, not a floating rail. */
39
- const ContentPane = ({ route, unread }: { route: string; unread: number }) => (
40
- <div className="flex min-w-0 flex-col gap-sm bg-canvas p-lg">
41
- <p className="text-caption uppercase text-muted">Current route</p>
42
- <p className="text-heading-md text-ink">{route}</p>
43
- <p className="text-body-sm text-body">
44
- Click any destination in the navigation to route there. The rail keeps its
45
- header and footer pinned while the destination list scrolls on its own.
46
- </p>
47
- <p className="text-body-sm text-body">
48
- {unread > 0
49
- ? `${unread} unread notifications waiting in the footer bell.`
50
- : "Notifications cleared — the badge is gone."}
51
- </p>
52
- </div>
53
- );
54
-
55
43
  export default function Preview() {
56
44
  const [activeId, setActiveId] = React.useState("payments");
57
- const [route, setRoute] = React.useState("/payments");
45
+ const [route, setRoute] = React.useState("/tasks");
58
46
  const [unread, setUnread] = React.useState(4);
47
+ const [pending, setPending] = React.useState(2);
59
48
 
60
49
  const handleNavigate = (item: SidebarNavTarget) => {
61
50
  setRoute(item.href ?? `#${item.id}`);
@@ -63,99 +52,68 @@ export default function Preview() {
63
52
  };
64
53
 
65
54
  return (
66
- <div className="mx-auto w-full max-w-3xl space-y-xl p-lg">
67
- {/* The whole control: one instance, responsive in CSS. */}
68
- <section className="space-y-sm">
55
+ <div className="mx-auto w-full max-w-3xl space-y-sm p-lg">
56
+ <div className="flex flex-wrap items-center justify-between gap-sm">
69
57
  <p className="text-caption uppercase text-muted">
70
- Routed app shell — rail above md, header below
58
+ Count pill unfolded, dot on the glyph once folded
71
59
  </p>
72
- <div className="grid w-full grid-cols-1 overflow-hidden rounded-xl border border-hairline bg-surface md:grid-cols-[auto_minmax(0,1fr)]">
73
- <Sidebar
74
- wordmark="Nimbus"
75
- homeHref="/"
76
- items={ITEMS}
77
- activeId={activeId}
78
- onNavigate={handleNavigate}
79
- secondaryItems={SECONDARY_ITEMS}
80
- secondaryActiveId="settings"
81
- accounts={ACCOUNTS}
82
- profile={PROFILE}
83
- plan={{ name: "Pro", price: "$2,400.00 / month", href: "/billing" }}
84
- notificationCount={unread}
85
- onNotifications={() => setUnread(0)}
86
- mobileActions={
87
- <>
88
- <Button variant="bare" size="icon-sm" aria-label="Search">
89
- <Icon name="search" size={20} strokeWidth={1.75} />
90
- </Button>
91
- <Button variant="bare" size="icon-sm" aria-label="Notifications">
92
- <Icon name="bell" size={20} strokeWidth={1.75} />
93
- </Button>
94
- <Avatar name={PROFILE.name} size="sm" />
95
- </>
96
- }
97
- mobileDrawerFooter={
98
- <Button variant="outline-soft" size="md" fullWidth>
99
- Switch theme
100
- </Button>
101
- }
102
- />
103
- <ContentPane route={route} unread={unread} />
104
- </div>
105
- </section>
60
+ <Button
61
+ variant="outline-soft"
62
+ size="sm"
63
+ onClick={() => setPending((current) => (current === 0 ? 2 : 0))}
64
+ >
65
+ {pending === 0 ? "Show pending counts" : "Clear pending counts"}
66
+ </Button>
67
+ </div>
106
68
 
107
- {/* The desktop part on its own, folded down to its icon column. */}
108
- <section className="space-y-sm">
109
- <p className="text-caption uppercase text-muted">
110
- Desktop rail — folded, with a notification badge
111
- </p>
112
- <div className="grid w-full grid-cols-1 overflow-hidden rounded-xl border border-hairline bg-surface md:grid-cols-[auto_minmax(0,1fr)]">
113
- <SidebarDesktop
114
- items={ITEMS}
115
- activeId={activeId}
116
- onNavigate={handleNavigate}
117
- secondaryItems={SECONDARY_ITEMS}
118
- accounts={ACCOUNTS}
119
- profile={PROFILE}
120
- notificationCount={12}
121
- defaultFolded
122
- />
123
- <ContentPane route={route} unread={unread} />
124
- </div>
125
- </section>
126
-
127
- {/* The mobile part: the flush header, with the drawer it opens. */}
128
- <section className="space-y-sm">
129
- <p className="text-caption uppercase text-muted">
130
- Mobile header — tap the menu to open the drawer
131
- </p>
132
- <div className="w-full overflow-hidden rounded-xl border border-hairline bg-surface">
133
- <SidebarMobile
134
- items={ITEMS}
135
- activeId={activeId}
136
- onNavigate={handleNavigate}
137
- secondaryItems={SECONDARY_ITEMS}
138
- accounts={ACCOUNTS}
139
- profile={PROFILE}
140
- plan={null}
141
- notificationCount={unread}
142
- mobileActions={
143
- <>
144
- <Button variant="bare" size="icon-sm" aria-label="Search">
145
- <Icon name="search" size={20} strokeWidth={1.75} />
146
- </Button>
147
- <Avatar name={PROFILE.name} size="sm" />
148
- </>
149
- }
150
- mobileDrawerFooter={
151
- <Button variant="outline-soft" size="md" fullWidth>
152
- Switch theme
69
+ <div className="grid w-full grid-cols-1 overflow-hidden rounded-xl border border-hairline bg-surface md:grid-cols-[auto_minmax(0,1fr)]">
70
+ <Sidebar
71
+ wordmark="Nimbus"
72
+ homeHref="/"
73
+ items={buildItems(pending)}
74
+ activeId={activeId}
75
+ onNavigate={handleNavigate}
76
+ secondaryItems={SECONDARY_ITEMS}
77
+ secondaryActiveId="settings"
78
+ accounts={ACCOUNTS}
79
+ profile={PROFILE}
80
+ plan={{ name: "Pro", price: "$2,400.00 / month", href: "/billing" }}
81
+ notificationCount={unread}
82
+ onNotifications={() => setUnread(0)}
83
+ defaultExpandedId={null}
84
+ mobileActions={
85
+ <>
86
+ <Button variant="bare" size="icon-sm" aria-label="Search">
87
+ <Icon name="search" size={20} strokeWidth={1.75} />
88
+ </Button>
89
+ <Button variant="bare" size="icon-sm" aria-label="Notifications">
90
+ <Icon name="bell" size={20} strokeWidth={1.75} />
153
91
  </Button>
154
- }
155
- />
156
- <ContentPane route={route} unread={unread} />
92
+ <Avatar name={PROFILE.name} size="sm" />
93
+ </>
94
+ }
95
+ mobileDrawerFooter={
96
+ <Button variant="outline-soft" size="md" fullWidth>
97
+ Switch theme
98
+ </Button>
99
+ }
100
+ />
101
+
102
+ <div className="flex min-w-0 flex-col gap-sm bg-canvas p-lg">
103
+ <p className="text-caption uppercase text-muted">Current route</p>
104
+ <p className="text-heading-md text-ink">{route}</p>
105
+ <p className="text-body-sm text-body">
106
+ Tasks carries a count pill after its label. Collapse the rail and the
107
+ same count becomes a dot on the glyph, while the row still announces
108
+ the number to a screen reader.
109
+ </p>
110
+ <p className="text-body-sm text-body">
111
+ {unread > 0
112
+ ? `${unread} unread notifications sit in the footer bell.`
113
+ : "Notifications cleared — the bell badge is gone."}
114
+ </p>
157
115
  </div>
158
- </section>
116
+ </div>
159
117
  </div>
160
118
  );
161
119
  }
@@ -36,6 +36,12 @@ export interface SidebarDesktopNavItem {
36
36
  icon: IconName;
37
37
  /** Destination for a plain row. */
38
38
  href?: string;
39
+ /**
40
+ * Pending-work count painted as a pill after the label, and as a dot on the
41
+ * glyph while the rail is folded. A number (99+ collapses), or a short string
42
+ * such as "New". Hidden when undefined, empty, or zero.
43
+ */
44
+ badge?: number | string;
39
45
  /** Nested rows: turns the parent into a disclosure. */
40
46
  children?: Array<{ id: string; label: string; href?: string }>;
41
47
  }
@@ -149,6 +155,85 @@ const glyph = (name: IconName, size = 16, strokeWidth = 1.75) => (
149
155
  />
150
156
  );
151
157
 
158
+ /** True when an item carries a count worth painting. Zero and empties hide. */
159
+ const hasBadge = (badge?: number | string) => {
160
+ if (badge === undefined || badge === null) {
161
+ return false;
162
+ }
163
+ if (typeof badge === "number") {
164
+ return Number.isFinite(badge) && badge !== 0;
165
+ }
166
+ const trimmed = badge.trim();
167
+ return trimmed.length > 0 && trimmed !== "0";
168
+ };
169
+
170
+ /** The pill copy: numbers above 99 collapse so a row never reflows. */
171
+ const badgeText = (badge: number | string) =>
172
+ typeof badge === "number"
173
+ ? badge > 99
174
+ ? "99+"
175
+ : String(badge)
176
+ : badge.trim();
177
+
178
+ /**
179
+ * The row's accessible name. The count rides along as pending work, so a folded
180
+ * icon rail still announces "API logs, 12 pending" instead of a bare glyph.
181
+ */
182
+ const rowAccessibleName = (label: string, badge?: number | string) => {
183
+ if (!hasBadge(badge)) {
184
+ return label;
185
+ }
186
+ return typeof badge === "number"
187
+ ? `${label}, ${badge} pending`
188
+ : `${label}, ${badgeText(badge as string)}`;
189
+ };
190
+
191
+ /**
192
+ * The count pill that follows a row's label. It wears the same recipe as the
193
+ * footer bell: the brand fill with inverse type on the navy rail, flipped to the
194
+ * inverse fill while the row itself is the selected brand block.
195
+ */
196
+ const RailBadge = ({
197
+ badge,
198
+ active,
199
+ }: {
200
+ badge: number | string;
201
+ active?: boolean;
202
+ }) => (
203
+ <span
204
+ aria-hidden="true"
205
+ data-active={active || undefined}
206
+ className={cn(
207
+ "ml-auto flex h-4 min-w-4 shrink-0 items-center justify-center rounded-full",
208
+ "bg-primary px-xxs text-caption text-on-primary",
209
+ "data-[active=true]:bg-on-primary data-[active=true]:text-primary",
210
+ )}
211
+ >
212
+ {badgeText(badge)}
213
+ </span>
214
+ );
215
+
216
+ /**
217
+ * The leading glyph plus, while the rail is folded, a dot on its top corner.
218
+ * The folded rail has no room for a pill, so the dot carries the signal and the
219
+ * count lives in the row's accessible name.
220
+ */
221
+ const railGlyph = (item: SidebarDesktopNavItem, folded: boolean) => (
222
+ <span className="relative inline-flex shrink-0 items-center">
223
+ {glyph(item.icon, 16, 1.75)}
224
+ {folded && hasBadge(item.badge) ? (
225
+ <span
226
+ aria-hidden="true"
227
+ className={cn(
228
+ "pointer-events-none absolute -right-xxs -top-xxs size-2 rounded-full",
229
+ "bg-primary",
230
+ "group-data-[active=true]:bg-on-primary",
231
+ )}
232
+ />
233
+ ) : null}
234
+ </span>
235
+ );
236
+
152
237
  /** The visible label, kept in the tree as an accessible name while folded. */
153
238
  const RowLabel = ({
154
239
  folded,
@@ -170,7 +255,7 @@ const RowLabel = ({
170
255
  */
171
256
  const railRowClass = (active: boolean, folded: boolean) =>
172
257
  cn(
173
- "flex w-full cursor-pointer items-center rounded-lg border-0 bg-transparent",
258
+ "group flex w-full cursor-pointer items-center rounded-lg border-0 bg-transparent",
174
259
  "text-body-md transition duration-150 ease-out",
175
260
  "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-focus-blue",
176
261
  "focus-visible:ring-inset",
@@ -201,6 +286,10 @@ interface PanelBox {
201
286
  * width and the labels change, so the item order and the active row survive the
202
287
  * fold.
203
288
  *
289
+ * A destination may carry a count: unfolded it reads as a pill after the label,
290
+ * folded it becomes a dot on the glyph, and either way the number is part of the
291
+ * row's accessible name. Nothing is painted when the count is absent or zero.
292
+ *
204
293
  * The rail fills the height of its parent (`h-full`), so a one-viewport shell
205
294
  * (`md:sticky md:top-0 md:h-dvh`) is all the sizing it needs. The header, the
206
295
  * account switcher and the footer stay pinned; only the destination list
@@ -606,6 +695,7 @@ export const SidebarDesktop = React.forwardRef<HTMLElement, SidebarDesktopProps>
606
695
  >
607
696
  {navItems.map((item) => {
608
697
  const active = item.id === activeId;
698
+ const badged = hasBadge(item.badge);
609
699
 
610
700
  if (!item.children?.length) {
611
701
  return (
@@ -614,11 +704,19 @@ export const SidebarDesktop = React.forwardRef<HTMLElement, SidebarDesktopProps>
614
704
  href={item.href ?? `#${item.id}`}
615
705
  onClick={navigate({ id: item.id, href: item.href })}
616
706
  aria-current={active ? "page" : undefined}
617
- aria-label={isFolded ? item.label : undefined}
707
+ data-active={active}
708
+ aria-label={
709
+ isFolded || badged
710
+ ? rowAccessibleName(item.label, item.badge)
711
+ : undefined
712
+ }
618
713
  className={railRowClass(active, isFolded)}
619
714
  >
620
- {glyph(item.icon, 16, 1.75)}
715
+ {railGlyph(item, isFolded)}
621
716
  <RowLabel folded={isFolded}>{item.label}</RowLabel>
717
+ {!isFolded && badged ? (
718
+ <RailBadge badge={item.badge as number | string} active={active} />
719
+ ) : null}
622
720
  </a>
623
721
  );
624
722
  }
@@ -633,21 +731,34 @@ export const SidebarDesktop = React.forwardRef<HTMLElement, SidebarDesktopProps>
633
731
  size="rail"
634
732
  aria-expanded={expanded}
635
733
  aria-controls={subId}
636
- aria-label={isFolded ? item.label : undefined}
734
+ aria-label={
735
+ isFolded || badged
736
+ ? rowAccessibleName(item.label, item.badge)
737
+ : undefined
738
+ }
637
739
  data-active={active}
638
740
  data-folded={isFolded}
639
741
  onClick={() => toggleExpanded(item.id)}
640
- leadingIcon={glyph(item.icon, 16, 1.75)}
742
+ className="group"
743
+ leadingIcon={railGlyph(item, isFolded)}
641
744
  trailingIcon={
642
745
  isFolded ? undefined : (
643
- <span
644
- aria-hidden="true"
645
- className={cn(
646
- "flex shrink-0 items-center transition-transform duration-200 ease-out",
647
- expanded ? "rotate-180" : "rotate-0",
648
- )}
649
- >
650
- {glyph("chevron-down", 16, 2)}
746
+ <span className="flex items-center gap-xs">
747
+ {badged ? (
748
+ <RailBadge
749
+ badge={item.badge as number | string}
750
+ active={active}
751
+ />
752
+ ) : null}
753
+ <span
754
+ aria-hidden="true"
755
+ className={cn(
756
+ "flex shrink-0 items-center transition-transform duration-200 ease-out",
757
+ expanded ? "rotate-180" : "rotate-0",
758
+ )}
759
+ >
760
+ {glyph("chevron-down", 16, 2)}
761
+ </span>
651
762
  </span>
652
763
  )
653
764
  }
@@ -713,6 +824,7 @@ export const SidebarDesktop = React.forwardRef<HTMLElement, SidebarDesktopProps>
713
824
  <nav aria-label="Secondary" className="mt-lg flex flex-col gap-xxs">
714
825
  {secondaryNav.map((item) => {
715
826
  const active = item.id === secondaryActiveId;
827
+ const badged = hasBadge(item.badge);
716
828
 
717
829
  return (
718
830
  <a
@@ -720,11 +832,19 @@ export const SidebarDesktop = React.forwardRef<HTMLElement, SidebarDesktopProps>
720
832
  href={item.href ?? `#${item.id}`}
721
833
  onClick={navigate({ id: item.id, href: item.href })}
722
834
  aria-current={active ? "page" : undefined}
723
- aria-label={isFolded ? item.label : undefined}
835
+ data-active={active}
836
+ aria-label={
837
+ isFolded || badged
838
+ ? rowAccessibleName(item.label, item.badge)
839
+ : undefined
840
+ }
724
841
  className={railRowClass(active, isFolded)}
725
842
  >
726
- {glyph(item.icon, 16, 1.75)}
843
+ {railGlyph(item, isFolded)}
727
844
  <RowLabel folded={isFolded}>{item.label}</RowLabel>
845
+ {!isFolded && badged ? (
846
+ <RailBadge badge={item.badge as number | string} active={active} />
847
+ ) : null}
728
848
  </a>
729
849
  );
730
850
  })}
@@ -33,6 +33,12 @@ export interface SidebarMobileNavItem {
33
33
  icon: IconName;
34
34
  /** Destination for a plain row. */
35
35
  href?: string;
36
+ /**
37
+ * Pending-work count painted as a pill after the label. A number (99+
38
+ * collapses), or a short string such as "New". Hidden when undefined, empty,
39
+ * or zero, and always part of the row's accessible name.
40
+ */
41
+ badge?: number | string;
36
42
  /** Nested rows: turns the parent into a disclosure. */
37
43
  children?: Array<{ id: string; label: string; href?: string }>;
38
44
  }
@@ -156,6 +162,64 @@ const glyph = (name: IconName, size = 18, strokeWidth = 1.75) => (
156
162
  <Icon name={name} size={size} strokeWidth={strokeWidth} style={{ color: "inherit" }} />
157
163
  );
158
164
 
165
+ /** True when an item carries a count worth painting. Zero and empties hide. */
166
+ const hasBadge = (badge?: number | string) => {
167
+ if (badge === undefined || badge === null) {
168
+ return false;
169
+ }
170
+ if (typeof badge === "number") {
171
+ return Number.isFinite(badge) && badge !== 0;
172
+ }
173
+ const trimmed = badge.trim();
174
+ return trimmed.length > 0 && trimmed !== "0";
175
+ };
176
+
177
+ /** The pill copy: numbers above 99 collapse so a row never reflows. */
178
+ const badgeText = (badge: number | string) =>
179
+ typeof badge === "number"
180
+ ? badge > 99
181
+ ? "99+"
182
+ : String(badge)
183
+ : badge.trim();
184
+
185
+ /**
186
+ * The row's accessible name. The count rides along as pending work, so the row
187
+ * announces "Tasks, 2 pending" instead of only its label.
188
+ */
189
+ const rowAccessibleName = (label: string, badge?: number | string) => {
190
+ if (!hasBadge(badge)) {
191
+ return label;
192
+ }
193
+ return typeof badge === "number"
194
+ ? `${label}, ${badge} pending`
195
+ : `${label}, ${badgeText(badge as string)}`;
196
+ };
197
+
198
+ /**
199
+ * The count pill that follows a row's label. It wears the brand fill with
200
+ * inverse type on the light drawer, flipped to the inverse fill while the row
201
+ * itself is the selected brand block.
202
+ */
203
+ const NavBadge = ({
204
+ badge,
205
+ active,
206
+ }: {
207
+ badge: number | string;
208
+ active?: boolean;
209
+ }) => (
210
+ <span
211
+ aria-hidden="true"
212
+ data-active={active || undefined}
213
+ className={cn(
214
+ "ml-auto flex h-4 min-w-4 shrink-0 items-center justify-center rounded-full",
215
+ "bg-primary px-xxs text-caption text-on-primary",
216
+ "data-[active=true]:bg-on-primary data-[active=true]:text-primary",
217
+ )}
218
+ >
219
+ {badgeText(badge)}
220
+ </span>
221
+ );
222
+
159
223
  /**
160
224
  * A destination row in the drawer. A nav row spans the panel's gutter, so its
161
225
  * press is a tint rather than a scale — scaling would pull the fill away from
@@ -174,8 +238,8 @@ const rowClass = (active: boolean) =>
174
238
  );
175
239
 
176
240
  /** A nested destination: one step quieter and tighter than its parent, indented
177
- * onto the parent's label by an invisible glyph-sized spacer rather than a
178
- * second padding utility. */
241
+ * onto the parent's label by an invisible glyph-sized spacer rather than a
242
+ * second padding utility. */
179
243
  const subRowClass = cn(
180
244
  "flex min-h-7 w-full cursor-pointer items-center gap-md rounded-md px-md py-xxs",
181
245
  "text-body-sm text-body transition duration-150 ease-out",
@@ -203,6 +267,10 @@ const isModified = (event: React.MouseEvent) =>
203
267
  * profile row pinned to the foot of the panel. A row click closes the drawer, so
204
268
  * a routed navigation never leaves the panel hanging open behind the new page.
205
269
  *
270
+ * A destination may carry a count: it reads as a pill after the label, and the
271
+ * number is part of the row's accessible name. Nothing is painted when the
272
+ * count is absent or zero.
273
+ *
206
274
  * The panel stays mounted and travels with `translate-x`, so the close
207
275
  * animation is never skipped; it only drops out of the paint once it has
208
276
  * finished travelling, and nothing inside can be tabbed while it is off-canvas.
@@ -557,6 +625,7 @@ export const SidebarMobile = React.forwardRef<HTMLDivElement, SidebarMobileProps
557
625
  <nav aria-label="Primary" className="mt-md flex flex-col gap-xxs">
558
626
  {navItems.map((item) => {
559
627
  const active = item.id === activeId;
628
+ const badged = hasBadge(item.badge);
560
629
 
561
630
  if (!item.children?.length) {
562
631
  return (
@@ -565,10 +634,21 @@ export const SidebarMobile = React.forwardRef<HTMLDivElement, SidebarMobileProps
565
634
  href={item.href ?? `#${item.id}`}
566
635
  onClick={navigate({ id: item.id, href: item.href })}
567
636
  aria-current={active ? "page" : undefined}
637
+ aria-label={
638
+ badged
639
+ ? rowAccessibleName(item.label, item.badge)
640
+ : undefined
641
+ }
568
642
  className={rowClass(active)}
569
643
  >
570
644
  {glyph(item.icon, 18, 1.75)}
571
645
  <span className="truncate">{item.label}</span>
646
+ {badged ? (
647
+ <NavBadge
648
+ badge={item.badge as number | string}
649
+ active={active}
650
+ />
651
+ ) : null}
572
652
  </a>
573
653
  );
574
654
  }
@@ -583,18 +663,32 @@ export const SidebarMobile = React.forwardRef<HTMLDivElement, SidebarMobileProps
583
663
  size="rail"
584
664
  aria-expanded={expanded}
585
665
  aria-controls={subId}
666
+ aria-label={
667
+ badged
668
+ ? rowAccessibleName(item.label, item.badge)
669
+ : undefined
670
+ }
586
671
  data-active={active}
587
672
  onClick={() => toggleExpanded(item.id)}
588
673
  className="px-md"
589
674
  leadingIcon={glyph(item.icon, 18, 1.75)}
590
675
  trailingIcon={
591
- <span
592
- className={cn(
593
- "flex shrink-0 items-center transition-transform duration-200 ease-out",
594
- expanded ? "rotate-180" : "rotate-0",
595
- )}
596
- >
597
- {glyph("chevron-down", 18, 2)}
676
+ <span className="flex items-center gap-xs">
677
+ {badged ? (
678
+ <NavBadge
679
+ badge={item.badge as number | string}
680
+ active={active}
681
+ />
682
+ ) : null}
683
+ <span
684
+ aria-hidden="true"
685
+ className={cn(
686
+ "flex shrink-0 items-center transition-transform duration-200 ease-out",
687
+ expanded ? "rotate-180" : "rotate-0",
688
+ )}
689
+ >
690
+ {glyph("chevron-down", 18, 2)}
691
+ </span>
598
692
  </span>
599
693
  }
600
694
  >
@@ -659,6 +753,7 @@ export const SidebarMobile = React.forwardRef<HTMLDivElement, SidebarMobileProps
659
753
  <nav aria-label="Secondary" className="flex flex-col gap-xxs">
660
754
  {secondaryNav.map((item) => {
661
755
  const active = item.id === secondaryActiveId;
756
+ const badged = hasBadge(item.badge);
662
757
 
663
758
  return (
664
759
  <a
@@ -666,10 +761,21 @@ export const SidebarMobile = React.forwardRef<HTMLDivElement, SidebarMobileProps
666
761
  href={item.href ?? `#${item.id}`}
667
762
  onClick={navigate({ id: item.id, href: item.href })}
668
763
  aria-current={active ? "page" : undefined}
764
+ aria-label={
765
+ badged
766
+ ? rowAccessibleName(item.label, item.badge)
767
+ : undefined
768
+ }
669
769
  className={rowClass(active)}
670
770
  >
671
771
  {glyph(item.icon, 18, 1.75)}
672
772
  <span className="truncate">{item.label}</span>
773
+ {badged ? (
774
+ <NavBadge
775
+ badge={item.badge as number | string}
776
+ active={active}
777
+ />
778
+ ) : null}
673
779
  </a>
674
780
  );
675
781
  })}