@timbal-ai/timbal-react 1.3.0 → 1.4.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.
@@ -426,6 +426,15 @@ interface AppShellProps {
426
426
  onNavOpenChange?: (open: boolean) => void;
427
427
  className?: string;
428
428
  mainClassName?: string;
429
+ /**
430
+ * Make the content region a bounded, non-scrolling flex column instead of the
431
+ * default padded scroll area. Use for full-bleed pages that own their own
432
+ * scroll — a full-page chat (`TimbalChat` / `Thread`), a canvas, a map, an
433
+ * editor — so a `h-full` / `flex-1 min-h-0` child fills exactly and a pinned
434
+ * footer (e.g. the chat composer) stays put instead of riding down on scroll.
435
+ * Do **not** combine with `h-[calc(100dvh-…)]` guesses on the child.
436
+ */
437
+ contentFill?: boolean;
429
438
  }
430
439
  /**
431
440
  * App-first layout: sidebar + topbar + main, with optional **floating** copilot.
@@ -557,7 +566,8 @@ type AppDensityClassKey = keyof typeof APP_DENSITY_CLASSES;
557
566
  declare function appDensityClass(key: AppDensityClassKey, density?: AppDensity): string;
558
567
 
559
568
  interface PageHeaderProps {
560
- title: ReactNode;
569
+ /** Page title. Omit for a headerless page (no `<h1>` rendered). */
570
+ title?: ReactNode;
561
571
  description?: ReactNode;
562
572
  actions?: ReactNode;
563
573
  className?: string;
@@ -573,6 +583,13 @@ interface PageProps extends PageHeaderProps {
573
583
  * `compact` tightens page insets, section gaps, and card padding.
574
584
  */
575
585
  density?: AppDensity;
586
+ /**
587
+ * Make the page a bounded, full-height flex column instead of a centered,
588
+ * content-sized column. Pair with `AppShell contentFill` for full-bleed pages
589
+ * (a full-page chat, a canvas, an editor) whose body should fill the viewport
590
+ * and own its own scroll. Give the fill child `min-h-0 flex-1`.
591
+ */
592
+ fill?: boolean;
576
593
  className?: string;
577
594
  }
578
595
  declare const Page: FC<PageProps>;
@@ -239,6 +239,40 @@ You are **not** required to copy any example layout, page title, section order,
239
239
 
240
240
  When in doubt: compose from the **component menu** + **guidelines**, then adapt creatively to the request.
241
241
 
242
+ ### Layout archetypes \u2014 pick the shape that fits (don't default to one)
243
+
244
+ The most common failure is shipping the **same** layout every time: sidebar + topbar + \`Page\` + one \`MetricRow\` + one full-width \`DataTable\`. That is *one* archetype, not *the* layout. Choose deliberately \u2014 different domains want different shapes, and varying the shell/page composition is encouraged.
245
+
246
+ | Archetype | When | Compose |
247
+ |-----------|------|---------|
248
+ | **Sidebar dashboard** | Multi-section product (CRM, billing, ops) with nav | \`StudioSidebar\` in \`AppShell.sidebar\` + \`Page\` \u2192 \`Section\` |
249
+ | **Focused / no-chrome** | A single tool or one-screen utility | \`AppShell\` (no sidebar) + \`Page\` (optionally just \`AppShellTopbar\`); or a centered narrow column |
250
+ | **Bento overview** | Home / at-a-glance dashboards | \`Page\` + an **asymmetric grid** of \`SurfaceCard\` / \`ChartPanel\` / \`StatTile\` spanning different widths (not a uniform row + table) |
251
+ | **Split master\u2013detail** | Inbox, triage queue, record browser, log explorer | \`AppShell contentFill\` + \`Page fill\` + a two-column flex row, each pane \`min-h-0 overflow-y-auto\` |
252
+ | **Full-page chat / canvas** | Chat-first app, editor, map, single full-bleed surface | \`AppShell contentFill\` + headerless \`Page fill\` + a \`min-h-0 flex-1\` child (e.g. \`TimbalChat\`) |
253
+ | **Copilot overlay** | A data app that also wants an assistant | any of the above + \`AppShell chat={<AppChatPanel />}\` (floating, never a second column) |
254
+ | **Section-switcher** | One page, several views | \`SubNav\` / \`PillSegmentedTabs\` (\`trackVariant="flush"\`) switching panels with state/router |
255
+
256
+ Mix them: vary the grid columns, density, header placement (\`Page\` actions vs. a global \`AppShellTopbar\`), and whether there's a sidebar at all. Two dashboards for two domains should not look identical.
257
+
258
+ ### Full-height pages (chat, canvas, split views)
259
+
260
+ The content region is a **padded scroll area** by default \u2014 great for stacked \`Page\` \u2192 \`Section\` content, wrong for a surface that must fill the viewport. For full-bleed pages:
261
+
262
+ - Pass **\`contentFill\`** to \`AppShell\` \u2192 the content region becomes a bounded, non-scrolling flex column (clipped, no bottom padding).
263
+ - Pass **\`fill\`** to \`Page\` \u2192 the page becomes a \`min-h-0 flex-1\` flex column.
264
+ - 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\`.
265
+
266
+ \`\`\`tsx
267
+ <AppShell contentFill topbar={<AppShellTopbar actions={<ModeToggle />} />}>
268
+ <Page fill> {/* headerless: omit title */}
269
+ <TimbalChat workforceId="\u2026" className="min-h-0 flex-1" />
270
+ </Page>
271
+ </AppShell>
272
+ \`\`\`
273
+
274
+ **Don't** size full-height content with \`h-[calc(100dvh-\u2026)]\` (guesses chrome height \u2192 spurious scrollbar) or \`min-h-[\u2026]\` (free-growing floor \u2192 a pinned footer like the chat composer rides down on scroll). Let \`contentFill\` + \`fill\` provide the bounded height. \`Page\` with no \`title\` renders **no header** \u2014 you don't need to abandon \`Page\` to drop a heading.
275
+
242
276
  ### Module layout (source folders)
243
277
 
244
278
  Presentational groups \u2014 import from the package root, not from these paths:
@@ -297,12 +331,12 @@ The cause of slop is dropping **below** the curated block layer into raw primiti
297
331
 
298
332
  | Component | Use for |
299
333
  |-----------|---------|
300
- | \`AppShell\` | Shell: optional \`sidebar\`, \`topbar\`, main \`children\`, optional floating \`chat\`. Props: \`chatTriggerLabel\`, \`chatCollapsible\`, \`chatWidth\`, \`chatHeight\`, controlled \`chatOpen\`. |
334
+ | \`AppShell\` | Shell: optional \`sidebar\`, \`topbar\`, main \`children\`, optional floating \`chat\`. Props: \`chatTriggerLabel\`, \`chatCollapsible\`, \`chatWidth\`, \`chatHeight\`, controlled \`chatOpen\`, **\`contentFill\`** (bounded non-scrolling content region for full-bleed pages \u2014 chat/canvas/split view). |
301
335
  | \`AppShellTopbar\` | Full-width top bar: \`start\`, \`actions\` slots. |
302
336
  | \`AppCopilotProvider\` | React context for copilot-aware tools (page, filters, selection, etc.). |
303
337
  | \`AppChatPanel\` | Floating thread: \`workforceId\`, \`welcome\`, \`debug\`. |
304
338
  | \`useAppShellChat\` | Custom open/close trigger when \`hideChatTrigger\` on shell. |
305
- | \`Page\` | Page title, description, \`breadcrumbs\`, \`actions\`, \`density\` (\`"default"\` | \`"compact"\`), children. |
339
+ | \`Page\` | Page title, description, \`breadcrumbs\`, \`actions\`, \`density\` (\`"default"\` | \`"compact"\`), children. **\`title\` is optional** \u2014 omit it for a headerless page (no \`<h1>\`). **\`fill\`** makes it a \`min-h-0 flex-1\` column for full-height content (pair with \`AppShell contentFill\`). |
306
340
  | \`Section\` | Titled block inside a page. Optional \`density\` overrides inherited page density. |
307
341
  | \`SubNav\` | **Section switcher** (Overview / Reports pill bar): \`items\`, \`activeId\`, \`onChange\`. Never use Radix/shadcn \`Tabs\` \u2014 it is not in this package. Switch panels with state or the router. |
308
342
  | **Menus** | **Select** = short list, no search. **Combobox** = searchable (same trigger as Select). **Command** only inside \`PopoverContent variant="list"\` or Combobox \u2014 never padded default Popover. See \`examples/app-kit/src/recipes/primitives-catalog.ts\`. |
@@ -395,8 +429,11 @@ Ready-made **section patterns** assembled from the components above. Each is a c
395
429
  - **Empty states** \u2014 no-data / no-results / first-run. Compose \`EmptyState\` + \`Card\` + \`Button\`.
396
430
  - **Sign-in card** \u2014 centered auth entry. Compose \`Card\` + \`Input\` + \`Label\` + \`Button\`.
397
431
 
398
- **Shells & theming**
432
+ **Shells & layouts**
399
433
  - **Minimal shell** \u2014 \`AppShell\` + \`Page\` (no sidebar/chat).
434
+ - **Bento dashboard** \u2014 \`Page\` + an asymmetric grid of \`SurfaceCard\` / \`ChartPanel\` / \`StatTile\` (varied spans) for overview/home screens.
435
+ - **Split view** \u2014 master\u2013detail: \`AppShell contentFill\` + \`Page fill\` + a two-pane flex row (list + detail), each pane \`min-h-0 overflow-y-auto\`.
436
+ - **Full-page chat** \u2014 \`AppShell contentFill\` + headerless \`Page fill\` + \`TimbalChat className="min-h-0 flex-1"\` (composer pinned; no \`h-[calc(...)]\`).
400
437
  - **Copilot overlay** \u2014 \`AppShell\` + floating \`AppChatPanel\`.
401
438
  - **Theme presets** \u2014 apply a brand preset programmatically (\`applyThemePreset\` / \`applyTimbalTheme\`); never hand-author OKLCH and don't expose a theme picker to end users.
402
439
 
@@ -1600,6 +1637,7 @@ var AppShellBody = ({
1600
1637
  sidebar,
1601
1638
  topbarContent,
1602
1639
  mainClassName,
1640
+ contentFill = false,
1603
1641
  insetPaddingPx,
1604
1642
  insetExpanded,
1605
1643
  children
@@ -1622,7 +1660,10 @@ var AppShellBody = ({
1622
1660
  "div",
1623
1661
  {
1624
1662
  className: cn(
1625
- "aui-app-shell-scroll flex min-h-0 flex-1 flex-col overflow-y-auto",
1663
+ "aui-app-shell-scroll flex min-h-0 flex-1 flex-col",
1664
+ // Padded scroll region by default; a full-bleed page (chat / canvas) owns
1665
+ // its own scroll, so clip here and let the bounded `main` fill exactly.
1666
+ contentFill ? "overflow-hidden" : "overflow-y-auto",
1626
1667
  !topbarContent && appShellInsetTopClass
1627
1668
  ),
1628
1669
  children: [
@@ -1631,8 +1672,11 @@ var AppShellBody = ({
1631
1672
  "main",
1632
1673
  {
1633
1674
  className: cn(
1634
- "aui-app-shell-main min-w-0 flex-1",
1635
- appShellInsetBottomClass,
1675
+ // Bounded flex column by default so `h-full` / `flex-1 min-h-0` children
1676
+ // (full-page chat, canvas) resolve a height without `mainClassName` surgery.
1677
+ "aui-app-shell-main flex min-h-0 min-w-0 flex-1 flex-col",
1678
+ // Bottom breathing room for scrolling content; full-bleed pages skip it.
1679
+ !contentFill && appShellInsetBottomClass,
1636
1680
  mainClassName
1637
1681
  ),
1638
1682
  children
@@ -1662,7 +1706,8 @@ var AppShell = ({
1662
1706
  defaultNavOpen = false,
1663
1707
  onNavOpenChange,
1664
1708
  className,
1665
- mainClassName
1709
+ mainClassName,
1710
+ contentFill = false
1666
1711
  }) => {
1667
1712
  const topbarContent = topbar ?? header;
1668
1713
  const hasChat = Boolean(chat);
@@ -1709,6 +1754,7 @@ var AppShell = ({
1709
1754
  sidebar,
1710
1755
  topbarContent,
1711
1756
  mainClassName,
1757
+ contentFill,
1712
1758
  insetPaddingPx,
1713
1759
  insetExpanded,
1714
1760
  children
@@ -1858,11 +1904,14 @@ var PageHeader = ({
1858
1904
  className
1859
1905
  }) => {
1860
1906
  const pageHeaderClass = useAppDensityClass("pageHeader");
1907
+ if (title == null && description == null && actions == null) {
1908
+ return null;
1909
+ }
1861
1910
  return /* @__PURE__ */ jsxs7("header", { className: cn("aui-app-page-header", pageHeaderClass, className), children: [
1862
- /* @__PURE__ */ jsxs7("div", { className: "min-w-0", children: [
1863
- /* @__PURE__ */ jsx10("h1", { className: "text-2xl font-semibold tracking-tight text-foreground", children: title }),
1911
+ title != null || description != null ? /* @__PURE__ */ jsxs7("div", { className: "min-w-0", children: [
1912
+ title != null ? /* @__PURE__ */ jsx10("h1", { className: "text-2xl font-semibold tracking-tight text-foreground", children: title }) : null,
1864
1913
  description ? /* @__PURE__ */ jsx10("p", { className: "mt-1 text-sm text-muted-foreground", children: description }) : null
1865
- ] }),
1914
+ ] }) : null,
1866
1915
  actions ? /* @__PURE__ */ jsx10("div", { className: "aui-app-page-header-actions flex shrink-0 flex-wrap items-center gap-2", children: actions }) : null
1867
1916
  ] });
1868
1917
  };
@@ -1872,6 +1921,7 @@ import { jsx as jsx11, jsxs as jsxs8 } from "react/jsx-runtime";
1872
1921
  var PageFrame = ({
1873
1922
  children,
1874
1923
  breadcrumbs,
1924
+ fill = false,
1875
1925
  className,
1876
1926
  ...headerProps
1877
1927
  }) => {
@@ -1880,7 +1930,11 @@ var PageFrame = ({
1880
1930
  return /* @__PURE__ */ jsxs8(
1881
1931
  "div",
1882
1932
  {
1883
- className: cn("aui-app-page", pageColumnClass, className),
1933
+ className: cn(
1934
+ "aui-app-page",
1935
+ fill ? "flex min-h-0 min-w-0 flex-1 flex-col" : pageColumnClass,
1936
+ className
1937
+ ),
1884
1938
  "data-density": density,
1885
1939
  children: [
1886
1940
  breadcrumbs,
@@ -1894,10 +1948,20 @@ var Page = ({
1894
1948
  density = "default",
1895
1949
  children,
1896
1950
  breadcrumbs,
1951
+ fill = false,
1897
1952
  className,
1898
1953
  ...headerProps
1899
1954
  }) => {
1900
- return /* @__PURE__ */ jsx11(AppDensityProvider, { density, children: /* @__PURE__ */ jsx11(PageFrame, { breadcrumbs, className, ...headerProps, children }) });
1955
+ return /* @__PURE__ */ jsx11(AppDensityProvider, { density, children: /* @__PURE__ */ jsx11(
1956
+ PageFrame,
1957
+ {
1958
+ breadcrumbs,
1959
+ fill,
1960
+ className,
1961
+ ...headerProps,
1962
+ children
1963
+ }
1964
+ ) });
1901
1965
  };
1902
1966
 
1903
1967
  // src/app/layout/Section.tsx
@@ -2810,6 +2874,7 @@ var Breadcrumbs = ({ items, className }) => {
2810
2874
  };
2811
2875
 
2812
2876
  // src/app/forms/Field.tsx
2877
+ import { useId as useId4 } from "react";
2813
2878
  import { jsx as jsx36, jsxs as jsxs28 } from "react/jsx-runtime";
2814
2879
  var Field = ({
2815
2880
  label,
@@ -2835,7 +2900,8 @@ var FieldInput = ({
2835
2900
  id,
2836
2901
  ...inputProps
2837
2902
  }) => {
2838
- const inputId = id ?? inputProps.name;
2903
+ const autoId = useId4();
2904
+ const inputId = id ?? inputProps.name ?? autoId;
2839
2905
  return /* @__PURE__ */ jsx36(
2840
2906
  Field,
2841
2907
  {
@@ -2858,6 +2924,7 @@ var FieldInput = ({
2858
2924
  };
2859
2925
 
2860
2926
  // src/app/forms/FieldTextarea.tsx
2927
+ import { useId as useId5 } from "react";
2861
2928
  import { jsx as jsx37 } from "react/jsx-runtime";
2862
2929
  var textareaClass = cn(
2863
2930
  appInputClass,
@@ -2872,7 +2939,8 @@ var FieldTextarea = ({
2872
2939
  id,
2873
2940
  ...props
2874
2941
  }) => {
2875
- const textareaId = id ?? props.name;
2942
+ const autoId = useId5();
2943
+ const textareaId = id ?? props.name ?? autoId;
2876
2944
  return /* @__PURE__ */ jsx37(
2877
2945
  Field,
2878
2946
  {
@@ -2895,6 +2963,7 @@ var FieldTextarea = ({
2895
2963
  };
2896
2964
 
2897
2965
  // src/app/forms/FieldSelect.tsx
2966
+ import { useId as useId6 } from "react";
2898
2967
  import { ChevronDownIcon } from "lucide-react";
2899
2968
  import { jsx as jsx38, jsxs as jsxs29 } from "react/jsx-runtime";
2900
2969
  var selectWrapClass = "relative";
@@ -2912,7 +2981,8 @@ var FieldSelect = ({
2912
2981
  id,
2913
2982
  ...props
2914
2983
  }) => {
2915
- const selectId = id ?? props.name;
2984
+ const autoId = useId6();
2985
+ const selectId = id ?? props.name ?? autoId;
2916
2986
  return /* @__PURE__ */ jsx38(
2917
2987
  Field,
2918
2988
  {
@@ -2945,6 +3015,7 @@ var FieldSelect = ({
2945
3015
  };
2946
3016
 
2947
3017
  // src/app/forms/FieldSwitch.tsx
3018
+ import { useId as useId7 } from "react";
2948
3019
  import { jsx as jsx39, jsxs as jsxs30 } from "react/jsx-runtime";
2949
3020
  var trackClass = cn(
2950
3021
  "relative inline-flex h-5 w-9 shrink-0 items-center rounded-full transition-[background,box-shadow,border-color] duration-200",
@@ -2965,7 +3036,8 @@ var FieldSwitch = ({
2965
3036
  id,
2966
3037
  ...props
2967
3038
  }) => {
2968
- const inputId = id ?? props.name ?? "switch";
3039
+ const autoId = useId7();
3040
+ const inputId = id ?? props.name ?? autoId;
2969
3041
  return /* @__PURE__ */ jsxs30(
2970
3042
  "label",
2971
3043
  {
@@ -3430,7 +3502,7 @@ function DataTable({
3430
3502
  }
3431
3503
 
3432
3504
  // src/app/data/ChartPanel.tsx
3433
- import { useId as useId4 } from "react";
3505
+ import { useId as useId8 } from "react";
3434
3506
  import { jsx as jsx45, jsxs as jsxs35 } from "react/jsx-runtime";
3435
3507
  var ChartPanel = ({
3436
3508
  title,
@@ -3446,7 +3518,7 @@ var ChartPanel = ({
3446
3518
  const height = heightProp ?? APP_DENSITY_CHART_HEIGHT[density];
3447
3519
  const metricChartPlotRegionClass = useAppDensityClass("metricChartPlotRegion");
3448
3520
  const chartPanelBodyClass = useAppDensityClass("chartPanelBody");
3449
- const titleId = useId4();
3521
+ const titleId = useId8();
3450
3522
  const resolvedTitle = title ?? artifact?.title;
3451
3523
  const hasHeader = Boolean(resolvedTitle || description || actions);
3452
3524
  const body = loading ? /* @__PURE__ */ jsx45(Skeleton, { className: "w-full rounded-lg", style: { height }, "aria-hidden": true }) : children ?? (artifact ? /* @__PURE__ */ jsx45(ChartArtifactView, { artifact, embedded: true, height }) : null);
@@ -3489,7 +3561,7 @@ var ChartPanel = ({
3489
3561
  };
3490
3562
 
3491
3563
  // src/app/data/MetricRow.tsx
3492
- import { useId as useId5, useState as useState5 } from "react";
3564
+ import { useId as useId9, useState as useState5 } from "react";
3493
3565
  import { jsx as jsx46, jsxs as jsxs36 } from "react/jsx-runtime";
3494
3566
  var MetricRow = ({
3495
3567
  title,
@@ -3504,7 +3576,7 @@ var MetricRow = ({
3504
3576
  className
3505
3577
  }) => {
3506
3578
  const metricTileClass = useAppDensityClass("metricTile");
3507
- const titleId = useId5();
3579
+ const titleId = useId9();
3508
3580
  const selectable = onMetricChange != null || activeMetricId != null;
3509
3581
  const [internalId, setInternalId] = useState5(
3510
3582
  defaultActiveMetricId ?? metrics[0]?.id
@@ -3573,7 +3645,7 @@ var MetricRow = ({
3573
3645
  };
3574
3646
 
3575
3647
  // src/app/data/MetricChartCard.tsx
3576
- import { useId as useId6, useState as useState6 } from "react";
3648
+ import { useId as useId10, useState as useState6 } from "react";
3577
3649
  import { jsx as jsx47, jsxs as jsxs37 } from "react/jsx-runtime";
3578
3650
  var MetricChartCard = ({
3579
3651
  title,
@@ -3597,7 +3669,7 @@ var MetricChartCard = ({
3597
3669
  const height = heightProp ?? APP_DENSITY_CHART_HEIGHT[density];
3598
3670
  const metricChartRegionClass = useAppDensityClass("metricChartRegion");
3599
3671
  const metricTileClass = useAppDensityClass("metricTile");
3600
- const titleId = useId6();
3672
+ const titleId = useId10();
3601
3673
  const [internalId, setInternalId] = useState6(
3602
3674
  defaultActiveMetricId ?? metrics[0]?.id
3603
3675
  );
@@ -3794,7 +3866,7 @@ function useLiveQuery(fetcher, options = {}) {
3794
3866
  }
3795
3867
 
3796
3868
  // src/charts/sparkline.tsx
3797
- import { useId as useId7 } from "react";
3869
+ import { useId as useId11 } from "react";
3798
3870
  import { Fragment as Fragment6, jsx as jsx48, jsxs as jsxs38 } from "react/jsx-runtime";
3799
3871
  var Sparkline = ({
3800
3872
  data,
@@ -3807,7 +3879,7 @@ var Sparkline = ({
3807
3879
  className,
3808
3880
  ariaLabel = "Trend"
3809
3881
  }) => {
3810
- const uid = useId7();
3882
+ const uid = useId11();
3811
3883
  const values = data.map((d) => typeof d === "number" ? d : toNum(d[dataKey]));
3812
3884
  if (values.length === 0) {
3813
3885
  return /* @__PURE__ */ jsx48("span", { className: cn("inline-block", className), style: { width, height } });