@timbal-ai/timbal-react 1.6.0 → 1.7.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 (32) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/README.md +8 -0
  3. package/dist/app.cjs +167 -75
  4. package/dist/app.d.cts +3 -3
  5. package/dist/app.d.ts +3 -3
  6. package/dist/app.esm.js +5 -5
  7. package/dist/{chart-artifact-VAqgH-My.d.cts → chart-artifact-CuTiCITz.d.cts} +230 -15
  8. package/dist/{chart-artifact-C2pZQsaP.d.ts → chart-artifact-U-x0UNJm.d.ts} +230 -15
  9. package/dist/chat.esm.js +3 -3
  10. package/dist/{chunk-YYEI6XME.esm.js → chunk-22KWC2LS.esm.js} +5 -5
  11. package/dist/{chunk-MBS7XHV2.esm.js → chunk-45NXD3IG.esm.js} +3 -3
  12. package/dist/{chunk-24B4I4XC.esm.js → chunk-64RHAJVG.esm.js} +1 -1
  13. package/dist/{chunk-WQIQW7EM.esm.js → chunk-7AGIAQE6.esm.js} +1 -1
  14. package/dist/{chunk-NO5AWNWT.esm.js → chunk-7WU3IKAN.esm.js} +1 -1
  15. package/dist/{chunk-6SQMTBPL.esm.js → chunk-M5IBJBEY.esm.js} +109 -23
  16. package/dist/{chunk-HSL36SJ4.esm.js → chunk-PMMI7LBV.esm.js} +20 -8
  17. package/dist/{chunk-TMP7RIA7.esm.js → chunk-VKXOHVDE.esm.js} +2 -2
  18. package/dist/{chunk-ELEY66OH.esm.js → chunk-XOCOZU7J.esm.js} +11 -1
  19. package/dist/cli/timbal-ui-lint.mjs +534 -0
  20. package/dist/index.cjs +303 -200
  21. package/dist/index.d.cts +2 -2
  22. package/dist/index.d.ts +2 -2
  23. package/dist/index.esm.js +9 -9
  24. package/dist/{kanban-FFBeaZPS.d.cts → kanban-BQxWliCS.d.cts} +17 -0
  25. package/dist/{kanban-FFBeaZPS.d.ts → kanban-BQxWliCS.d.ts} +17 -0
  26. package/dist/studio.cjs +104 -85
  27. package/dist/studio.esm.js +6 -6
  28. package/dist/ui.cjs +6 -6
  29. package/dist/ui.d.cts +1 -1
  30. package/dist/ui.d.ts +1 -1
  31. package/dist/ui.esm.js +4 -4
  32. package/package.json +13 -3
@@ -2,9 +2,11 @@ import {
2
2
  SIDEBAR_INSET_PX_EXPANDED,
3
3
  STORAGE_KEYS,
4
4
  ShellInsetProvider,
5
+ ShellNavProvider,
5
6
  studioChromeShellStyle,
6
- studioSidebarWidthTransition
7
- } from "./chunk-ELEY66OH.esm.js";
7
+ studioSidebarWidthTransition,
8
+ useOptionalShellNav
9
+ } from "./chunk-XOCOZU7J.esm.js";
8
10
  import {
9
11
  ChartArtifactView,
10
12
  LineAreaChart,
@@ -16,7 +18,7 @@ import {
16
18
  studioIntegrationCardClass,
17
19
  studioTopbarPillHeightClass,
18
20
  toNum
19
- } from "./chunk-WQIQW7EM.esm.js";
21
+ } from "./chunk-7AGIAQE6.esm.js";
20
22
  import {
21
23
  Checkbox,
22
24
  CopyButton,
@@ -29,7 +31,7 @@ import {
29
31
  SelectTrigger,
30
32
  SelectValue,
31
33
  Skeleton
32
- } from "./chunk-NO5AWNWT.esm.js";
34
+ } from "./chunk-7WU3IKAN.esm.js";
33
35
  import {
34
36
  PillSegmentedTabs
35
37
  } from "./chunk-R4RQT2XQ.esm.js";
@@ -45,7 +47,7 @@ import {
45
47
  TIMBAL_V2_SWITCH_TRACK_OFF,
46
48
  TimbalV2Button,
47
49
  controlClass
48
- } from "./chunk-MBS7XHV2.esm.js";
50
+ } from "./chunk-45NXD3IG.esm.js";
49
51
  import {
50
52
  cn
51
53
  } from "./chunk-EDEKQYSU.esm.js";
@@ -178,6 +180,13 @@ var HOUSE_RULES = [
178
180
  slop: `<span className="text-blue-600 bg-green-50">`,
179
181
  good: `<span className="text-primary bg-muted">`
180
182
  },
183
+ {
184
+ id: "chart-token-color",
185
+ rule: "Pass chart and theme color tokens directly (var(--chart-1)) \u2014 never wrap them in hsl(), rgb(), or oklch().",
186
+ why: "The --chart-N and theme tokens are already full OKLCH colors. Wrapping them in hsl()/rgb() is invalid CSS, so the chart renders empty/uncolored \u2014 and the build still passes, so it's a silent runtime bug.",
187
+ slop: `<Cell fill="hsl(var(--chart-1))" />`,
188
+ good: `<Cell fill="var(--chart-1)" />`
189
+ },
181
190
  {
182
191
  id: "no-decorative-icons",
183
192
  rule: "Icons must earn their place (action, nav, or status). Never add an icon beside a label that already says the thing.",
@@ -224,7 +233,11 @@ var HOUSE_RULES = [
224
233
  {
225
234
  id: "compose-from-blocks",
226
235
  rule: "Build from premade blocks (MetricRow, MetricChartCard, DataTable, IntegrationCard). Drop to raw primitives only when no block fits.",
227
- why: "Slop appears the moment generation falls below the curated block layer."
236
+ why: "Slop appears the moment generation falls below the curated block layer.",
237
+ // "Should have used a block" is a judgement about absence, not a textual
238
+ // pattern — no high-precision deterministic check exists, so this stays
239
+ // prompt-only rather than risk false-positives blocking valid UIs.
240
+ enforcement: "prompt-only"
228
241
  },
229
242
  {
230
243
  id: "use-kit-controls",
@@ -305,7 +318,7 @@ The content region is a **padded scroll area** by default \u2014 great for stack
305
318
  - Give the filling child **\`min-h-0 flex-1\`** (or \`h-full\`) so its own scroll/footer resolves \u2014 e.g. \`<TimbalChat className="min-h-0 flex-1" />\`, or a two-pane row where each pane is \`min-h-0 overflow-y-auto\`.
306
319
 
307
320
  \`\`\`tsx
308
- <AppShell contentFill topbar={<div className="flex justify-end p-4"><ModeToggle /></div>}>
321
+ <AppShell contentFill> {/* no global topbar / theme switch */}
309
322
  <Page fill> {/* headerless: omit title */}
310
323
  <TimbalChat workforceId="\u2026" className="min-h-0 flex-1" />
311
324
  </Page>
@@ -409,6 +422,8 @@ The cause of slop is dropping **below** the curated block layer into raw primiti
409
422
 
410
423
  Charts run on **recharts** with shadcn \`ChartContainer\` / \`ChartTooltipContent\` chrome (see \`src/ui/chart.tsx\`). Series colors default to \`--chart-1..6\`; override those CSS tokens to rebrand every chart.
411
424
 
425
+ > **React 19 requirement \u2014 do not hand-roll SVG charts to work around this.** recharts under React 19 crashes (\`Cannot assign to read only property 'lanes'\`, blank route) when \`immer\` resolves to **11.0.0**. The fix is a dependency override in the app's \`package.json\` \u2014 \`"overrides": { "immer": ">=11.0.1" }\` (Yarn: \`"resolutions"\`) \u2014 **not** a code change. Always keep using \`LineAreaChart\` / \`PieChart\` / \`ChartPanel\`; never replace them with raw SVG/CSS charts.
426
+
412
427
  | Component | Use for |
413
428
  |-----------|---------|
414
429
  | \`LineAreaChart\` | Cartesian engine (shadcn-style chrome). Bar fills use theme gradients automatically. Props: \`data\`, \`xKey\`, \`series: [{ dataKey, label?, color? }]\`, \`variant\` (\`area\`\\|\`line\`\\|\`bar\`), \`orientation\` (\`horizontal\` for horizontal bars), \`stacked\`, \`curve\`, \`dots\`, \`gridLines\`, \`tooltipIndicator\`, \`layout\` (\`flush\` \u2014 hides axes by default; category + values on hover tooltip), \`showXAxis\` / \`showYAxis\` to opt back in, \`clipTicks\` (truncates long axis labels when axes are on), \`height\`, \`showLegend\`, \`formatX\`, \`formatValue\`, \`ariaLabel\`. |
@@ -573,6 +588,7 @@ var RAW_COLOR_RE = new RegExp(
573
588
  "g"
574
589
  );
575
590
  var COLOR_LITERAL_RE = /#[0-9a-fA-F]{3,8}\b|\b(?:oklch|rgba?|hsla?)\s*\(/g;
591
+ var COLOR_FN_WRAPPING_VAR_RE = /\b(?:hsl|hsla|rgb|rgba|oklch|oklab|lab|lch|hwb|color)\s*\(\s*var\(\s*--/i;
576
592
  var INLINE_STYLE_COLOR_RE = /style=\{\{[^}]*\b(?:color|background|backgroundColor|borderColor|fill|stroke)\b/;
577
593
  var BOLD_VALUE_RE = /text-(?:xl|2xl|3xl|4xl|5xl|6xl)[^"'`]*\bfont-(?:bold|extrabold|black|semibold)|font-(?:bold|extrabold|black|semibold)[^"'`]*text-(?:xl|2xl|3xl|4xl|5xl|6xl)/;
578
594
  var GRADIENT_RE = /\bbg-(?:gradient|linear|radial|conic)-/;
@@ -589,15 +605,32 @@ var GRADIENT_DIRECTIONS = /* @__PURE__ */ new Set([
589
605
  var ICON_IMPORT_RE = /from\s+["']lucide-react["']/;
590
606
  var RAW_CONTROL_SURFACE_RE = /\bborder-input\b/;
591
607
  var COLORED_HOVER_RE = /\bhover:(?:bg|from|to|via)-(?:primary|destructive|success|warn|danger|blue|emerald|green|amber|red|indigo|violet|purple|pink|rose|sky|cyan|teal|lime|yellow|orange|fuchsia)\b/;
608
+ var TREND_CONTEXT_RE = /\b(?:trend|delta|TrendingUp|TrendingDown|ArrowUp|ArrowDown|ArrowUpRight|ArrowDownRight|MoveUp|MoveDown)\b|[+\-]\d+(?:\.\d+)?\s*%/;
609
+ var TREND_COLOR_RE = /\b(?:text|bg|border)-(?:success|destructive|emerald|green|lime|teal|red|rose|orange|amber)(?:-\d{2,3})?(?:\/\d{1,3})?\b/;
592
610
  var RESERVED_GRADIENT_SET = new Set(RESERVED_GRADIENT_TOKENS);
593
611
  function stripVariants(util) {
594
612
  return util.replace(/^(?:[a-z-]+:)*/, "");
595
613
  }
614
+ function describeArg(value) {
615
+ if (value === null) return "null";
616
+ if (Array.isArray(value)) return "an array";
617
+ const t = typeof value;
618
+ if (t === "object") {
619
+ const keys = Object.keys(value).slice(0, 4);
620
+ return keys.length ? `an object with keys { ${keys.join(", ")} }` : "an object";
621
+ }
622
+ return `a ${t}`;
623
+ }
596
624
  function isCommentOrImport(line) {
597
625
  const t = line.trim();
598
626
  return t.startsWith("//") || t.startsWith("*") || t.startsWith("/*") || t.startsWith("import ") || t.startsWith("export ");
599
627
  }
600
628
  function lintGeneratedUi(source, options = {}) {
629
+ if (typeof source !== "string") {
630
+ throw new TypeError(
631
+ `lintGeneratedUi(source, options?) expects the generated code as a string, but received ${describeArg(source)}. Pass the raw .tsx source \u2014 lintGeneratedUi(code) \u2014 not an object like { filename, source } and not a previous LintResult.`
632
+ );
633
+ }
601
634
  const maxIcons = options.maxIconsPerView ?? SLOP_BUDGETS.maxIconsPerView;
602
635
  const maxRowDividers = options.maxRowDividers ?? SLOP_BUDGETS.maxRowDividers;
603
636
  const findings = [];
@@ -632,6 +665,16 @@ function lintGeneratedUi(source, options = {}) {
632
665
  if (cardMatch) {
633
666
  const isSelfClosing = /\/>/.test(line) && line.indexOf(cardMatch[0]) < line.indexOf("/>");
634
667
  if (!isSelfClosing) {
668
+ if (openCards.length > 0) {
669
+ const parentCard = openCards[openCards.length - 1];
670
+ findings.push({
671
+ rule: "no-card-in-card",
672
+ severity: "warn",
673
+ line: lineNo,
674
+ message: `Card inside card. A <${cardMatch[1]}> is nested inside the <${parentCard.type}> opened on L${parentCard.line}. Double borders/shadows add no information \u2014 group with spacing or a <Section> instead.`,
675
+ snippet: line.trim().slice(0, 120)
676
+ });
677
+ }
635
678
  openCards.push({ type: cardMatch[1], line: lineNo });
636
679
  }
637
680
  }
@@ -667,7 +710,17 @@ function lintGeneratedUi(source, options = {}) {
667
710
  });
668
711
  }
669
712
  }
670
- const literals = line.match(COLOR_LITERAL_RE);
713
+ const wrapsTokenInColorFn = COLOR_FN_WRAPPING_VAR_RE.test(line);
714
+ if (wrapsTokenInColorFn) {
715
+ findings.push({
716
+ rule: "chart-token-color-fn",
717
+ severity: "error",
718
+ line: lineNo,
719
+ message: "Color function wrapping a token (e.g. hsl(var(--chart-1))). The --chart-N and theme tokens are already OKLCH colors \u2014 wrapping them in hsl()/rgb() is invalid CSS and renders an empty/uncolored chart (the build still passes). Pass the token directly: var(--chart-1), or let the app-kit charts use --chart-N automatically.",
720
+ snippet: line.trim().slice(0, 120)
721
+ });
722
+ }
723
+ const literals = wrapsTokenInColorFn ? null : line.match(COLOR_LITERAL_RE);
671
724
  if (literals) {
672
725
  findings.push({
673
726
  rule: "color-literal",
@@ -704,6 +757,15 @@ function lintGeneratedUi(source, options = {}) {
704
757
  snippet: line.trim().slice(0, 120)
705
758
  });
706
759
  }
760
+ if (TREND_CONTEXT_RE.test(line) && TREND_COLOR_RE.test(line)) {
761
+ findings.push({
762
+ rule: "neutral-trend",
763
+ severity: "warn",
764
+ line: lineNo,
765
+ message: "Colored trend indicator. House style: don't tint deltas green/red on every metric \u2014 show a trend only when the change is the point, and keep it muted (text-muted-foreground).",
766
+ snippet: line.trim().slice(0, 120)
767
+ });
768
+ }
707
769
  if (BOLD_VALUE_RE.test(line)) {
708
770
  findings.push({
709
771
  rule: "bold-metric",
@@ -809,6 +871,11 @@ function lintGeneratedUi(source, options = {}) {
809
871
  };
810
872
  }
811
873
  function formatLintReport(findings) {
874
+ if (!Array.isArray(findings)) {
875
+ throw new TypeError(
876
+ `formatLintReport(findings) expects the findings array, but received ${describeArg(findings)}. Pass result.findings \u2014 formatLintReport(lintGeneratedUi(code).findings) \u2014 not the whole LintResult.`
877
+ );
878
+ }
812
879
  if (findings.length === 0) return "";
813
880
  const lines = findings.slice().sort((a, b) => a.line - b.line).map((f) => {
814
881
  const tag = f.severity === "error" ? "ERROR" : "warn ";
@@ -2004,20 +2071,20 @@ function useAppShellChat() {
2004
2071
  }
2005
2072
 
2006
2073
  // src/app/layout/app-shell-nav-context.tsx
2007
- import { createContext as createContext3, useContext as useContext3 } from "react";
2008
- var AppShellNavContext = createContext3(null);
2009
- var AppShellNavProvider = AppShellNavContext.Provider;
2074
+ var AppShellNavProvider = ShellNavProvider;
2075
+ var NAV_NOOP = {
2076
+ open: false,
2077
+ setOpen: () => {
2078
+ },
2079
+ toggle: () => {
2080
+ }
2081
+ };
2010
2082
  function useAppShellNav() {
2011
- return useContext3(AppShellNavContext) ?? {
2012
- open: false,
2013
- setOpen: () => {
2014
- },
2015
- toggle: () => {
2016
- }
2017
- };
2083
+ return useOptionalShellNav() ?? NAV_NOOP;
2018
2084
  }
2019
2085
 
2020
2086
  // src/app/layout/AppShell.tsx
2087
+ import { MenuIcon } from "lucide-react";
2021
2088
  import { motion, useReducedMotion } from "motion/react";
2022
2089
  import { useCallback, useEffect, useMemo, useState as useState2 } from "react";
2023
2090
  import { jsx as jsx7, jsxs as jsxs6 } from "react/jsx-runtime";
@@ -2027,6 +2094,12 @@ var floatingTriggerClass = cn(
2027
2094
  "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background",
2028
2095
  "bottom-6 right-6 max-sm:bottom-4 max-sm:right-4"
2029
2096
  );
2097
+ var floatingNavTriggerClass = cn(
2098
+ "aui-app-shell-nav-trigger-fixed fixed left-4 top-4 z-30 inline-flex size-10 items-center justify-center rounded-xl md:hidden",
2099
+ "border border-border/60 bg-card/85 text-foreground shadow-card-elevated backdrop-blur-xl supports-backdrop-filter:bg-card/75",
2100
+ "transition-colors hover:bg-card",
2101
+ "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background"
2102
+ );
2030
2103
  var floatingPanelClass = cn(
2031
2104
  "aui-app-shell-chat-float fixed z-50 flex flex-col overflow-hidden rounded-2xl border border-border/60 shadow-card-elevated",
2032
2105
  "bg-card/85 backdrop-blur-xl supports-backdrop-filter:bg-card/75",
@@ -2117,12 +2190,14 @@ var AppShell = ({
2117
2190
  navOpen: navOpenProp,
2118
2191
  defaultNavOpen = false,
2119
2192
  onNavOpenChange,
2193
+ mobileSidebarTrigger = "auto",
2120
2194
  className,
2121
2195
  mainClassName,
2122
2196
  contentFill = false
2123
2197
  }) => {
2124
2198
  const topbarContent = topbar ?? header;
2125
2199
  const hasChat = Boolean(chat);
2200
+ const showFloatingNavTrigger = Boolean(sidebar) && mobileSidebarTrigger !== "none" && !(mobileSidebarTrigger === "topbar") && !topbarContent;
2126
2201
  const [uncontrolledNavOpen, setUncontrolledNavOpen] = useState2(defaultNavOpen);
2127
2202
  const isNavControlled = navOpenProp !== void 0;
2128
2203
  const navOpen = isNavControlled ? navOpenProp : uncontrolledNavOpen;
@@ -2182,6 +2257,17 @@ var AppShell = ({
2182
2257
  style: studioChromeShellStyle,
2183
2258
  children: [
2184
2259
  sidebar,
2260
+ showFloatingNavTrigger && !navOpen ? /* @__PURE__ */ jsx7(
2261
+ "button",
2262
+ {
2263
+ type: "button",
2264
+ "aria-label": "Open navigation",
2265
+ "aria-expanded": false,
2266
+ onClick: () => setNavOpen(true),
2267
+ className: floatingNavTriggerClass,
2268
+ children: /* @__PURE__ */ jsx7(MenuIcon, { className: "size-5", "aria-hidden": true })
2269
+ }
2270
+ ) : null,
2185
2271
  sidebar && navOpen ? /* @__PURE__ */ jsx7(
2186
2272
  "button",
2187
2273
  {
@@ -2266,7 +2352,7 @@ var AppShellChatTrigger = ({
2266
2352
  };
2267
2353
 
2268
2354
  // src/app/layout/AppShellSidebarTrigger.tsx
2269
- import { MenuIcon } from "lucide-react";
2355
+ import { MenuIcon as MenuIcon2 } from "lucide-react";
2270
2356
  import { jsx as jsx9 } from "react/jsx-runtime";
2271
2357
  var AppShellSidebarTrigger = ({
2272
2358
  label = "Open navigation",
@@ -2285,7 +2371,7 @@ var AppShellSidebarTrigger = ({
2285
2371
  "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-foreground/10",
2286
2372
  className
2287
2373
  ),
2288
- children: /* @__PURE__ */ jsx9(MenuIcon, { className: "size-5", "aria-hidden": true })
2374
+ children: /* @__PURE__ */ jsx9(MenuIcon2, { className: "size-5", "aria-hidden": true })
2289
2375
  }
2290
2376
  );
2291
2377
  };
@@ -2423,9 +2509,9 @@ var Stack = ({
2423
2509
  );
2424
2510
 
2425
2511
  // src/app/copilot/app-copilot-context.tsx
2426
- import { createContext as createContext4, useContext as useContext4 } from "react";
2512
+ import { createContext as createContext3, useContext as useContext3 } from "react";
2427
2513
  import { jsx as jsx14 } from "react/jsx-runtime";
2428
- var AppCopilotContext = createContext4(null);
2514
+ var AppCopilotContext = createContext3(null);
2429
2515
  var AppCopilotProvider = ({
2430
2516
  value,
2431
2517
  children
@@ -2433,7 +2519,7 @@ var AppCopilotProvider = ({
2433
2519
  return /* @__PURE__ */ jsx14(AppCopilotContext.Provider, { value, children });
2434
2520
  };
2435
2521
  function useAppCopilotContext() {
2436
- return useContext4(AppCopilotContext) ?? {};
2522
+ return useContext3(AppCopilotContext) ?? {};
2437
2523
  }
2438
2524
 
2439
2525
  // src/app/chat/AppChatPanel.tsx
@@ -16,11 +16,12 @@ import {
16
16
  studioSidebarEntriesTransition,
17
17
  studioSidebarEntryItemVariants,
18
18
  studioSidebarWidthTransition,
19
+ useOptionalShellNav,
19
20
  useShellInsetReporter
20
- } from "./chunk-ELEY66OH.esm.js";
21
+ } from "./chunk-XOCOZU7J.esm.js";
21
22
  import {
22
23
  WorkforceSelector
23
- } from "./chunk-TMP7RIA7.esm.js";
24
+ } from "./chunk-VKXOHVDE.esm.js";
24
25
  import {
25
26
  Composer,
26
27
  TimbalChat,
@@ -43,7 +44,7 @@ import {
43
44
  studioTopbarIconPillClass,
44
45
  studioTopbarPillHeightClass,
45
46
  useTimbalRuntime
46
- } from "./chunk-WQIQW7EM.esm.js";
47
+ } from "./chunk-7AGIAQE6.esm.js";
47
48
  import {
48
49
  DropdownMenu,
49
50
  DropdownMenuContent,
@@ -51,7 +52,7 @@ import {
51
52
  DropdownMenuLabel,
52
53
  DropdownMenuSeparator,
53
54
  DropdownMenuTrigger
54
- } from "./chunk-24B4I4XC.esm.js";
55
+ } from "./chunk-64RHAJVG.esm.js";
55
56
  import {
56
57
  PillSegmentedTabs
57
58
  } from "./chunk-R4RQT2XQ.esm.js";
@@ -63,7 +64,7 @@ import {
63
64
  Tooltip,
64
65
  TooltipContent,
65
66
  TooltipTrigger
66
- } from "./chunk-MBS7XHV2.esm.js";
67
+ } from "./chunk-45NXD3IG.esm.js";
67
68
  import {
68
69
  cn
69
70
  } from "./chunk-EDEKQYSU.esm.js";
@@ -1119,14 +1120,25 @@ var StudioSidebar = ({
1119
1120
  window.addEventListener("resize", onResize);
1120
1121
  return () => window.removeEventListener("resize", onResize);
1121
1122
  }, [mobileBreakpointPx]);
1123
+ const shellNav = useOptionalShellNav();
1124
+ const isControlled = mobileOpenProp !== void 0;
1125
+ const usingShellNav = !isControlled && shellNav !== null;
1122
1126
  const [internalMobileOpen, setInternalMobileOpen] = useState5(false);
1123
- const mobileOpen = mobileOpenProp ?? internalMobileOpen;
1127
+ const mobileOpen = isControlled ? mobileOpenProp : usingShellNav ? shellNav.open : internalMobileOpen;
1124
1128
  const setMobileOpen = useCallback3(
1125
1129
  (next) => {
1126
- if (mobileOpenProp === void 0) setInternalMobileOpen(next);
1130
+ if (isControlled) {
1131
+ onMobileOpenChangeProp?.(next);
1132
+ return;
1133
+ }
1134
+ if (usingShellNav) {
1135
+ shellNav.setOpen(next);
1136
+ } else {
1137
+ setInternalMobileOpen(next);
1138
+ }
1127
1139
  onMobileOpenChangeProp?.(next);
1128
1140
  },
1129
- [mobileOpenProp, onMobileOpenChangeProp]
1141
+ [isControlled, usingShellNav, shellNav, onMobileOpenChangeProp]
1130
1142
  );
1131
1143
  const handleSelect = useCallback3(
1132
1144
  (id) => {
@@ -1,9 +1,9 @@
1
1
  import {
2
2
  studioTopbarPillHeightClass
3
- } from "./chunk-WQIQW7EM.esm.js";
3
+ } from "./chunk-7AGIAQE6.esm.js";
4
4
  import {
5
5
  controlSurfaceClass
6
- } from "./chunk-MBS7XHV2.esm.js";
6
+ } from "./chunk-45NXD3IG.esm.js";
7
7
  import {
8
8
  cn
9
9
  } from "./chunk-EDEKQYSU.esm.js";
@@ -119,6 +119,14 @@ function useShellInsetReporter() {
119
119
  return useContext(ShellInsetContext);
120
120
  }
121
121
 
122
+ // src/layout/shell-nav-context.tsx
123
+ import { createContext as createContext2, useContext as useContext2 } from "react";
124
+ var ShellNavContext = createContext2(null);
125
+ var ShellNavProvider = ShellNavContext.Provider;
126
+ function useOptionalShellNav() {
127
+ return useContext2(ShellNavContext);
128
+ }
129
+
122
130
  export {
123
131
  SIDEBAR_WIDTH_PX,
124
132
  SIDEBAR_WIDTH_COLLAPSED_PX,
@@ -138,5 +146,7 @@ export {
138
146
  studioSidebarDrawerTransition,
139
147
  studioSidebarBackdropTransition,
140
148
  ShellInsetProvider,
141
- useShellInsetReporter
149
+ useShellInsetReporter,
150
+ ShellNavProvider,
151
+ useOptionalShellNav
142
152
  };