@devalok/shilp-sutra 0.51.0 → 0.53.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 (81) hide show
  1. package/AGENTS.md +1 -1
  2. package/MIGRATION.md +12 -0
  3. package/dist/_chunks/motion-provider.js +7 -6
  4. package/dist/_chunks/motion-provider.js.map +1 -1
  5. package/dist/_chunks/success.js +53 -53
  6. package/dist/ai/command-bar.js +158 -158
  7. package/dist/ai/command-bar.js.map +1 -1
  8. package/dist/ai/conversation.js +5 -5
  9. package/dist/composed/command-palette.js +17 -17
  10. package/dist/composed/command-palette.js.map +1 -1
  11. package/dist/composed/priority-indicator.d.ts +15 -6
  12. package/dist/composed/priority-indicator.d.ts.map +1 -1
  13. package/dist/composed/priority-indicator.js +37 -88
  14. package/dist/composed/priority-indicator.js.map +1 -1
  15. package/dist/composed/schedule-view.d.ts +17 -2
  16. package/dist/composed/schedule-view.d.ts.map +1 -1
  17. package/dist/composed/schedule-view.js +163 -64
  18. package/dist/composed/schedule-view.js.map +1 -1
  19. package/dist/motion/motion-provider.d.ts.map +1 -1
  20. package/dist/shell/bottom-navbar.d.ts +32 -6
  21. package/dist/shell/bottom-navbar.d.ts.map +1 -1
  22. package/dist/shell/bottom-navbar.js +156 -129
  23. package/dist/shell/bottom-navbar.js.map +1 -1
  24. package/dist/tokens/primitives.css +6 -2
  25. package/dist/tokens/semantic.css +20 -1
  26. package/dist/tokens/utilities.css +6 -0
  27. package/dist/ui/autocomplete.d.ts +34 -32
  28. package/dist/ui/autocomplete.d.ts.map +1 -1
  29. package/dist/ui/autocomplete.js +115 -110
  30. package/dist/ui/autocomplete.js.map +1 -1
  31. package/dist/ui/button-group.js +2 -0
  32. package/dist/ui/button-group.js.map +1 -1
  33. package/dist/ui/button-processing.d.ts.map +1 -1
  34. package/dist/ui/button-processing.js +1 -0
  35. package/dist/ui/button-processing.js.map +1 -1
  36. package/dist/ui/button.d.ts +3 -3
  37. package/dist/ui/button.d.ts.map +1 -1
  38. package/dist/ui/button.js +26 -0
  39. package/dist/ui/button.js.map +1 -1
  40. package/dist/ui/color-input.d.ts.map +1 -1
  41. package/dist/ui/color-input.js +82 -82
  42. package/dist/ui/color-input.js.map +1 -1
  43. package/dist/ui/combobox.js +4 -4
  44. package/dist/ui/icon.d.ts +2 -0
  45. package/dist/ui/icon.d.ts.map +1 -1
  46. package/dist/ui/icon.js +31 -26
  47. package/dist/ui/icon.js.map +1 -1
  48. package/dist/ui/index.js +30 -30
  49. package/dist/ui/search-input.d.ts.map +1 -1
  50. package/dist/ui/search-input.js +5 -4
  51. package/dist/ui/search-input.js.map +1 -1
  52. package/dist/ui/segmented-control.d.ts +28 -6
  53. package/dist/ui/segmented-control.d.ts.map +1 -1
  54. package/dist/ui/segmented-control.js +61 -43
  55. package/dist/ui/segmented-control.js.map +1 -1
  56. package/dist/ui/sidebar.js +7 -7
  57. package/dist/ui/split-button.d.ts.map +1 -1
  58. package/dist/ui/split-button.js +7 -0
  59. package/dist/ui/split-button.js.map +1 -1
  60. package/docs/components/composed/priority-indicator.md +22 -11
  61. package/docs/components/composed/schedule-view.md +20 -5
  62. package/docs/components/shell/bottom-navbar.md +24 -5
  63. package/docs/components/ui/autocomplete.md +26 -10
  64. package/docs/components/ui/button.md +2 -2
  65. package/docs/components/ui/segmented-control.md +31 -11
  66. package/docs/recipes/install-remix.md +3 -3
  67. package/docs/recipes/install-vite.md +3 -3
  68. package/docs/recipes/server-components.md +2 -2
  69. package/llms.txt +1 -1
  70. package/make-kit/foundations/color.md +20 -0
  71. package/make-kit/foundations/dark-mode.md +6 -9
  72. package/make-kit/foundations/icons.md +3 -3
  73. package/make-kit/setup.md +4 -4
  74. package/mcp-manifest.json +239 -35
  75. package/package.json +1 -1
  76. package/scripts/welcome.mjs +59 -1
  77. package/skill/SKILL.md +1 -1
  78. package/skill/references/components.md +1 -1
  79. package/skill/references/server-components.md +2 -2
  80. package/skill/references/setup-remix.md +3 -3
  81. package/skill/references/setup-vite.md +3 -3
@@ -1,29 +1,55 @@
1
1
  "use client";
2
2
  import { IconInput } from '../ui/lib/icon-input';
3
3
  import * as React from 'react';
4
+ export interface BottomNavbarUser {
5
+ name: string;
6
+ role?: string;
7
+ }
4
8
  export interface BottomNavItem {
5
9
  title: string;
6
10
  href: string;
7
11
  /** Icon for this nav item. Accepts any `IconInput`. */
8
12
  icon: IconInput;
13
+ /** Icon shown when the item is active — e.g. a filled variant. Falls back to `icon`. */
14
+ activeIcon?: IconInput;
9
15
  /** When true, the route matches only when the path is exactly equal */
10
16
  exact?: boolean;
11
17
  /** Notification badge count. 0 or undefined = hidden, 1–99 = shown, >99 = "99+" */
12
18
  badge?: number;
19
+ /**
20
+ * Roles allowed to see this item, matched against `user.role`. Omit to show
21
+ * it to everyone. For arbitrary logic, use `canView` instead.
22
+ */
23
+ roles?: string[];
24
+ /**
25
+ * Visibility predicate — full control over whether this item renders.
26
+ * Receives the current `user` (or `null`). Takes precedence over `roles`.
27
+ */
28
+ canView?: (user: BottomNavbarUser | null) => boolean;
13
29
  }
14
- export interface BottomNavbarUser {
15
- name: string;
16
- role?: string;
17
- }
30
+ /**
31
+ * Active-item indicator, animated between items on selection:
32
+ * - `pill` (default) — Material-3 tonal pill behind the icon
33
+ * - `underline` — top accent bar (Material-2 tab style)
34
+ * - `tint` — subtle background tint on the whole active cell
35
+ * - `none` — no shape; rely on the filled `activeIcon` + accent color (iOS)
36
+ */
37
+ export type BottomNavIndicator = 'pill' | 'underline' | 'tint' | 'none';
38
+ /** Show labels always (default) or only for the selected item (narrow viewports). */
39
+ export type BottomNavLabelVisibility = 'always' | 'selected';
18
40
  export interface BottomNavbarProps extends React.HTMLAttributes<HTMLElement> {
19
41
  /** Currently active pathname */
20
42
  currentPath?: string;
21
- /** User information (used to determine admin status, presence) */
43
+ /** Current user. Drives per-item role gating via `item.roles` / `item.canView`. */
22
44
  user?: BottomNavbarUser | null;
23
45
  /** Primary nav items shown directly in the bottom bar (max 4 recommended) */
24
46
  primaryItems?: BottomNavItem[];
25
- /** Additional items shown in the "More" overflow menu */
47
+ /** Additional items shown in the "More" overflow sheet */
26
48
  moreItems?: BottomNavItem[];
49
+ /** Active-indicator style. @default 'pill' */
50
+ indicator?: BottomNavIndicator;
51
+ /** Label visibility. @default 'always' */
52
+ labelVisibility?: BottomNavLabelVisibility;
27
53
  /** Additional className for the nav element */
28
54
  className?: string;
29
55
  }
@@ -1 +1 @@
1
- {"version":3,"file":"bottom-navbar.d.ts","sourceRoot":"","sources":["../../src/shell/bottom-navbar.tsx"],"names":[],"mappings":"AAUA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAK9B,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAA;AAUrD,MAAM,WAAW,aAAa;IAC5B,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,EAAE,MAAM,CAAA;IACZ,uDAAuD;IACvD,IAAI,EAAE,SAAS,CAAA;IACf,uEAAuE;IACvE,KAAK,CAAC,EAAE,OAAO,CAAA;IACf,mFAAmF;IACnF,KAAK,CAAC,EAAE,MAAM,CAAA;CACf;AAED,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,CAAC,EAAE,MAAM,CAAA;CACd;AAED,MAAM,WAAW,iBACf,SAAQ,KAAK,CAAC,cAAc,CAAC,WAAW,CAAC;IACzC,gCAAgC;IAChC,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,kEAAkE;IAClE,IAAI,CAAC,EAAE,gBAAgB,GAAG,IAAI,CAAA;IAC9B,6EAA6E;IAC7E,YAAY,CAAC,EAAE,aAAa,EAAE,CAAA;IAC9B,yDAAyD;IACzD,SAAS,CAAC,EAAE,aAAa,EAAE,CAAA;IAC3B,+CAA+C;IAC/C,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AA4ED,QAAA,MAAM,YAAY,uFA6JjB,CAAA;AAGD,OAAO,EAAE,YAAY,EAAE,CAAA"}
1
+ {"version":3,"file":"bottom-navbar.d.ts","sourceRoot":"","sources":["../../src/shell/bottom-navbar.tsx"],"names":[],"mappings":"AAWA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAM9B,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAA;AAWrD,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,CAAC,EAAE,MAAM,CAAA;CACd;AAED,MAAM,WAAW,aAAa;IAC5B,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,EAAE,MAAM,CAAA;IACZ,uDAAuD;IACvD,IAAI,EAAE,SAAS,CAAA;IACf,wFAAwF;IACxF,UAAU,CAAC,EAAE,SAAS,CAAA;IACtB,uEAAuE;IACvE,KAAK,CAAC,EAAE,OAAO,CAAA;IACf,mFAAmF;IACnF,KAAK,CAAC,EAAE,MAAM,CAAA;IACd;;;OAGG;IACH,KAAK,CAAC,EAAE,MAAM,EAAE,CAAA;IAChB;;;OAGG;IACH,OAAO,CAAC,EAAE,CAAC,IAAI,EAAE,gBAAgB,GAAG,IAAI,KAAK,OAAO,CAAA;CACrD;AAED;;;;;;GAMG;AACH,MAAM,MAAM,kBAAkB,GAAG,MAAM,GAAG,WAAW,GAAG,MAAM,GAAG,MAAM,CAAA;AACvE,qFAAqF;AACrF,MAAM,MAAM,wBAAwB,GAAG,QAAQ,GAAG,UAAU,CAAA;AAE5D,MAAM,WAAW,iBAAkB,SAAQ,KAAK,CAAC,cAAc,CAAC,WAAW,CAAC;IAC1E,gCAAgC;IAChC,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,mFAAmF;IACnF,IAAI,CAAC,EAAE,gBAAgB,GAAG,IAAI,CAAA;IAC9B,6EAA6E;IAC7E,YAAY,CAAC,EAAE,aAAa,EAAE,CAAA;IAC9B,0DAA0D;IAC1D,SAAS,CAAC,EAAE,aAAa,EAAE,CAAA;IAC3B,8CAA8C;IAC9C,SAAS,CAAC,EAAE,kBAAkB,CAAA;IAC9B,0CAA0C;IAC1C,eAAe,CAAC,EAAE,wBAAwB,CAAA;IAC1C,+CAA+C;IAC/C,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAwID,QAAA,MAAM,YAAY,uFA0HjB,CAAA;AAGD,OAAO,EAAE,YAAY,EAAE,CAAA"}
@@ -4,158 +4,185 @@ import { springs as t } from "../ui/lib/motion.js";
4
4
  import { cn as n } from "../ui/lib/utils.js";
5
5
  import { Icon as r } from "../ui/icon.js";
6
6
  import { normalizeIcon as i } from "../_chunks/normalize-icon.js";
7
- import { useLink as a } from "../_chunks/link-context.js";
7
+ import { BadgeCompound as a } from "../_chunks/badge-group.js";
8
+ import { Sheet as o, SheetContent as s, SheetHeader as c, SheetTitle as l, SheetTrigger as u } from "../ui/sheet.js";
9
+ import { useLink as d } from "../_chunks/link-context.js";
8
10
  import "./link-context.js";
9
- import * as o from "react";
10
- import { useCallback as s, useEffect as c, useRef as l, useState as u } from "react";
11
- import { Fragment as d, jsx as f, jsxs as p } from "react/jsx-runtime";
12
- import { IconDots as m, IconX as h } from "@tabler/icons-react";
13
- import { AnimatePresence as g, motion as _, useReducedMotion as v } from "framer-motion";
11
+ import * as f from "react";
12
+ import { useState as p } from "react";
13
+ import { jsx as m, jsxs as h } from "react/jsx-runtime";
14
+ import { IconDots as g } from "@tabler/icons-react";
15
+ import { motion as _, useReducedMotion as v } from "framer-motion";
14
16
  //#region src/shell/bottom-navbar.tsx
15
- function y({ count: e }) {
17
+ function y(e, t) {
18
+ return e.canView ? e.canView(t) : e.roles && e.roles.length > 0 ? !!t?.role && e.roles.includes(t.role) : !0;
19
+ }
20
+ function b({ count: e }) {
16
21
  if (!e || e <= 0) return null;
17
- let t = e > 99 ? "99+" : String(e), r = e >= 10;
18
- return /* @__PURE__ */ f("span", {
22
+ let t = e > 99 ? "99+" : String(e);
23
+ return /* @__PURE__ */ m(a, {
24
+ color: "error",
25
+ variant: "solid",
26
+ size: "xs",
19
27
  "aria-label": `${e} notifications`,
20
- className: n("absolute -right-1 -top-0.5 flex h-4 min-w-4 items-center justify-center rounded-pill bg-error-9 text-[10px] font-semibold leading-none text-error-fg animate-in zoom-in-75", r ? "px-0.5" : ""),
28
+ className: "pointer-events-none absolute -end-1 -top-1 justify-center px-ds-01 tabular-nums motion-safe:animate-in motion-safe:zoom-in-75",
21
29
  children: t
22
30
  });
23
31
  }
24
- function b({ item: r, isActive: o, onClick: s }) {
25
- let c = a(), l = v();
26
- return /* @__PURE__ */ f(_.div, {
32
+ function x({ indicator: e, layoutId: n, reduced: r }) {
33
+ if (e === "none") return null;
34
+ let i = {
35
+ layoutId: n,
36
+ "aria-hidden": !0,
37
+ transition: r ? { duration: 0 } : t.snappy,
38
+ initial: !r && { opacity: 0 },
39
+ animate: { opacity: 1 }
40
+ };
41
+ return e === "underline" ? /* @__PURE__ */ m(_.span, {
42
+ ...i,
43
+ className: "absolute top-0 h-[3px] w-full rounded-b-control-inner bg-accent-9"
44
+ }) : e === "tint" ? /* @__PURE__ */ m(_.span, {
45
+ ...i,
46
+ className: "absolute inset-0 rounded-control bg-accent-3"
47
+ }) : /* @__PURE__ */ m(_.span, {
48
+ ...i,
49
+ className: "absolute inset-0 rounded-pill bg-accent-3"
50
+ });
51
+ }
52
+ var S = "relative flex h-16 w-full min-w-0 cursor-pointer flex-col items-center justify-center gap-ds-02 px-ds-01 pt-0 text-body-sm transition-colors duration-fast-02 ease-productive-standard";
53
+ function C({ item: r, isActive: a, indicator: o, indicatorId: s, showLabel: c, reduced: l }) {
54
+ let u = d();
55
+ return /* @__PURE__ */ m(_.div, {
27
56
  whileTap: l ? void 0 : { scale: .96 },
28
57
  transition: l ? { duration: 0 } : t.snappy,
29
- className: "flex max-w-[70px] flex-1",
30
- children: /* @__PURE__ */ f(c, {
58
+ className: "flex min-w-0 flex-1 basis-0",
59
+ children: /* @__PURE__ */ h(u, {
31
60
  href: r.href,
32
- onClick: s,
33
61
  "aria-label": r.title,
34
- "aria-current": o ? "page" : void 0,
35
- className: n("flex h-16 w-full cursor-pointer flex-col items-center gap-ds-02 p-ds-02 pt-0 text-body-sm transition-colors duration-fast-02 ease-productive-standard", o ? "font-semibold text-accent-11" : "text-surface-fg-subtle"),
36
- children: /* @__PURE__ */ p("div", {
37
- className: "relative flex w-full flex-col items-center gap-ds-02",
62
+ "aria-current": a ? "page" : void 0,
63
+ className: n(S, a ? "font-semibold text-accent-11" : "text-surface-fg-subtle"),
64
+ children: [a && o === "tint" && /* @__PURE__ */ m(x, {
65
+ indicator: "tint",
66
+ layoutId: s,
67
+ reduced: l
68
+ }), /* @__PURE__ */ h("div", {
69
+ className: "relative flex w-full min-w-0 flex-col items-center gap-ds-02",
38
70
  children: [
39
- o && /* @__PURE__ */ f(_.div, {
40
- layoutId: "bottom-nav-indicator",
41
- className: "absolute top-0 h-[3px] w-full rounded-b-control-inner bg-accent-9 p-0",
42
- "aria-hidden": "true",
43
- transition: l ? { duration: 0 } : t.snappy
71
+ a && o === "underline" && /* @__PURE__ */ m(x, {
72
+ indicator: "underline",
73
+ layoutId: s,
74
+ reduced: l
44
75
  }),
45
- /* @__PURE__ */ p("div", {
46
- className: "relative p-ds-03",
47
- children: [/* @__PURE__ */ f("span", {
48
- className: "[&>svg]:h-ico-md [&>svg]:w-ico-md",
49
- "aria-hidden": "true",
50
- children: /* @__PURE__ */ f(e, {
51
- size: "md",
52
- children: i(r.icon)
53
- })
54
- }), r.badge != null && /* @__PURE__ */ f(y, { count: r.badge })]
76
+ /* @__PURE__ */ h("div", {
77
+ className: "relative px-ds-04 py-ds-01",
78
+ children: [
79
+ a && o === "pill" && /* @__PURE__ */ m(x, {
80
+ indicator: "pill",
81
+ layoutId: s,
82
+ reduced: l
83
+ }),
84
+ /* @__PURE__ */ m("span", {
85
+ className: "relative [&>svg]:h-ico-md [&>svg]:w-ico-md",
86
+ "aria-hidden": "true",
87
+ children: /* @__PURE__ */ m(e, {
88
+ size: "md",
89
+ children: i(a && r.activeIcon ? r.activeIcon : r.icon)
90
+ })
91
+ }),
92
+ r.badge != null && /* @__PURE__ */ m(b, { count: r.badge })
93
+ ]
55
94
  }),
56
- /* @__PURE__ */ f("span", {
57
- className: "text-center",
95
+ c && /* @__PURE__ */ m("span", {
96
+ className: "max-w-full truncate text-center",
58
97
  children: r.title
59
98
  })
60
99
  ]
61
- })
100
+ })]
62
101
  })
63
102
  });
64
103
  }
65
- var x = o.forwardRef(({ currentPath: o = "/", user: y, primaryItems: x = [], moreItems: S = [], className: C, ...w }, T) => {
66
- let E = a(), D = v(), [O, k] = u(!1), A = l(null), j = s(() => k(!1), []);
67
- c(() => {
68
- O && A.current && A.current.querySelector("a, button")?.focus();
69
- }, [O]);
70
- let M = (e, t = !1) => t || e === "/" ? o === e : o.startsWith(e), N = S.some((e) => M(e.href, e.exact));
71
- return /* @__PURE__ */ p(d, { children: [/* @__PURE__ */ f(g, { children: O && /* @__PURE__ */ p("div", {
72
- className: "fixed inset-0 z-overlay md:hidden",
73
- onClick: () => k(!1),
74
- children: [/* @__PURE__ */ f("div", { className: "absolute inset-0 bg-overlay" }), /* @__PURE__ */ p(_.div, {
75
- ref: A,
76
- role: "dialog",
77
- "aria-label": "More navigation",
78
- initial: { y: "100%" },
79
- animate: { y: 0 },
80
- exit: { y: "100%" },
81
- transition: D ? { duration: 0 } : t.smooth,
82
- className: "absolute bottom-[72px] left-0 right-0 rounded-t-bubble border-t border-surface-border-strong bg-surface-overlay p-ds-05 pb-ds-03",
83
- onClick: (e) => e.stopPropagation(),
84
- onKeyDown: (e) => {
85
- e.stopPropagation(), e.key === "Escape" && j();
86
- },
87
- children: [/* @__PURE__ */ p("div", {
88
- className: "mb-ds-04 flex items-center justify-between",
89
- children: [/* @__PURE__ */ f("span", {
90
- className: "text-body-md font-semibold text-surface-fg",
91
- children: "More"
92
- }), /* @__PURE__ */ f("button", {
93
- onClick: () => k(!1),
94
- "aria-label": "Close more menu",
95
- className: "flex h-ds-sm w-ds-sm items-center justify-center rounded-pill hover:bg-surface-raised-hover",
96
- children: /* @__PURE__ */ f(r, {
97
- icon: h,
98
- size: "sm",
99
- className: "text-surface-fg-muted"
100
- })
101
- })]
102
- }), /* @__PURE__ */ f("div", {
103
- className: "grid grid-cols-4 gap-ds-03",
104
- children: S.map((t) => /* @__PURE__ */ p(E, {
105
- href: t.href,
106
- onClick: () => k(!1),
107
- className: n("flex flex-col items-center gap-ds-02b rounded-overlay-lg p-ds-04 text-body-sm transition-colors ease-productive-standard", M(t.href, t.exact) ? "bg-surface-raised-hover text-accent-11" : "text-surface-fg-subtle hover:bg-surface-raised-hover"),
108
- children: [/* @__PURE__ */ f("span", {
109
- className: "[&>svg]:h-ico-md [&>svg]:w-ico-md",
110
- children: /* @__PURE__ */ f(e, {
111
- size: "md",
112
- children: i(t.icon)
113
- })
114
- }), /* @__PURE__ */ f("span", {
115
- className: "text-center",
116
- children: t.title
117
- })]
118
- }, t.href))
119
- })]
120
- })]
121
- }) }), /* @__PURE__ */ p("nav", {
122
- ...w,
123
- ref: T,
104
+ var w = f.forwardRef(({ currentPath: a = "/", user: b = null, primaryItems: w = [], moreItems: T = [], indicator: E = "pill", labelVisibility: D = "always", className: O, ...k }, A) => {
105
+ let j = d(), M = v() ?? !1, [N, P] = p(!1), F = `${f.useId()}-nav-indicator`, I = (e, t = !1) => t || e === "/" ? a === e : a.startsWith(e), L = w.filter((e) => y(e, b)), R = T.filter((e) => y(e, b)), z = R.some((e) => I(e.href, e.exact)), B = N || z, V = D === "always" || B;
106
+ return /* @__PURE__ */ h("nav", {
107
+ ...k,
108
+ ref: A,
124
109
  "aria-label": "Mobile navigation",
125
- className: n("fixed bottom-0 left-0 right-0 z-sticky flex w-full flex-row items-start justify-between border-t border-surface-border-strong bg-surface-chrome px-ds-05 pb-safe pt-0 md:hidden", C),
126
- children: [x.map((e) => /* @__PURE__ */ f(b, {
110
+ className: n("fixed bottom-0 start-0 end-0 z-sticky flex w-full flex-row items-stretch justify-between border-t border-surface-border-strong bg-surface-chrome px-ds-05 pb-safe pt-0 md:hidden", O),
111
+ children: [L.map((e) => /* @__PURE__ */ m(C, {
127
112
  item: e,
128
- isActive: M(e.href, e.exact)
129
- }, e.href)), S.length > 0 && /* @__PURE__ */ f(_.button, {
130
- type: "button",
131
- onClick: () => k(!O),
132
- "aria-label": "More navigation options",
133
- "aria-expanded": O,
134
- whileTap: D ? void 0 : { y: -2 },
135
- transition: D ? { duration: 0 } : t.snappy,
136
- className: n("flex h-16 max-w-[70px] flex-1 cursor-pointer flex-col items-center gap-ds-02 p-ds-02 pt-0 text-body-sm", O || N ? "font-semibold text-accent-11" : "text-surface-fg-subtle"),
137
- children: /* @__PURE__ */ p("div", {
138
- className: "relative flex w-full flex-col items-center gap-ds-02",
139
- children: [
140
- (O || N) && /* @__PURE__ */ f(_.div, {
141
- layoutId: "bottom-nav-indicator",
142
- className: "absolute top-0 h-[3px] w-full rounded-b-control-inner bg-accent-9 p-0",
143
- "aria-hidden": "true",
144
- transition: D ? { duration: 0 } : t.snappy
145
- }),
146
- /* @__PURE__ */ f("div", {
147
- className: "p-ds-03",
148
- children: /* @__PURE__ */ f(r, { icon: m })
149
- }),
150
- /* @__PURE__ */ f("span", {
151
- className: "text-center",
152
- children: "More"
113
+ isActive: I(e.href, e.exact),
114
+ indicator: E,
115
+ indicatorId: F,
116
+ showLabel: D === "always" || I(e.href, e.exact),
117
+ reduced: M
118
+ }, e.href)), R.length > 0 && /* @__PURE__ */ h(o, {
119
+ open: N,
120
+ onOpenChange: P,
121
+ children: [/* @__PURE__ */ m(u, {
122
+ asChild: !0,
123
+ children: /* @__PURE__ */ h(_.button, {
124
+ type: "button",
125
+ "aria-label": "More navigation options",
126
+ whileTap: M ? void 0 : { scale: .96 },
127
+ transition: M ? { duration: 0 } : t.snappy,
128
+ className: n(S, "min-w-0 flex-1 basis-0", B ? "font-semibold text-accent-11" : "text-surface-fg-subtle"),
129
+ children: [B && E === "tint" && /* @__PURE__ */ m(x, {
130
+ indicator: "tint",
131
+ layoutId: F,
132
+ reduced: M
133
+ }), /* @__PURE__ */ h("div", {
134
+ className: "relative flex w-full min-w-0 flex-col items-center gap-ds-02",
135
+ children: [
136
+ B && E === "underline" && /* @__PURE__ */ m(x, {
137
+ indicator: "underline",
138
+ layoutId: F,
139
+ reduced: M
140
+ }),
141
+ /* @__PURE__ */ h("div", {
142
+ className: "relative px-ds-04 py-ds-01",
143
+ children: [B && E === "pill" && /* @__PURE__ */ m(x, {
144
+ indicator: "pill",
145
+ layoutId: F,
146
+ reduced: M
147
+ }), /* @__PURE__ */ m(r, { icon: g })]
148
+ }),
149
+ V && /* @__PURE__ */ m("span", {
150
+ className: "max-w-full truncate text-center",
151
+ children: "More"
152
+ })
153
+ ]
154
+ })]
155
+ })
156
+ }), /* @__PURE__ */ h(s, {
157
+ side: "bottom",
158
+ className: "rounded-t-bubble",
159
+ children: [/* @__PURE__ */ m(c, { children: /* @__PURE__ */ m(l, { children: "More" }) }), /* @__PURE__ */ m("div", {
160
+ className: "grid grid-cols-[repeat(auto-fit,minmax(72px,1fr))] gap-ds-03 pt-ds-04",
161
+ children: R.map((t) => {
162
+ let r = I(t.href, t.exact);
163
+ return /* @__PURE__ */ h(j, {
164
+ href: t.href,
165
+ onClick: () => P(!1),
166
+ "aria-current": r ? "page" : void 0,
167
+ className: n("flex min-w-0 flex-col items-center gap-ds-02b rounded-overlay-lg p-ds-04 text-body-sm transition-colors ease-productive-standard", r ? "bg-surface-raised-hover text-accent-11" : "text-surface-fg-subtle hover:bg-surface-raised-hover"),
168
+ children: [/* @__PURE__ */ m("span", {
169
+ className: "[&>svg]:h-ico-md [&>svg]:w-ico-md",
170
+ "aria-hidden": "true",
171
+ children: /* @__PURE__ */ m(e, {
172
+ size: "md",
173
+ children: i(t.icon)
174
+ })
175
+ }), /* @__PURE__ */ m("span", {
176
+ className: "max-w-full truncate text-center",
177
+ children: t.title
178
+ })]
179
+ }, t.href);
153
180
  })
154
- ]
155
- })
181
+ })]
182
+ })]
156
183
  })]
157
- })] });
184
+ });
158
185
  });
159
- x.displayName = "BottomNavbar";
186
+ w.displayName = "BottomNavbar";
160
187
  //#endregion
161
- export { x as BottomNavbar };
188
+ export { w as BottomNavbar };
@@ -1 +1 @@
1
- {"version":3,"file":"bottom-navbar.js","names":[],"sources":["../../src/shell/bottom-navbar.tsx"],"sourcesContent":["'use client'\n\n/**\n * BottomNavbar -- Mobile bottom navigation bar.\n *\n * Props-driven: accepts currentPath, user, navItems instead of\n * reading from Remix hooks or Zustand stores.\n */\nimport { IconDots, IconX } from '@tabler/icons-react'\nimport { AnimatePresence, motion, useReducedMotion } from 'framer-motion'\nimport * as React from 'react'\nimport { useCallback,useEffect, useRef, useState } from 'react'\n\nimport { Icon } from '../ui/icon'\nimport { IconProvider } from '../ui/icon-context'\nimport type { IconInput } from '../ui/lib/icon-input'\nimport { springs } from '../ui/lib/motion'\nimport { normalizeIcon } from '../ui/lib/normalize-icon'\nimport { cn } from '../ui/lib/utils'\nimport { useLink } from './link-context'\n\n// -----------------------------------------------------------------------\n// Types\n// -----------------------------------------------------------------------\n\nexport interface BottomNavItem {\n title: string\n href: string\n /** Icon for this nav item. Accepts any `IconInput`. */\n icon: IconInput\n /** When true, the route matches only when the path is exactly equal */\n exact?: boolean\n /** Notification badge count. 0 or undefined = hidden, 1–99 = shown, >99 = \"99+\" */\n badge?: number\n}\n\nexport interface BottomNavbarUser {\n name: string\n role?: string\n}\n\nexport interface BottomNavbarProps\n extends React.HTMLAttributes<HTMLElement> {\n /** Currently active pathname */\n currentPath?: string\n /** User information (used to determine admin status, presence) */\n user?: BottomNavbarUser | null\n /** Primary nav items shown directly in the bottom bar (max 4 recommended) */\n primaryItems?: BottomNavItem[]\n /** Additional items shown in the \"More\" overflow menu */\n moreItems?: BottomNavItem[]\n /** Additional className for the nav element */\n className?: string\n}\n\n// -----------------------------------------------------------------------\n// NavBadge (internal)\n// -----------------------------------------------------------------------\n\nfunction NavBadge({ count }: { count: number }) {\n if (!count || count <= 0) return null\n const display = count > 99 ? '99+' : String(count)\n const isMultiDigit = count >= 10\n return (\n <span\n aria-label={`${count} notifications`}\n className={cn(\n 'absolute -right-1 -top-0.5 flex h-4 min-w-4 items-center justify-center rounded-pill bg-error-9 text-[10px] font-semibold leading-none text-error-fg animate-in zoom-in-75',\n isMultiDigit ? 'px-0.5' : '',\n )}\n >\n {display}\n </span>\n )\n}\n\n// -----------------------------------------------------------------------\n// BottomNavLink (internal)\n// -----------------------------------------------------------------------\n\nfunction BottomNavLink({\n item,\n isActive,\n onClick,\n}: {\n item: BottomNavItem\n isActive: boolean\n onClick?: () => void\n}) {\n const Link = useLink()\n const prefersReducedMotion = useReducedMotion()\n return (\n <motion.div whileTap={prefersReducedMotion ? undefined : { scale: 0.96 }} transition={prefersReducedMotion ? { duration: 0 } : springs.snappy} className=\"flex max-w-[70px] flex-1\">\n <Link\n href={item.href}\n onClick={onClick}\n aria-label={item.title}\n aria-current={isActive ? 'page' : undefined}\n className={cn(\n 'flex h-16 w-full cursor-pointer flex-col items-center gap-ds-02 p-ds-02 pt-0 text-body-sm transition-colors duration-fast-02 ease-productive-standard',\n isActive\n ? 'font-semibold text-accent-11'\n : 'text-surface-fg-subtle',\n )}\n >\n <div className=\"relative flex w-full flex-col items-center gap-ds-02\">\n {isActive && (\n <motion.div\n layoutId=\"bottom-nav-indicator\"\n className=\"absolute top-0 h-[3px] w-full rounded-b-control-inner bg-accent-9 p-0\"\n aria-hidden=\"true\"\n transition={prefersReducedMotion ? { duration: 0 } : springs.snappy}\n />\n )}\n <div className=\"relative p-ds-03\">\n <span className=\"[&>svg]:h-ico-md [&>svg]:w-ico-md\" aria-hidden=\"true\"><IconProvider size=\"md\">{normalizeIcon(item.icon)}</IconProvider></span>\n {item.badge != null && <NavBadge count={item.badge} />}\n </div>\n <span className=\"text-center\">{item.title}</span>\n </div>\n </Link>\n </motion.div>\n )\n}\n\n// -----------------------------------------------------------------------\n// BottomNavbar\n// -----------------------------------------------------------------------\n\nconst BottomNavbar = React.forwardRef<HTMLElement, BottomNavbarProps>(\n (\n {\n currentPath = '/',\n user: _user,\n primaryItems = [],\n moreItems = [],\n className,\n ...props\n },\n ref,\n ) => {\n const Link = useLink()\n const prefersReducedMotion = useReducedMotion()\n const [showMore, setShowMore] = useState(false)\n const overlayRef = useRef<HTMLDivElement>(null)\n\n const closeMore = useCallback(() => setShowMore(false), [])\n\n // Focus first focusable item when overlay opens\n useEffect(() => {\n if (showMore && overlayRef.current) {\n const firstFocusable = overlayRef.current.querySelector<HTMLElement>('a, button')\n firstFocusable?.focus()\n }\n }, [showMore])\n\n const isActive = (path: string, exact = false) => {\n if (exact || path === '/') {\n return currentPath === path\n }\n return currentPath.startsWith(path)\n }\n\n // Check if any \"more\" item is active\n const isMoreActive = moreItems.some((item) =>\n isActive(item.href, item.exact),\n )\n\n return (\n <>\n {/* More Menu Overlay */}\n <AnimatePresence>\n {showMore && (\n // eslint-disable-next-line jsx-a11y/no-static-element-interactions, jsx-a11y/click-events-have-key-events -- backdrop overlay, dismiss via mouse only; keyboard users close via Escape\n <div\n className=\"fixed inset-0 z-overlay md:hidden\"\n onClick={() => setShowMore(false)}\n >\n <div className=\"absolute inset-0 bg-overlay\" />\n { }\n <motion.div\n ref={overlayRef}\n role=\"dialog\"\n aria-label=\"More navigation\"\n initial={{ y: '100%' }}\n animate={{ y: 0 }}\n exit={{ y: '100%' }}\n transition={prefersReducedMotion ? { duration: 0 } : springs.smooth}\n className=\"absolute bottom-[72px] left-0 right-0 rounded-t-bubble border-t border-surface-border-strong bg-surface-overlay p-ds-05 pb-ds-03\"\n onClick={(e) => e.stopPropagation()}\n onKeyDown={(e) => {\n e.stopPropagation()\n if (e.key === 'Escape') closeMore()\n }}\n >\n <div className=\"mb-ds-04 flex items-center justify-between\">\n <span className=\"text-body-md font-semibold text-surface-fg\">\n More\n </span>\n <button\n onClick={() => setShowMore(false)}\n aria-label=\"Close more menu\"\n className=\"flex h-ds-sm w-ds-sm items-center justify-center rounded-pill hover:bg-surface-raised-hover\"\n >\n <Icon icon={IconX} size=\"sm\" className=\"text-surface-fg-muted\" />\n </button>\n </div>\n <div className=\"grid grid-cols-4 gap-ds-03\">\n {moreItems.map((item) => (\n <Link\n key={item.href}\n href={item.href}\n onClick={() => setShowMore(false)}\n className={cn(\n 'flex flex-col items-center gap-ds-02b rounded-overlay-lg p-ds-04 text-body-sm transition-colors ease-productive-standard',\n isActive(item.href, item.exact)\n ? 'bg-surface-raised-hover text-accent-11'\n : 'text-surface-fg-subtle hover:bg-surface-raised-hover',\n )}\n >\n <span className=\"[&>svg]:h-ico-md [&>svg]:w-ico-md\"><IconProvider size=\"md\">{normalizeIcon(item.icon)}</IconProvider></span>\n <span className=\"text-center\">\n {item.title}\n </span>\n </Link>\n ))}\n </div>\n </motion.div>\n </div>\n )}\n </AnimatePresence>\n\n {/* Bottom Navigation Bar */}\n <nav\n {...props}\n ref={ref}\n aria-label=\"Mobile navigation\"\n className={cn(\n 'fixed bottom-0 left-0 right-0 z-sticky flex w-full flex-row items-start justify-between border-t border-surface-border-strong bg-surface-chrome px-ds-05 pb-safe pt-0 md:hidden',\n className,\n )}\n >\n {primaryItems.map((item) => (\n <BottomNavLink\n key={item.href}\n item={item}\n isActive={isActive(item.href, item.exact)}\n />\n ))}\n\n {/* More Button */}\n {moreItems.length > 0 && (\n <motion.button\n type=\"button\"\n onClick={() => setShowMore(!showMore)}\n aria-label=\"More navigation options\"\n aria-expanded={showMore}\n whileTap={prefersReducedMotion ? undefined : { y: -2 }}\n transition={prefersReducedMotion ? { duration: 0 } : springs.snappy}\n className={cn(\n 'flex h-16 max-w-[70px] flex-1 cursor-pointer flex-col items-center gap-ds-02 p-ds-02 pt-0 text-body-sm',\n showMore || isMoreActive\n ? 'font-semibold text-accent-11'\n : 'text-surface-fg-subtle',\n )}\n >\n <div className=\"relative flex w-full flex-col items-center gap-ds-02\">\n {(showMore || isMoreActive) && (\n <motion.div\n layoutId=\"bottom-nav-indicator\"\n className=\"absolute top-0 h-[3px] w-full rounded-b-control-inner bg-accent-9 p-0\"\n aria-hidden=\"true\"\n transition={prefersReducedMotion ? { duration: 0 } : springs.snappy}\n />\n )}\n <div className=\"p-ds-03\">\n <Icon icon={IconDots} />\n </div>\n <span className=\"text-center\">More</span>\n </div>\n </motion.button>\n )}\n </nav>\n </>\n )\n },\n)\nBottomNavbar.displayName = 'BottomNavbar'\n\nexport { BottomNavbar }\n"],"mappings":"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"}
1
+ {"version":3,"file":"bottom-navbar.js","names":[],"sources":["../../src/shell/bottom-navbar.tsx"],"sourcesContent":["'use client'\n\n/**\n * BottomNavbar -- Mobile bottom navigation bar.\n *\n * Props-driven (currentPath / user / items) and router-agnostic via `useLink`.\n * The \"More\" overflow uses the DS `Sheet` primitive, so it inherits focus trap,\n * scroll lock, return-focus, `aria-modal`, and trigger↔panel ARIA wiring.\n */\nimport { IconDots } from '@tabler/icons-react'\nimport { motion, useReducedMotion } from 'framer-motion'\nimport * as React from 'react'\nimport { useState } from 'react'\n\nimport { Badge } from '../ui/badge'\nimport { Icon } from '../ui/icon'\nimport { IconProvider } from '../ui/icon-context'\nimport type { IconInput } from '../ui/lib/icon-input'\nimport { springs } from '../ui/lib/motion'\nimport { normalizeIcon } from '../ui/lib/normalize-icon'\nimport { cn } from '../ui/lib/utils'\nimport { Sheet, SheetContent, SheetHeader, SheetTitle, SheetTrigger } from '../ui/sheet'\nimport { useLink } from './link-context'\n\n// -----------------------------------------------------------------------\n// Types\n// -----------------------------------------------------------------------\n\nexport interface BottomNavbarUser {\n name: string\n role?: string\n}\n\nexport interface BottomNavItem {\n title: string\n href: string\n /** Icon for this nav item. Accepts any `IconInput`. */\n icon: IconInput\n /** Icon shown when the item is active — e.g. a filled variant. Falls back to `icon`. */\n activeIcon?: IconInput\n /** When true, the route matches only when the path is exactly equal */\n exact?: boolean\n /** Notification badge count. 0 or undefined = hidden, 1–99 = shown, >99 = \"99+\" */\n badge?: number\n /**\n * Roles allowed to see this item, matched against `user.role`. Omit to show\n * it to everyone. For arbitrary logic, use `canView` instead.\n */\n roles?: string[]\n /**\n * Visibility predicate — full control over whether this item renders.\n * Receives the current `user` (or `null`). Takes precedence over `roles`.\n */\n canView?: (user: BottomNavbarUser | null) => boolean\n}\n\n/**\n * Active-item indicator, animated between items on selection:\n * - `pill` (default) — Material-3 tonal pill behind the icon\n * - `underline` — top accent bar (Material-2 tab style)\n * - `tint` — subtle background tint on the whole active cell\n * - `none` — no shape; rely on the filled `activeIcon` + accent color (iOS)\n */\nexport type BottomNavIndicator = 'pill' | 'underline' | 'tint' | 'none'\n/** Show labels always (default) or only for the selected item (narrow viewports). */\nexport type BottomNavLabelVisibility = 'always' | 'selected'\n\nexport interface BottomNavbarProps extends React.HTMLAttributes<HTMLElement> {\n /** Currently active pathname */\n currentPath?: string\n /** Current user. Drives per-item role gating via `item.roles` / `item.canView`. */\n user?: BottomNavbarUser | null\n /** Primary nav items shown directly in the bottom bar (max 4 recommended) */\n primaryItems?: BottomNavItem[]\n /** Additional items shown in the \"More\" overflow sheet */\n moreItems?: BottomNavItem[]\n /** Active-indicator style. @default 'pill' */\n indicator?: BottomNavIndicator\n /** Label visibility. @default 'always' */\n labelVisibility?: BottomNavLabelVisibility\n /** Additional className for the nav element */\n className?: string\n}\n\n// -----------------------------------------------------------------------\n// Helpers\n// -----------------------------------------------------------------------\n\n/** Per-item role/visibility gate. `canView` wins; else `roles` matched against user.role; else visible. */\nfunction itemVisible(item: BottomNavItem, user: BottomNavbarUser | null): boolean {\n if (item.canView) return item.canView(user)\n if (item.roles && item.roles.length > 0) {\n return !!user?.role && item.roles.includes(user.role)\n }\n return true\n}\n\n// -----------------------------------------------------------------------\n// NavBadge (composes Badge)\n// -----------------------------------------------------------------------\n\nfunction NavBadge({ count }: { count: number }) {\n if (!count || count <= 0) return null\n const display = count > 99 ? '99+' : String(count)\n return (\n <Badge\n color=\"error\"\n variant=\"solid\"\n size=\"xs\"\n aria-label={`${count} notifications`}\n className=\"pointer-events-none absolute -end-1 -top-1 justify-center px-ds-01 tabular-nums motion-safe:animate-in motion-safe:zoom-in-75\"\n >\n {display}\n </Badge>\n )\n}\n\n// -----------------------------------------------------------------------\n// Active indicator (shared-element)\n// -----------------------------------------------------------------------\n\n/**\n * The active-item indicator. A single shared-element (`layoutId`) that SLIDES\n * to the selected item and fades in on first appearance. `none` renders\n * nothing (the filled `activeIcon` + accent color carry the state).\n */\nfunction ActiveIndicator({\n indicator,\n layoutId,\n reduced,\n}: {\n indicator: BottomNavIndicator\n layoutId: string\n reduced: boolean\n}) {\n if (indicator === 'none') return null\n const common = {\n layoutId,\n 'aria-hidden': true as const,\n transition: reduced ? { duration: 0 } : springs.snappy,\n initial: reduced ? false : { opacity: 0 },\n animate: { opacity: 1 },\n }\n if (indicator === 'underline') {\n return <motion.span {...common} className=\"absolute top-0 h-[3px] w-full rounded-b-control-inner bg-accent-9\" />\n }\n if (indicator === 'tint') {\n return <motion.span {...common} className=\"absolute inset-0 rounded-control bg-accent-3\" />\n }\n // pill (default) — Material-3 tonal pill behind the icon\n return <motion.span {...common} className=\"absolute inset-0 rounded-pill bg-accent-3\" />\n}\n\n// -----------------------------------------------------------------------\n// BottomNavLink (internal)\n// -----------------------------------------------------------------------\n\nconst itemClass =\n 'relative flex h-16 w-full min-w-0 cursor-pointer flex-col items-center justify-center gap-ds-02 px-ds-01 pt-0 text-body-sm transition-colors duration-fast-02 ease-productive-standard'\n\nfunction BottomNavLink({\n item,\n isActive,\n indicator,\n indicatorId,\n showLabel,\n reduced,\n}: {\n item: BottomNavItem\n isActive: boolean\n indicator: BottomNavIndicator\n indicatorId: string\n showLabel: boolean\n reduced: boolean\n}) {\n const Link = useLink()\n return (\n <motion.div\n whileTap={reduced ? undefined : { scale: 0.96 }}\n transition={reduced ? { duration: 0 } : springs.snappy}\n className=\"flex min-w-0 flex-1 basis-0\"\n >\n <Link\n href={item.href}\n aria-label={item.title}\n aria-current={isActive ? 'page' : undefined}\n className={cn(itemClass, isActive ? 'font-semibold text-accent-11' : 'text-surface-fg-subtle')}\n >\n {/* tint covers the whole cell — sits behind the content */}\n {isActive && indicator === 'tint' && (\n <ActiveIndicator indicator=\"tint\" layoutId={indicatorId} reduced={reduced} />\n )}\n <div className=\"relative flex w-full min-w-0 flex-col items-center gap-ds-02\">\n {isActive && indicator === 'underline' && (\n <ActiveIndicator indicator=\"underline\" layoutId={indicatorId} reduced={reduced} />\n )}\n <div className=\"relative px-ds-04 py-ds-01\">\n {isActive && indicator === 'pill' && (\n <ActiveIndicator indicator=\"pill\" layoutId={indicatorId} reduced={reduced} />\n )}\n <span className=\"relative [&>svg]:h-ico-md [&>svg]:w-ico-md\" aria-hidden=\"true\">\n <IconProvider size=\"md\">\n {normalizeIcon(isActive && item.activeIcon ? item.activeIcon : item.icon)}\n </IconProvider>\n </span>\n {item.badge != null && <NavBadge count={item.badge} />}\n </div>\n {showLabel && <span className=\"max-w-full truncate text-center\">{item.title}</span>}\n </div>\n </Link>\n </motion.div>\n )\n}\n\n// -----------------------------------------------------------------------\n// BottomNavbar\n// -----------------------------------------------------------------------\n\nconst BottomNavbar = React.forwardRef<HTMLElement, BottomNavbarProps>(\n (\n {\n currentPath = '/',\n user = null,\n primaryItems = [],\n moreItems = [],\n indicator = 'pill',\n labelVisibility = 'always',\n className,\n ...props\n },\n ref,\n ) => {\n const Link = useLink()\n const reduced = useReducedMotion() ?? false\n const [showMore, setShowMore] = useState(false)\n // Scope the shared-element layoutId per instance so two navbars don't animate into each other.\n const instanceId = React.useId()\n const indicatorId = `${instanceId}-nav-indicator`\n\n const isActive = (path: string, exact = false) => {\n if (exact || path === '/') return currentPath === path\n return currentPath.startsWith(path)\n }\n\n // Role/visibility gating\n const visiblePrimary = primaryItems.filter((item) => itemVisible(item, user))\n const visibleMore = moreItems.filter((item) => itemVisible(item, user))\n\n const isMoreActive = visibleMore.some((item) => isActive(item.href, item.exact))\n const moreSelected = showMore || isMoreActive\n const showMoreLabel = labelVisibility === 'always' || moreSelected\n\n return (\n <nav\n {...props}\n ref={ref}\n aria-label=\"Mobile navigation\"\n className={cn(\n 'fixed bottom-0 start-0 end-0 z-sticky flex w-full flex-row items-stretch justify-between border-t border-surface-border-strong bg-surface-chrome px-ds-05 pb-safe pt-0 md:hidden',\n className,\n )}\n >\n {visiblePrimary.map((item) => (\n <BottomNavLink\n key={item.href}\n item={item}\n isActive={isActive(item.href, item.exact)}\n indicator={indicator}\n indicatorId={indicatorId}\n showLabel={labelVisibility === 'always' || isActive(item.href, item.exact)}\n reduced={reduced}\n />\n ))}\n\n {visibleMore.length > 0 && (\n <Sheet open={showMore} onOpenChange={setShowMore}>\n <SheetTrigger asChild>\n <motion.button\n type=\"button\"\n aria-label=\"More navigation options\"\n whileTap={reduced ? undefined : { scale: 0.96 }}\n transition={reduced ? { duration: 0 } : springs.snappy}\n className={cn(\n itemClass,\n 'min-w-0 flex-1 basis-0',\n moreSelected ? 'font-semibold text-accent-11' : 'text-surface-fg-subtle',\n )}\n >\n {moreSelected && indicator === 'tint' && (\n <ActiveIndicator indicator=\"tint\" layoutId={indicatorId} reduced={reduced} />\n )}\n <div className=\"relative flex w-full min-w-0 flex-col items-center gap-ds-02\">\n {moreSelected && indicator === 'underline' && (\n <ActiveIndicator indicator=\"underline\" layoutId={indicatorId} reduced={reduced} />\n )}\n <div className=\"relative px-ds-04 py-ds-01\">\n {moreSelected && indicator === 'pill' && (\n <ActiveIndicator indicator=\"pill\" layoutId={indicatorId} reduced={reduced} />\n )}\n <Icon icon={IconDots} />\n </div>\n {showMoreLabel && <span className=\"max-w-full truncate text-center\">More</span>}\n </div>\n </motion.button>\n </SheetTrigger>\n\n <SheetContent side=\"bottom\" className=\"rounded-t-bubble\">\n <SheetHeader>\n <SheetTitle>More</SheetTitle>\n </SheetHeader>\n <div className=\"grid grid-cols-[repeat(auto-fit,minmax(72px,1fr))] gap-ds-03 pt-ds-04\">\n {visibleMore.map((item) => {\n const active = isActive(item.href, item.exact)\n return (\n <Link\n key={item.href}\n href={item.href}\n onClick={() => setShowMore(false)}\n aria-current={active ? 'page' : undefined}\n className={cn(\n 'flex min-w-0 flex-col items-center gap-ds-02b rounded-overlay-lg p-ds-04 text-body-sm transition-colors ease-productive-standard',\n active\n ? 'bg-surface-raised-hover text-accent-11'\n : 'text-surface-fg-subtle hover:bg-surface-raised-hover',\n )}\n >\n <span className=\"[&>svg]:h-ico-md [&>svg]:w-ico-md\" aria-hidden=\"true\">\n <IconProvider size=\"md\">{normalizeIcon(item.icon)}</IconProvider>\n </span>\n <span className=\"max-w-full truncate text-center\">{item.title}</span>\n </Link>\n )\n })}\n </div>\n </SheetContent>\n </Sheet>\n )}\n </nav>\n )\n },\n)\nBottomNavbar.displayName = 'BottomNavbar'\n\nexport { BottomNavbar }\n"],"mappings":"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"}
@@ -49,7 +49,9 @@
49
49
 
50
50
  /* ── Neutral — Warm grays — H:350 ── */
51
51
  --neutral-0: #ffffff;
52
- --color-surface-0: oklch(0.945 0.008 360);
52
+ /* Sunken well. Chroma follows the neutral cadence (was H:360 C:0.008 — an
53
+ outlier that read visibly pink on sunken surfaces). */
54
+ --color-surface-0: oklch(0.945 0.0013 350);
53
55
  --neutral-1: oklch(0.99 0.0003 350);
54
56
  --neutral-2: oklch(0.97 0.0008 350);
55
57
  --neutral-3: oklch(0.93 0.0018 350);
@@ -262,7 +264,9 @@
262
264
  --purple-12: oklch(0.88 0.0316 300);
263
265
 
264
266
  /* ── Neutral — Warm grays — H:350 ── */
265
- --color-surface-0: oklch(0.07 0.008 360);
267
+ /* Sunken well (deep). Chroma follows the neutral cadence — was H:360 C:0.008,
268
+ an outlier that read tinted rather than neutral. */
269
+ --color-surface-0: oklch(0.07 0.0004 350);
266
270
  --neutral-1: oklch(0.11 0.0002 350);
267
271
  --neutral-2: oklch(0.17 0.0007 350);
268
272
  --neutral-3: oklch(0.23 0.0019 350);
@@ -188,6 +188,14 @@
188
188
  --color-surface-border-card: color-mix(in oklch, var(--neutral-5) 30%, transparent);
189
189
  --color-backdrop: oklch(0 0 0 / 0.4);
190
190
 
191
+ /* Segmented control — translucent recess + raised thumb. Alpha, NOT a fixed
192
+ neutral tier: the track must read as a groove on ANY parent (page, card,
193
+ chrome), and a fixed lightness loses contrast against an unknown surface.
194
+ Elevation inverts in dark (see .dark override): recess in light, faint
195
+ lift in dark, thumb always a step toward the foreground. */
196
+ --color-segment-track: color-mix(in oklch, var(--neutral-12) 6%, transparent);
197
+ --color-segment-thumb: var(--neutral-1);
198
+
191
199
  /* Overlay + disabled — promoted from internal :root into @theme so
192
200
  `bg-overlay`, `text-overlay`, `bg-disabled`, `text-disabled` emit. */
193
201
  --color-overlay: oklch(0 0 0 / 0.50);
@@ -374,8 +382,10 @@
374
382
  --radius-overlay-sm rounded-overlay-sm Tooltip, Toast
375
383
  --radius-overlay rounded-overlay Popover, HoverCard, DropdownMenu / ContextMenu / Menubar content, listboxes, NavigationMenu viewport
376
384
  --radius-overlay-lg rounded-overlay-lg Dialog, AlertDialog, Sheet, BottomSheet, ColorInput picker
377
- --radius-pill rounded-pill Badge, Dot, Radio, Switch, Slider, Progress, Avatar circle, SegmentedControl item
385
+ --radius-pill rounded-pill Badge, Dot, Radio, Switch, Slider, Progress, Avatar circle
378
386
  --radius-bubble rounded-bubble ChatMessage bubble
387
+ (SegmentedControl is a rounded-RECT, not a pill: track rounded-surface,
388
+ thumb rounded-control — thumb inner radius sits tighter inside the track.)
379
389
  ─────────────────────────────────────────────────────────── */
380
390
  --radius-control: 6px;
381
391
  --radius-control-inner: 2px;
@@ -418,6 +428,10 @@
418
428
  ─────────────────────────────────────────────────────────── */
419
429
  --shadow-raised: var(--shadow-xs-internal);
420
430
  --shadow-raised-hover: var(--shadow-sm-internal);
431
+ /* Segmented-control thumb — soft float, deliberately RING-LESS (the track
432
+ carries no border/inset, so the thumb defines its own edge with shadow
433
+ alone). Uses --shadow-color/strength so it deepens in dark (strength 2.5). */
434
+ --shadow-segment: 0 1px 2px -0.5px oklch(var(--shadow-color) / calc(0.10 * var(--shadow-strength))), 0 3px 8px -2px oklch(var(--shadow-color) / calc(0.09 * var(--shadow-strength)));
421
435
  --shadow-floating: var(--shadow-md-internal);
422
436
  --shadow-overlay: var(--shadow-lg-internal);
423
437
 
@@ -601,6 +615,11 @@
601
615
  --color-surface-border-card: color-mix(in oklch, var(--neutral-6) 45%, transparent);
602
616
  --color-backdrop: oklch(0 0 0 / 0.6);
603
617
 
618
+ /* Segmented control — inverted elevation in dark: track is a faint LIGHTER
619
+ fill (can't go darker than a near-black page), thumb steps lighter still. */
620
+ --color-segment-track: color-mix(in oklch, oklch(1 0 0) 7%, transparent);
621
+ --color-segment-thumb: var(--neutral-3);
622
+
604
623
  /* Overlay */
605
624
  --color-overlay: oklch(0 0 0 / 0.70);
606
625
 
@@ -340,3 +340,9 @@
340
340
  ─────────────────────────────────────────────────────────────────── */
341
341
 
342
342
  @utility border-card { border-color: var(--color-surface-border-card); }
343
+ /* Stronger card edge (kbd caps, code blocks, skeleton/panel outlines, chip
344
+ borders) — one step up from the faint `border-card` mix. Dark-mode aware via
345
+ the semantic token. Was referenced in ~11 components with no matching utility
346
+ (dead class → border fell back to currentColor); defined here to restore the
347
+ intended hairline. */
348
+ @utility border-card-strong { border-color: var(--color-surface-border); }