@flanksource/clicky-ui 0.2.18 → 0.2.19

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 (50) hide show
  1. package/dist/components.cjs +6 -0
  2. package/dist/components.cjs.map +1 -1
  3. package/dist/components.d.ts +3 -0
  4. package/dist/components.d.ts.map +1 -1
  5. package/dist/components.js +6 -0
  6. package/dist/components.js.map +1 -1
  7. package/dist/data/TabButton.cjs +17 -2
  8. package/dist/data/TabButton.cjs.map +1 -1
  9. package/dist/data/TabButton.d.ts +5 -1
  10. package/dist/data/TabButton.d.ts.map +1 -1
  11. package/dist/data/TabButton.js +17 -2
  12. package/dist/data/TabButton.js.map +1 -1
  13. package/dist/data/version-info.cjs +3 -3
  14. package/dist/data/version-info.js +3 -3
  15. package/dist/data.d.ts +1 -1
  16. package/dist/data.d.ts.map +1 -1
  17. package/dist/index.cjs +6 -0
  18. package/dist/index.cjs.map +1 -1
  19. package/dist/index.js +6 -0
  20. package/dist/index.js.map +1 -1
  21. package/dist/layout/AppShell.cjs +182 -0
  22. package/dist/layout/AppShell.cjs.map +1 -0
  23. package/dist/layout/AppShell.d.ts +75 -0
  24. package/dist/layout/AppShell.d.ts.map +1 -0
  25. package/dist/layout/AppShell.js +182 -0
  26. package/dist/layout/AppShell.js.map +1 -0
  27. package/dist/layout/Panel.cjs +66 -0
  28. package/dist/layout/Panel.cjs.map +1 -0
  29. package/dist/layout/Panel.d.ts +34 -0
  30. package/dist/layout/Panel.d.ts.map +1 -0
  31. package/dist/layout/Panel.js +66 -0
  32. package/dist/layout/Panel.js.map +1 -0
  33. package/dist/layout/Tabs.cjs +36 -0
  34. package/dist/layout/Tabs.cjs.map +1 -0
  35. package/dist/layout/Tabs.d.ts +36 -0
  36. package/dist/layout/Tabs.d.ts.map +1 -0
  37. package/dist/layout/Tabs.js +36 -0
  38. package/dist/layout/Tabs.js.map +1 -0
  39. package/dist/styles.cjs +1 -0
  40. package/dist/styles.cjs.map +1 -1
  41. package/dist/styles.css +1 -1
  42. package/dist/styles.d.ts.map +1 -1
  43. package/dist/styles.js +1 -0
  44. package/dist/styles.js.map +1 -1
  45. package/dist/tailwind-preset.cjs +36 -19
  46. package/dist/tailwind-preset.cjs.map +1 -1
  47. package/dist/tailwind-preset.d.ts.map +1 -1
  48. package/dist/tailwind-preset.js +36 -19
  49. package/dist/tailwind-preset.js.map +1 -1
  50. package/package.json +1 -1
@@ -0,0 +1,75 @@
1
+ import { ReactNode } from 'react';
2
+ import { StaticIconComponent } from '../data/Icon';
3
+ export type AppShellNavItem = {
4
+ /** Stable key; also the React key. */
5
+ key: string;
6
+ /** Visible label (hidden when the rail is collapsed). */
7
+ label: ReactNode;
8
+ /** Leading icon (icon name or component). */
9
+ icon?: string | StaticIconComponent;
10
+ /** Highlights the item as the current location. */
11
+ active?: boolean;
12
+ /** Click handler (rendered as a button when no `href`). */
13
+ onClick?: () => void;
14
+ /** Destination (rendered as an anchor). */
15
+ href?: string;
16
+ /** Opens `href` in a new tab. */
17
+ external?: boolean;
18
+ /** Trailing adornment (count, status dot). */
19
+ badge?: ReactNode;
20
+ };
21
+ export type AppShellNavSection = {
22
+ /** Section heading; collapses to a divider when the rail is collapsed. */
23
+ label?: string;
24
+ items: AppShellNavItem[];
25
+ };
26
+ export type AppShellProps = {
27
+ /** Brand mark / wordmark. Shown in the rail header when a rail is present, else in the top bar. */
28
+ brand?: ReactNode;
29
+ /** Inline primary nav (e.g. tabs) shown after the brand in the top bar. */
30
+ nav?: ReactNode;
31
+ /** Centered search slot; grows to fill and is width-capped. */
32
+ search?: ReactNode;
33
+ /** Right-aligned cluster (icon buttons, settings/org picker, …). */
34
+ actions?: ReactNode;
35
+ /** Optional second top-bar row for filters / bulk actions. */
36
+ toolbar?: ReactNode;
37
+ /** Max width of the centered search slot. Defaults to 28rem. */
38
+ searchMaxWidth?: string | number;
39
+ /** Declarative nav sections rendered into the rail (collapse-aware). */
40
+ navSections?: AppShellNavSection[];
41
+ /** Custom sidebar content; receives the collapsed flag. Overrides navSections. */
42
+ sidebar?: ReactNode | ((collapsed: boolean) => ReactNode);
43
+ /** Pinned below the rail header (e.g. a context switcher). */
44
+ sidebarHeader?: ReactNode;
45
+ /** Pinned to the bottom of the rail (version, account). */
46
+ sidebarFooter?: ReactNode;
47
+ /** Show the collapse toggle. Defaults to true when a rail is present. */
48
+ collapsible?: boolean;
49
+ /** Initial collapsed state (uncontrolled). */
50
+ defaultCollapsed?: boolean;
51
+ /** localStorage key persisting the collapsed state. */
52
+ collapsedStorageKey?: string;
53
+ /** Expanded rail width in px. Defaults to 240. */
54
+ sidebarWidth?: number;
55
+ /** Collapsed rail width in px. Defaults to 56. */
56
+ collapsedWidth?: number;
57
+ /** Fixed header row, left side (breadcrumb, title, tabs). */
58
+ bodyHeader?: ReactNode;
59
+ /** Fixed header row, right side (entity actions) — same row as bodyHeader. */
60
+ bodyActions?: ReactNode;
61
+ /** Optional independent-scroll pane (e.g. a tree). Renders a SplitPane vs body-main. */
62
+ bodySidebar?: ReactNode;
63
+ /** bodySidebar width as a percent when present. Defaults to 24. */
64
+ bodySplit?: number;
65
+ /** Main content (body-main); fills the remaining space and scrolls. */
66
+ children: ReactNode;
67
+ className?: string;
68
+ headerClassName?: string;
69
+ toolbarClassName?: string;
70
+ sidebarClassName?: string;
71
+ bodyHeaderClassName?: string;
72
+ contentClassName?: string;
73
+ };
74
+ export declare function AppShell(props: AppShellProps): import("react/jsx-runtime").JSX.Element;
75
+ //# sourceMappingURL=AppShell.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"AppShell.d.ts","sourceRoot":"","sources":["../../src/layout/AppShell.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAuB,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAE5D,OAAO,EAAQ,KAAK,mBAAmB,EAAE,MAAM,cAAc,CAAC;AAY9D,MAAM,MAAM,eAAe,GAAG;IAC5B,sCAAsC;IACtC,GAAG,EAAE,MAAM,CAAC;IACZ,yDAAyD;IACzD,KAAK,EAAE,SAAS,CAAC;IACjB,6CAA6C;IAC7C,IAAI,CAAC,EAAE,MAAM,GAAG,mBAAmB,CAAC;IACpC,mDAAmD;IACnD,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,2DAA2D;IAC3D,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;IACrB,2CAA2C;IAC3C,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,iCAAiC;IACjC,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,8CAA8C;IAC9C,KAAK,CAAC,EAAE,SAAS,CAAC;CACnB,CAAC;AAEF,MAAM,MAAM,kBAAkB,GAAG;IAC/B,0EAA0E;IAC1E,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,eAAe,EAAE,CAAC;CAC1B,CAAC;AAEF,MAAM,MAAM,aAAa,GAAG;IAE1B,mGAAmG;IACnG,KAAK,CAAC,EAAE,SAAS,CAAC;IAClB,2EAA2E;IAC3E,GAAG,CAAC,EAAE,SAAS,CAAC;IAChB,+DAA+D;IAC/D,MAAM,CAAC,EAAE,SAAS,CAAC;IACnB,oEAAoE;IACpE,OAAO,CAAC,EAAE,SAAS,CAAC;IACpB,8DAA8D;IAC9D,OAAO,CAAC,EAAE,SAAS,CAAC;IACpB,gEAAgE;IAChE,cAAc,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IAGjC,wEAAwE;IACxE,WAAW,CAAC,EAAE,kBAAkB,EAAE,CAAC;IACnC,kFAAkF;IAClF,OAAO,CAAC,EAAE,SAAS,GAAG,CAAC,CAAC,SAAS,EAAE,OAAO,KAAK,SAAS,CAAC,CAAC;IAC1D,8DAA8D;IAC9D,aAAa,CAAC,EAAE,SAAS,CAAC;IAC1B,2DAA2D;IAC3D,aAAa,CAAC,EAAE,SAAS,CAAC;IAC1B,yEAAyE;IACzE,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,8CAA8C;IAC9C,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B,uDAAuD;IACvD,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,kDAAkD;IAClD,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,kDAAkD;IAClD,cAAc,CAAC,EAAE,MAAM,CAAC;IAGxB,6DAA6D;IAC7D,UAAU,CAAC,EAAE,SAAS,CAAC;IACvB,8EAA8E;IAC9E,WAAW,CAAC,EAAE,SAAS,CAAC;IACxB,wFAAwF;IACxF,WAAW,CAAC,EAAE,SAAS,CAAC;IACxB,mEAAmE;IACnE,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,uEAAuE;IACvE,QAAQ,EAAE,SAAS,CAAC;IAEpB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B,CAAC;AAOF,wBAAgB,QAAQ,CAAC,KAAK,EAAE,aAAa,2CAwK5C"}
@@ -0,0 +1,182 @@
1
+ import { jsxs, jsx, Fragment } from "react/jsx-runtime";
2
+ import { useState, useEffect } from "react";
3
+ import { cn } from "../lib/utils.js";
4
+ import { Icon } from "../data/Icon.js";
5
+ import { SplitPane } from "./SplitPane.js";
6
+ import { UiSidebar } from "../icons/components/UiSidebar.js";
7
+ function readStored(key) {
8
+ if (!key || typeof window === "undefined") return null;
9
+ return window.localStorage.getItem(key);
10
+ }
11
+ function AppShell(props) {
12
+ const {
13
+ brand,
14
+ nav,
15
+ search,
16
+ actions,
17
+ toolbar,
18
+ searchMaxWidth = "28rem",
19
+ navSections,
20
+ sidebar,
21
+ sidebarHeader,
22
+ sidebarFooter,
23
+ collapsible = true,
24
+ defaultCollapsed = false,
25
+ collapsedStorageKey,
26
+ sidebarWidth = 240,
27
+ collapsedWidth = 56,
28
+ bodyHeader,
29
+ bodyActions,
30
+ bodySidebar,
31
+ bodySplit = 24,
32
+ children,
33
+ className,
34
+ headerClassName,
35
+ toolbarClassName,
36
+ sidebarClassName,
37
+ bodyHeaderClassName,
38
+ contentClassName
39
+ } = props;
40
+ const hasSidebar = sidebar !== void 0 || ((navSections == null ? void 0 : navSections.length) ?? 0) > 0;
41
+ const [collapsed, setCollapsed] = useState(
42
+ () => readStored(collapsedStorageKey) === "true" || readStored(collapsedStorageKey) === null && defaultCollapsed
43
+ );
44
+ useEffect(() => {
45
+ if (collapsedStorageKey && typeof window !== "undefined")
46
+ window.localStorage.setItem(collapsedStorageKey, String(collapsed));
47
+ }, [collapsed, collapsedStorageKey]);
48
+ const railWidth = collapsed ? collapsedWidth : sidebarWidth;
49
+ const hasTopBar = !hasSidebar && brand !== void 0 || nav !== void 0 || search !== void 0 || actions !== void 0 || toolbar !== void 0;
50
+ const hasBodyHeader = bodyHeader !== void 0 || bodyActions !== void 0;
51
+ return /* @__PURE__ */ jsxs("div", { className: cn("flex h-full min-h-0 w-full bg-background", className), children: [
52
+ hasSidebar && /* @__PURE__ */ jsxs(
53
+ "aside",
54
+ {
55
+ style: { width: railWidth },
56
+ className: cn(
57
+ "flex shrink-0 flex-col overflow-hidden border-r border-sidebar-border bg-sidebar text-sidebar-foreground transition-[width] duration-200",
58
+ sidebarClassName
59
+ ),
60
+ children: [
61
+ /* @__PURE__ */ jsxs(
62
+ "div",
63
+ {
64
+ className: cn(
65
+ "flex h-14 shrink-0 items-center border-b border-sidebar-border",
66
+ collapsed ? "justify-center px-2" : "justify-between px-density-3"
67
+ ),
68
+ children: [
69
+ !collapsed && brand && /* @__PURE__ */ jsx("div", { className: "flex min-w-0 items-center gap-2", children: brand }),
70
+ collapsible && /* @__PURE__ */ jsx(
71
+ "button",
72
+ {
73
+ type: "button",
74
+ onClick: () => setCollapsed((c) => !c),
75
+ "aria-label": collapsed ? "Expand sidebar" : "Collapse sidebar",
76
+ title: collapsed ? "Expand sidebar" : "Collapse sidebar",
77
+ className: "flex h-8 w-8 shrink-0 items-center justify-center rounded-md text-sidebar-foreground/70 transition-colors hover:bg-sidebar-accent hover:text-sidebar-accent-foreground",
78
+ children: /* @__PURE__ */ jsx(Icon, { icon: UiSidebar, className: "h-4 w-4" })
79
+ }
80
+ )
81
+ ]
82
+ }
83
+ ),
84
+ sidebarHeader && /* @__PURE__ */ jsx("div", { className: "shrink-0 border-b border-sidebar-border px-density-3 py-density-2", children: sidebarHeader }),
85
+ /* @__PURE__ */ jsx("div", { className: "min-h-0 flex-1 overflow-y-auto py-density-2", children: typeof sidebar === "function" ? sidebar(collapsed) : sidebar !== void 0 ? sidebar : navSections && /* @__PURE__ */ jsx(NavSections, { sections: navSections, collapsed }) }),
86
+ sidebarFooter && /* @__PURE__ */ jsx("div", { className: "mt-auto shrink-0 border-t border-sidebar-border px-density-3 py-density-2", children: sidebarFooter })
87
+ ]
88
+ }
89
+ ),
90
+ /* @__PURE__ */ jsxs("div", { className: "flex min-h-0 min-w-0 flex-1 flex-col", children: [
91
+ hasTopBar && /* @__PURE__ */ jsxs("header", { className: "shrink-0 border-b border-border bg-card", children: [
92
+ /* @__PURE__ */ jsxs(
93
+ "div",
94
+ {
95
+ className: cn("flex h-14 items-center gap-density-3 px-density-4", headerClassName),
96
+ children: [
97
+ !hasSidebar && brand && /* @__PURE__ */ jsx("div", { className: "flex shrink-0 items-center gap-density-2", children: brand }),
98
+ nav && /* @__PURE__ */ jsx("div", { className: "flex shrink-0 items-center", children: nav }),
99
+ search !== void 0 && /* @__PURE__ */ jsx("div", { className: "flex min-w-0 flex-1 justify-center", children: /* @__PURE__ */ jsx("div", { className: "w-full", style: { maxWidth: searchMaxWidth }, children: search }) }),
100
+ search === void 0 && /* @__PURE__ */ jsx("div", { className: "flex-1" }),
101
+ actions && /* @__PURE__ */ jsx("div", { className: "flex shrink-0 items-center gap-density-2", children: actions })
102
+ ]
103
+ }
104
+ ),
105
+ toolbar && /* @__PURE__ */ jsx(
106
+ "div",
107
+ {
108
+ className: cn(
109
+ "flex items-center gap-density-2 border-t border-border bg-muted px-density-4 py-density-2",
110
+ toolbarClassName
111
+ ),
112
+ children: toolbar
113
+ }
114
+ )
115
+ ] }),
116
+ hasBodyHeader && /* @__PURE__ */ jsxs(
117
+ "div",
118
+ {
119
+ className: cn(
120
+ "flex shrink-0 items-start justify-between gap-density-3 border-b border-border bg-card px-density-4 py-density-2",
121
+ bodyHeaderClassName
122
+ ),
123
+ children: [
124
+ /* @__PURE__ */ jsx("div", { className: "min-w-0 flex-1", children: bodyHeader }),
125
+ bodyActions && /* @__PURE__ */ jsx("div", { className: "flex shrink-0 items-center gap-density-2", children: bodyActions })
126
+ ]
127
+ }
128
+ ),
129
+ bodySidebar !== void 0 ? /* @__PURE__ */ jsx(
130
+ SplitPane,
131
+ {
132
+ className: "min-h-0 flex-1",
133
+ defaultSplit: bodySplit,
134
+ minLeft: 12,
135
+ minRight: 30,
136
+ left: bodySidebar,
137
+ right: /* @__PURE__ */ jsx("div", { className: cn("h-full min-w-0", contentClassName), children }),
138
+ rightClass: "overflow-y-auto"
139
+ }
140
+ ) : /* @__PURE__ */ jsx("main", { className: cn("min-h-0 min-w-0 flex-1 overflow-auto", contentClassName), children })
141
+ ] })
142
+ ] });
143
+ }
144
+ function NavSections({
145
+ sections,
146
+ collapsed
147
+ }) {
148
+ return /* @__PURE__ */ jsx("nav", { className: cn("flex flex-col gap-0.5", collapsed ? "px-2" : "px-density-2"), children: sections.map((section, i) => /* @__PURE__ */ jsxs("div", { className: "flex flex-col", children: [
149
+ section.label && (collapsed ? /* @__PURE__ */ jsx("div", { className: "mx-2 mb-1 mt-3 border-t border-sidebar-border first:mt-1" }) : /* @__PURE__ */ jsx("div", { className: "mb-0.5 mt-3 px-density-2 text-[10px] font-semibold uppercase tracking-[0.18em] text-sidebar-foreground/55 first:mt-1", children: section.label })),
150
+ section.items.map((item) => /* @__PURE__ */ jsx(NavItemRow, { item, collapsed }, item.key))
151
+ ] }, section.label ?? `section-${i}`)) });
152
+ }
153
+ function NavItemRow({ item, collapsed }) {
154
+ const className = cn(
155
+ "flex w-full items-center gap-2.5 rounded-md px-density-2 py-1.5 text-left text-[13px] text-sidebar-foreground transition-colors hover:bg-sidebar-accent hover:text-sidebar-accent-foreground",
156
+ collapsed && "justify-center px-0",
157
+ item.active && "bg-sidebar-accent font-medium text-sidebar-primary"
158
+ );
159
+ const inner = /* @__PURE__ */ jsxs(Fragment, { children: [
160
+ item.icon && /* @__PURE__ */ jsx("span", { className: "flex h-4 w-4 shrink-0 items-center justify-center", children: /* @__PURE__ */ jsx(Icon, { ...typeof item.icon === "string" ? { name: item.icon } : { icon: item.icon } }) }),
161
+ !collapsed && /* @__PURE__ */ jsx("span", { className: "flex-1 truncate", children: item.label }),
162
+ !collapsed && item.badge
163
+ ] });
164
+ const title = collapsed && typeof item.label === "string" ? item.label : void 0;
165
+ if (item.href) {
166
+ return /* @__PURE__ */ jsx(
167
+ "a",
168
+ {
169
+ href: item.href,
170
+ className,
171
+ title,
172
+ ...item.external ? { target: "_blank", rel: "noopener noreferrer" } : {},
173
+ children: inner
174
+ }
175
+ );
176
+ }
177
+ return /* @__PURE__ */ jsx("button", { type: "button", onClick: item.onClick, className, title, children: inner });
178
+ }
179
+ export {
180
+ AppShell
181
+ };
182
+ //# sourceMappingURL=AppShell.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"AppShell.js","sources":["../../src/layout/AppShell.tsx"],"sourcesContent":["import { useEffect, useState, type ReactNode } from \"react\";\nimport { cn } from \"../lib/utils\";\nimport { Icon, type StaticIconComponent } from \"../data/Icon\";\nimport { UiSidebar } from \"../icons\";\nimport { SplitPane } from \"./SplitPane\";\n\n// AppShell is a sidebar-first application shell. The full-height DARK nav rail\n// owns the brand + collapse toggle and renders grouped nav sections; the top bar\n// (search, actions) sits to the RIGHT of the rail — there is no full-width top\n// bar when a rail is present. With no rail, the top bar spans full width. The\n// body carries a fixed bodyHeader + bodyActions row over an optional\n// bodySidebar | body-main split (body-main scrolls; bodySidebar — e.g. a tree —\n// scrolls independently via a resizable SplitPane).\n\nexport type AppShellNavItem = {\n /** Stable key; also the React key. */\n key: string;\n /** Visible label (hidden when the rail is collapsed). */\n label: ReactNode;\n /** Leading icon (icon name or component). */\n icon?: string | StaticIconComponent;\n /** Highlights the item as the current location. */\n active?: boolean;\n /** Click handler (rendered as a button when no `href`). */\n onClick?: () => void;\n /** Destination (rendered as an anchor). */\n href?: string;\n /** Opens `href` in a new tab. */\n external?: boolean;\n /** Trailing adornment (count, status dot). */\n badge?: ReactNode;\n};\n\nexport type AppShellNavSection = {\n /** Section heading; collapses to a divider when the rail is collapsed. */\n label?: string;\n items: AppShellNavItem[];\n};\n\nexport type AppShellProps = {\n // ── Top bar (right of the rail; full width when no rail) ─\n /** Brand mark / wordmark. Shown in the rail header when a rail is present, else in the top bar. */\n brand?: ReactNode;\n /** Inline primary nav (e.g. tabs) shown after the brand in the top bar. */\n nav?: ReactNode;\n /** Centered search slot; grows to fill and is width-capped. */\n search?: ReactNode;\n /** Right-aligned cluster (icon buttons, settings/org picker, …). */\n actions?: ReactNode;\n /** Optional second top-bar row for filters / bulk actions. */\n toolbar?: ReactNode;\n /** Max width of the centered search slot. Defaults to 28rem. */\n searchMaxWidth?: string | number;\n\n // ── Left nav rail (dark, collapsible, fixed width) ───────\n /** Declarative nav sections rendered into the rail (collapse-aware). */\n navSections?: AppShellNavSection[];\n /** Custom sidebar content; receives the collapsed flag. Overrides navSections. */\n sidebar?: ReactNode | ((collapsed: boolean) => ReactNode);\n /** Pinned below the rail header (e.g. a context switcher). */\n sidebarHeader?: ReactNode;\n /** Pinned to the bottom of the rail (version, account). */\n sidebarFooter?: ReactNode;\n /** Show the collapse toggle. Defaults to true when a rail is present. */\n collapsible?: boolean;\n /** Initial collapsed state (uncontrolled). */\n defaultCollapsed?: boolean;\n /** localStorage key persisting the collapsed state. */\n collapsedStorageKey?: string;\n /** Expanded rail width in px. Defaults to 240. */\n sidebarWidth?: number;\n /** Collapsed rail width in px. Defaults to 56. */\n collapsedWidth?: number;\n\n // ── Body ─────────────────────────────────────────────────\n /** Fixed header row, left side (breadcrumb, title, tabs). */\n bodyHeader?: ReactNode;\n /** Fixed header row, right side (entity actions) — same row as bodyHeader. */\n bodyActions?: ReactNode;\n /** Optional independent-scroll pane (e.g. a tree). Renders a SplitPane vs body-main. */\n bodySidebar?: ReactNode;\n /** bodySidebar width as a percent when present. Defaults to 24. */\n bodySplit?: number;\n /** Main content (body-main); fills the remaining space and scrolls. */\n children: ReactNode;\n\n className?: string;\n headerClassName?: string;\n toolbarClassName?: string;\n sidebarClassName?: string;\n bodyHeaderClassName?: string;\n contentClassName?: string;\n};\n\nfunction readStored(key: string | undefined): string | null {\n if (!key || typeof window === \"undefined\") return null;\n return window.localStorage.getItem(key);\n}\n\nexport function AppShell(props: AppShellProps) {\n const {\n brand,\n nav,\n search,\n actions,\n toolbar,\n searchMaxWidth = \"28rem\",\n navSections,\n sidebar,\n sidebarHeader,\n sidebarFooter,\n collapsible = true,\n defaultCollapsed = false,\n collapsedStorageKey,\n sidebarWidth = 240,\n collapsedWidth = 56,\n bodyHeader,\n bodyActions,\n bodySidebar,\n bodySplit = 24,\n children,\n className,\n headerClassName,\n toolbarClassName,\n sidebarClassName,\n bodyHeaderClassName,\n contentClassName,\n } = props;\n\n const hasSidebar = sidebar !== undefined || (navSections?.length ?? 0) > 0;\n\n const [collapsed, setCollapsed] = useState(\n () =>\n readStored(collapsedStorageKey) === \"true\" ||\n (readStored(collapsedStorageKey) === null && defaultCollapsed),\n );\n\n useEffect(() => {\n if (collapsedStorageKey && typeof window !== \"undefined\")\n window.localStorage.setItem(collapsedStorageKey, String(collapsed));\n }, [collapsed, collapsedStorageKey]);\n\n const railWidth = collapsed ? collapsedWidth : sidebarWidth;\n const hasTopBar =\n (!hasSidebar && brand !== undefined) ||\n nav !== undefined ||\n search !== undefined ||\n actions !== undefined ||\n toolbar !== undefined;\n const hasBodyHeader = bodyHeader !== undefined || bodyActions !== undefined;\n\n return (\n <div className={cn(\"flex h-full min-h-0 w-full bg-background\", className)}>\n {hasSidebar && (\n <aside\n style={{ width: railWidth }}\n className={cn(\n \"flex shrink-0 flex-col overflow-hidden border-r border-sidebar-border bg-sidebar text-sidebar-foreground transition-[width] duration-200\",\n sidebarClassName,\n )}\n >\n <div\n className={cn(\n \"flex h-14 shrink-0 items-center border-b border-sidebar-border\",\n collapsed ? \"justify-center px-2\" : \"justify-between px-density-3\",\n )}\n >\n {!collapsed && brand && <div className=\"flex min-w-0 items-center gap-2\">{brand}</div>}\n {collapsible && (\n <button\n type=\"button\"\n onClick={() => setCollapsed((c) => !c)}\n aria-label={collapsed ? \"Expand sidebar\" : \"Collapse sidebar\"}\n title={collapsed ? \"Expand sidebar\" : \"Collapse sidebar\"}\n className=\"flex h-8 w-8 shrink-0 items-center justify-center rounded-md text-sidebar-foreground/70 transition-colors hover:bg-sidebar-accent hover:text-sidebar-accent-foreground\"\n >\n <Icon icon={UiSidebar} className=\"h-4 w-4\" />\n </button>\n )}\n </div>\n {sidebarHeader && (\n <div className=\"shrink-0 border-b border-sidebar-border px-density-3 py-density-2\">\n {sidebarHeader}\n </div>\n )}\n <div className=\"min-h-0 flex-1 overflow-y-auto py-density-2\">\n {typeof sidebar === \"function\"\n ? sidebar(collapsed)\n : sidebar !== undefined\n ? sidebar\n : navSections && <NavSections sections={navSections} collapsed={collapsed} />}\n </div>\n {sidebarFooter && (\n <div className=\"mt-auto shrink-0 border-t border-sidebar-border px-density-3 py-density-2\">\n {sidebarFooter}\n </div>\n )}\n </aside>\n )}\n\n <div className=\"flex min-h-0 min-w-0 flex-1 flex-col\">\n {hasTopBar && (\n <header className=\"shrink-0 border-b border-border bg-card\">\n <div\n className={cn(\"flex h-14 items-center gap-density-3 px-density-4\", headerClassName)}\n >\n {!hasSidebar && brand && (\n <div className=\"flex shrink-0 items-center gap-density-2\">{brand}</div>\n )}\n {nav && <div className=\"flex shrink-0 items-center\">{nav}</div>}\n {search !== undefined && (\n <div className=\"flex min-w-0 flex-1 justify-center\">\n <div className=\"w-full\" style={{ maxWidth: searchMaxWidth }}>\n {search}\n </div>\n </div>\n )}\n {search === undefined && <div className=\"flex-1\" />}\n {actions && (\n <div className=\"flex shrink-0 items-center gap-density-2\">{actions}</div>\n )}\n </div>\n {toolbar && (\n <div\n className={cn(\n \"flex items-center gap-density-2 border-t border-border bg-muted px-density-4 py-density-2\",\n toolbarClassName,\n )}\n >\n {toolbar}\n </div>\n )}\n </header>\n )}\n\n {hasBodyHeader && (\n <div\n className={cn(\n \"flex shrink-0 items-start justify-between gap-density-3 border-b border-border bg-card px-density-4 py-density-2\",\n bodyHeaderClassName,\n )}\n >\n <div className=\"min-w-0 flex-1\">{bodyHeader}</div>\n {bodyActions && (\n <div className=\"flex shrink-0 items-center gap-density-2\">{bodyActions}</div>\n )}\n </div>\n )}\n\n {bodySidebar !== undefined ? (\n <SplitPane\n className=\"min-h-0 flex-1\"\n defaultSplit={bodySplit}\n minLeft={12}\n minRight={30}\n left={bodySidebar}\n right={<div className={cn(\"h-full min-w-0\", contentClassName)}>{children}</div>}\n rightClass=\"overflow-y-auto\"\n />\n ) : (\n <main className={cn(\"min-h-0 min-w-0 flex-1 overflow-auto\", contentClassName)}>\n {children}\n </main>\n )}\n </div>\n </div>\n );\n}\n\nfunction NavSections({\n sections,\n collapsed,\n}: {\n sections: AppShellNavSection[];\n collapsed: boolean;\n}) {\n return (\n <nav className={cn(\"flex flex-col gap-0.5\", collapsed ? \"px-2\" : \"px-density-2\")}>\n {sections.map((section, i) => (\n <div key={section.label ?? `section-${i}`} className=\"flex flex-col\">\n {section.label &&\n (collapsed ? (\n <div className=\"mx-2 mb-1 mt-3 border-t border-sidebar-border first:mt-1\" />\n ) : (\n <div className=\"mb-0.5 mt-3 px-density-2 text-[10px] font-semibold uppercase tracking-[0.18em] text-sidebar-foreground/55 first:mt-1\">\n {section.label}\n </div>\n ))}\n {section.items.map((item) => (\n <NavItemRow key={item.key} item={item} collapsed={collapsed} />\n ))}\n </div>\n ))}\n </nav>\n );\n}\n\nfunction NavItemRow({ item, collapsed }: { item: AppShellNavItem; collapsed: boolean }) {\n const className = cn(\n \"flex w-full items-center gap-2.5 rounded-md px-density-2 py-1.5 text-left text-[13px] text-sidebar-foreground transition-colors hover:bg-sidebar-accent hover:text-sidebar-accent-foreground\",\n collapsed && \"justify-center px-0\",\n item.active && \"bg-sidebar-accent font-medium text-sidebar-primary\",\n );\n const inner = (\n <>\n {item.icon && (\n <span className=\"flex h-4 w-4 shrink-0 items-center justify-center\">\n <Icon {...(typeof item.icon === \"string\" ? { name: item.icon } : { icon: item.icon })} />\n </span>\n )}\n {!collapsed && <span className=\"flex-1 truncate\">{item.label}</span>}\n {!collapsed && item.badge}\n </>\n );\n const title = collapsed && typeof item.label === \"string\" ? item.label : undefined;\n\n if (item.href) {\n return (\n <a\n href={item.href}\n className={className}\n title={title}\n {...(item.external ? { target: \"_blank\", rel: \"noopener noreferrer\" } : {})}\n >\n {inner}\n </a>\n );\n }\n return (\n <button type=\"button\" onClick={item.onClick} className={className} title={title}>\n {inner}\n </button>\n );\n}\n"],"names":[],"mappings":"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"}
@@ -0,0 +1,66 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
+ const jsxRuntime = require("react/jsx-runtime");
4
+ const utils = require("../lib/utils.cjs");
5
+ const Icon = require("../data/Icon.cjs");
6
+ const toneRing = {
7
+ default: "",
8
+ danger: "border-l-2 border-l-red-500",
9
+ warning: "border-l-2 border-l-yellow-500",
10
+ success: "border-l-2 border-l-green-500",
11
+ info: "border-l-2 border-l-blue-500"
12
+ };
13
+ function Panel({
14
+ title,
15
+ icon,
16
+ count,
17
+ actions,
18
+ footer,
19
+ tone = "default",
20
+ padded = true,
21
+ className,
22
+ headerClassName,
23
+ bodyClassName,
24
+ children
25
+ }) {
26
+ const hasHeader = title !== void 0 || icon !== void 0 || actions !== void 0;
27
+ return /* @__PURE__ */ jsxRuntime.jsxs(
28
+ "div",
29
+ {
30
+ className: utils.cn(
31
+ "rounded-md border border-border bg-card overflow-hidden",
32
+ toneRing[tone],
33
+ className
34
+ ),
35
+ children: [
36
+ hasHeader && /* @__PURE__ */ jsxRuntime.jsxs(
37
+ "div",
38
+ {
39
+ className: utils.cn(
40
+ "flex items-center gap-2 px-density-3 py-density-2 border-b border-border",
41
+ headerClassName
42
+ ),
43
+ children: [
44
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex min-w-0 flex-1 items-center gap-2", children: [
45
+ icon && /* @__PURE__ */ jsxRuntime.jsx(
46
+ Icon.Icon,
47
+ {
48
+ ...typeof icon === "string" ? { name: icon } : { icon },
49
+ className: "text-base text-muted-foreground"
50
+ }
51
+ ),
52
+ title !== void 0 && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "font-medium text-sm truncate text-card-foreground", children: title }),
53
+ count !== void 0 && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "inline-flex items-center justify-center rounded-full bg-muted px-1.5 text-[11px] font-semibold text-muted-foreground", children: count })
54
+ ] }),
55
+ actions && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex shrink-0 items-center gap-1", children: actions })
56
+ ]
57
+ }
58
+ ),
59
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: utils.cn(padded && "px-density-3 py-density-2", bodyClassName), children }),
60
+ footer && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-density-3 py-density-2 border-t border-border", children: footer })
61
+ ]
62
+ }
63
+ );
64
+ }
65
+ exports.Panel = Panel;
66
+ //# sourceMappingURL=Panel.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Panel.cjs","sources":["../../src/layout/Panel.tsx"],"sourcesContent":["import type { ReactNode } from \"react\";\nimport { cn } from \"../lib/utils\";\nimport { Icon, type StaticIconComponent } from \"../data/Icon\";\n\nexport type PanelTone = \"default\" | \"danger\" | \"warning\" | \"success\" | \"info\";\n\nexport type PanelProps = {\n /** Header title. Omit (with no icon/count/actions) to render a headerless card. */\n title?: ReactNode;\n /** Optional leading icon. */\n icon?: string | StaticIconComponent;\n /** Optional count pill shown after the title. */\n count?: number;\n /** Right-aligned header content (copy buttons, summaries, links). */\n actions?: ReactNode;\n /** Optional footer row below the body. */\n footer?: ReactNode;\n /** Semantic left-border accent. */\n tone?: PanelTone;\n /** Pad the body. Defaults to true; pass false for flush row lists. */\n padded?: boolean;\n /** Classes applied to the panel root. */\n className?: string;\n /** Classes applied to the header row. */\n headerClassName?: string;\n /** Classes applied to the body. */\n bodyClassName?: string;\n /** Panel content. */\n children: ReactNode;\n};\n\nconst toneRing: Record<PanelTone, string> = {\n default: \"\",\n danger: \"border-l-2 border-l-red-500\",\n warning: \"border-l-2 border-l-yellow-500\",\n success: \"border-l-2 border-l-green-500\",\n info: \"border-l-2 border-l-blue-500\",\n};\n\n/**\n * A non-collapsible carded surface with an optional header. Use for content\n * panels (checks, results, comments) where {@link Section}'s disclosure\n * behaviour isn't wanted. Sits on `bg-card` with a border and rounded corners.\n */\nexport function Panel({\n title,\n icon,\n count,\n actions,\n footer,\n tone = \"default\",\n padded = true,\n className,\n headerClassName,\n bodyClassName,\n children,\n}: PanelProps) {\n const hasHeader = title !== undefined || icon !== undefined || actions !== undefined;\n return (\n <div\n className={cn(\n \"rounded-md border border-border bg-card overflow-hidden\",\n toneRing[tone],\n className,\n )}\n >\n {hasHeader && (\n <div\n className={cn(\n \"flex items-center gap-2 px-density-3 py-density-2 border-b border-border\",\n headerClassName,\n )}\n >\n <div className=\"flex min-w-0 flex-1 items-center gap-2\">\n {icon && (\n <Icon\n {...(typeof icon === \"string\" ? { name: icon } : { icon })}\n className=\"text-base text-muted-foreground\"\n />\n )}\n {title !== undefined && (\n <span className=\"font-medium text-sm truncate text-card-foreground\">{title}</span>\n )}\n {count !== undefined && (\n <span className=\"inline-flex items-center justify-center rounded-full bg-muted px-1.5 text-[11px] font-semibold text-muted-foreground\">\n {count}\n </span>\n )}\n </div>\n {actions && <div className=\"flex shrink-0 items-center gap-1\">{actions}</div>}\n </div>\n )}\n <div className={cn(padded && \"px-density-3 py-density-2\", bodyClassName)}>{children}</div>\n {footer && <div className=\"px-density-3 py-density-2 border-t border-border\">{footer}</div>}\n </div>\n );\n}\n"],"names":["jsxs","cn","jsx","Icon"],"mappings":";;;;;AA+BA,MAAM,WAAsC;AAAA,EAC1C,SAAS;AAAA,EACT,QAAQ;AAAA,EACR,SAAS;AAAA,EACT,SAAS;AAAA,EACT,MAAM;AACR;AAOO,SAAS,MAAM;AAAA,EACpB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,OAAO;AAAA,EACP,SAAS;AAAA,EACT;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAAe;AACb,QAAM,YAAY,UAAU,UAAa,SAAS,UAAa,YAAY;AAC3E,SACEA,2BAAAA;AAAAA,IAAC;AAAA,IAAA;AAAA,MACC,WAAWC,MAAAA;AAAAA,QACT;AAAA,QACA,SAAS,IAAI;AAAA,QACb;AAAA,MAAA;AAAA,MAGD,UAAA;AAAA,QAAA,aACCD,2BAAAA;AAAAA,UAAC;AAAA,UAAA;AAAA,YACC,WAAWC,MAAAA;AAAAA,cACT;AAAA,cACA;AAAA,YAAA;AAAA,YAGF,UAAA;AAAA,cAAAD,2BAAAA,KAAC,OAAA,EAAI,WAAU,0CACZ,UAAA;AAAA,gBAAA,QACCE,2BAAAA;AAAAA,kBAACC,KAAAA;AAAAA,kBAAA;AAAA,oBACE,GAAI,OAAO,SAAS,WAAW,EAAE,MAAM,KAAA,IAAS,EAAE,KAAA;AAAA,oBACnD,WAAU;AAAA,kBAAA;AAAA,gBAAA;AAAA,gBAGb,UAAU,UACTD,2BAAAA,IAAC,QAAA,EAAK,WAAU,qDAAqD,UAAA,OAAM;AAAA,gBAE5E,UAAU,UACTA,2BAAAA,IAAC,QAAA,EAAK,WAAU,wHACb,UAAA,MAAA,CACH;AAAA,cAAA,GAEJ;AAAA,cACC,WAAWA,2BAAAA,IAAC,OAAA,EAAI,WAAU,oCAAoC,UAAA,QAAA,CAAQ;AAAA,YAAA;AAAA,UAAA;AAAA,QAAA;AAAA,QAG3EA,+BAAC,SAAI,WAAWD,MAAAA,GAAG,UAAU,6BAA6B,aAAa,GAAI,UAAS;AAAA,QACnF,UAAUC,2BAAAA,IAAC,OAAA,EAAI,WAAU,oDAAoD,UAAA,OAAA,CAAO;AAAA,MAAA;AAAA,IAAA;AAAA,EAAA;AAG3F;;"}
@@ -0,0 +1,34 @@
1
+ import { ReactNode } from 'react';
2
+ import { StaticIconComponent } from '../data/Icon';
3
+ export type PanelTone = "default" | "danger" | "warning" | "success" | "info";
4
+ export type PanelProps = {
5
+ /** Header title. Omit (with no icon/count/actions) to render a headerless card. */
6
+ title?: ReactNode;
7
+ /** Optional leading icon. */
8
+ icon?: string | StaticIconComponent;
9
+ /** Optional count pill shown after the title. */
10
+ count?: number;
11
+ /** Right-aligned header content (copy buttons, summaries, links). */
12
+ actions?: ReactNode;
13
+ /** Optional footer row below the body. */
14
+ footer?: ReactNode;
15
+ /** Semantic left-border accent. */
16
+ tone?: PanelTone;
17
+ /** Pad the body. Defaults to true; pass false for flush row lists. */
18
+ padded?: boolean;
19
+ /** Classes applied to the panel root. */
20
+ className?: string;
21
+ /** Classes applied to the header row. */
22
+ headerClassName?: string;
23
+ /** Classes applied to the body. */
24
+ bodyClassName?: string;
25
+ /** Panel content. */
26
+ children: ReactNode;
27
+ };
28
+ /**
29
+ * A non-collapsible carded surface with an optional header. Use for content
30
+ * panels (checks, results, comments) where {@link Section}'s disclosure
31
+ * behaviour isn't wanted. Sits on `bg-card` with a border and rounded corners.
32
+ */
33
+ export declare function Panel({ title, icon, count, actions, footer, tone, padded, className, headerClassName, bodyClassName, children, }: PanelProps): import("react/jsx-runtime").JSX.Element;
34
+ //# sourceMappingURL=Panel.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Panel.d.ts","sourceRoot":"","sources":["../../src/layout/Panel.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAEvC,OAAO,EAAQ,KAAK,mBAAmB,EAAE,MAAM,cAAc,CAAC;AAE9D,MAAM,MAAM,SAAS,GAAG,SAAS,GAAG,QAAQ,GAAG,SAAS,GAAG,SAAS,GAAG,MAAM,CAAC;AAE9E,MAAM,MAAM,UAAU,GAAG;IACvB,mFAAmF;IACnF,KAAK,CAAC,EAAE,SAAS,CAAC;IAClB,6BAA6B;IAC7B,IAAI,CAAC,EAAE,MAAM,GAAG,mBAAmB,CAAC;IACpC,iDAAiD;IACjD,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,qEAAqE;IACrE,OAAO,CAAC,EAAE,SAAS,CAAC;IACpB,0CAA0C;IAC1C,MAAM,CAAC,EAAE,SAAS,CAAC;IACnB,mCAAmC;IACnC,IAAI,CAAC,EAAE,SAAS,CAAC;IACjB,sEAAsE;IACtE,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,yCAAyC;IACzC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,yCAAyC;IACzC,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,mCAAmC;IACnC,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,qBAAqB;IACrB,QAAQ,EAAE,SAAS,CAAC;CACrB,CAAC;AAUF;;;;GAIG;AACH,wBAAgB,KAAK,CAAC,EACpB,KAAK,EACL,IAAI,EACJ,KAAK,EACL,OAAO,EACP,MAAM,EACN,IAAgB,EAChB,MAAa,EACb,SAAS,EACT,eAAe,EACf,aAAa,EACb,QAAQ,GACT,EAAE,UAAU,2CAwCZ"}
@@ -0,0 +1,66 @@
1
+ import { jsxs, jsx } from "react/jsx-runtime";
2
+ import { cn } from "../lib/utils.js";
3
+ import { Icon } from "../data/Icon.js";
4
+ const toneRing = {
5
+ default: "",
6
+ danger: "border-l-2 border-l-red-500",
7
+ warning: "border-l-2 border-l-yellow-500",
8
+ success: "border-l-2 border-l-green-500",
9
+ info: "border-l-2 border-l-blue-500"
10
+ };
11
+ function Panel({
12
+ title,
13
+ icon,
14
+ count,
15
+ actions,
16
+ footer,
17
+ tone = "default",
18
+ padded = true,
19
+ className,
20
+ headerClassName,
21
+ bodyClassName,
22
+ children
23
+ }) {
24
+ const hasHeader = title !== void 0 || icon !== void 0 || actions !== void 0;
25
+ return /* @__PURE__ */ jsxs(
26
+ "div",
27
+ {
28
+ className: cn(
29
+ "rounded-md border border-border bg-card overflow-hidden",
30
+ toneRing[tone],
31
+ className
32
+ ),
33
+ children: [
34
+ hasHeader && /* @__PURE__ */ jsxs(
35
+ "div",
36
+ {
37
+ className: cn(
38
+ "flex items-center gap-2 px-density-3 py-density-2 border-b border-border",
39
+ headerClassName
40
+ ),
41
+ children: [
42
+ /* @__PURE__ */ jsxs("div", { className: "flex min-w-0 flex-1 items-center gap-2", children: [
43
+ icon && /* @__PURE__ */ jsx(
44
+ Icon,
45
+ {
46
+ ...typeof icon === "string" ? { name: icon } : { icon },
47
+ className: "text-base text-muted-foreground"
48
+ }
49
+ ),
50
+ title !== void 0 && /* @__PURE__ */ jsx("span", { className: "font-medium text-sm truncate text-card-foreground", children: title }),
51
+ count !== void 0 && /* @__PURE__ */ jsx("span", { className: "inline-flex items-center justify-center rounded-full bg-muted px-1.5 text-[11px] font-semibold text-muted-foreground", children: count })
52
+ ] }),
53
+ actions && /* @__PURE__ */ jsx("div", { className: "flex shrink-0 items-center gap-1", children: actions })
54
+ ]
55
+ }
56
+ ),
57
+ /* @__PURE__ */ jsx("div", { className: cn(padded && "px-density-3 py-density-2", bodyClassName), children }),
58
+ footer && /* @__PURE__ */ jsx("div", { className: "px-density-3 py-density-2 border-t border-border", children: footer })
59
+ ]
60
+ }
61
+ );
62
+ }
63
+ export {
64
+ Panel
65
+ };
66
+ //# sourceMappingURL=Panel.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Panel.js","sources":["../../src/layout/Panel.tsx"],"sourcesContent":["import type { ReactNode } from \"react\";\nimport { cn } from \"../lib/utils\";\nimport { Icon, type StaticIconComponent } from \"../data/Icon\";\n\nexport type PanelTone = \"default\" | \"danger\" | \"warning\" | \"success\" | \"info\";\n\nexport type PanelProps = {\n /** Header title. Omit (with no icon/count/actions) to render a headerless card. */\n title?: ReactNode;\n /** Optional leading icon. */\n icon?: string | StaticIconComponent;\n /** Optional count pill shown after the title. */\n count?: number;\n /** Right-aligned header content (copy buttons, summaries, links). */\n actions?: ReactNode;\n /** Optional footer row below the body. */\n footer?: ReactNode;\n /** Semantic left-border accent. */\n tone?: PanelTone;\n /** Pad the body. Defaults to true; pass false for flush row lists. */\n padded?: boolean;\n /** Classes applied to the panel root. */\n className?: string;\n /** Classes applied to the header row. */\n headerClassName?: string;\n /** Classes applied to the body. */\n bodyClassName?: string;\n /** Panel content. */\n children: ReactNode;\n};\n\nconst toneRing: Record<PanelTone, string> = {\n default: \"\",\n danger: \"border-l-2 border-l-red-500\",\n warning: \"border-l-2 border-l-yellow-500\",\n success: \"border-l-2 border-l-green-500\",\n info: \"border-l-2 border-l-blue-500\",\n};\n\n/**\n * A non-collapsible carded surface with an optional header. Use for content\n * panels (checks, results, comments) where {@link Section}'s disclosure\n * behaviour isn't wanted. Sits on `bg-card` with a border and rounded corners.\n */\nexport function Panel({\n title,\n icon,\n count,\n actions,\n footer,\n tone = \"default\",\n padded = true,\n className,\n headerClassName,\n bodyClassName,\n children,\n}: PanelProps) {\n const hasHeader = title !== undefined || icon !== undefined || actions !== undefined;\n return (\n <div\n className={cn(\n \"rounded-md border border-border bg-card overflow-hidden\",\n toneRing[tone],\n className,\n )}\n >\n {hasHeader && (\n <div\n className={cn(\n \"flex items-center gap-2 px-density-3 py-density-2 border-b border-border\",\n headerClassName,\n )}\n >\n <div className=\"flex min-w-0 flex-1 items-center gap-2\">\n {icon && (\n <Icon\n {...(typeof icon === \"string\" ? { name: icon } : { icon })}\n className=\"text-base text-muted-foreground\"\n />\n )}\n {title !== undefined && (\n <span className=\"font-medium text-sm truncate text-card-foreground\">{title}</span>\n )}\n {count !== undefined && (\n <span className=\"inline-flex items-center justify-center rounded-full bg-muted px-1.5 text-[11px] font-semibold text-muted-foreground\">\n {count}\n </span>\n )}\n </div>\n {actions && <div className=\"flex shrink-0 items-center gap-1\">{actions}</div>}\n </div>\n )}\n <div className={cn(padded && \"px-density-3 py-density-2\", bodyClassName)}>{children}</div>\n {footer && <div className=\"px-density-3 py-density-2 border-t border-border\">{footer}</div>}\n </div>\n );\n}\n"],"names":[],"mappings":";;;AA+BA,MAAM,WAAsC;AAAA,EAC1C,SAAS;AAAA,EACT,QAAQ;AAAA,EACR,SAAS;AAAA,EACT,SAAS;AAAA,EACT,MAAM;AACR;AAOO,SAAS,MAAM;AAAA,EACpB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,OAAO;AAAA,EACP,SAAS;AAAA,EACT;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAAe;AACb,QAAM,YAAY,UAAU,UAAa,SAAS,UAAa,YAAY;AAC3E,SACE;AAAA,IAAC;AAAA,IAAA;AAAA,MACC,WAAW;AAAA,QACT;AAAA,QACA,SAAS,IAAI;AAAA,QACb;AAAA,MAAA;AAAA,MAGD,UAAA;AAAA,QAAA,aACC;AAAA,UAAC;AAAA,UAAA;AAAA,YACC,WAAW;AAAA,cACT;AAAA,cACA;AAAA,YAAA;AAAA,YAGF,UAAA;AAAA,cAAA,qBAAC,OAAA,EAAI,WAAU,0CACZ,UAAA;AAAA,gBAAA,QACC;AAAA,kBAAC;AAAA,kBAAA;AAAA,oBACE,GAAI,OAAO,SAAS,WAAW,EAAE,MAAM,KAAA,IAAS,EAAE,KAAA;AAAA,oBACnD,WAAU;AAAA,kBAAA;AAAA,gBAAA;AAAA,gBAGb,UAAU,UACT,oBAAC,QAAA,EAAK,WAAU,qDAAqD,UAAA,OAAM;AAAA,gBAE5E,UAAU,UACT,oBAAC,QAAA,EAAK,WAAU,wHACb,UAAA,MAAA,CACH;AAAA,cAAA,GAEJ;AAAA,cACC,WAAW,oBAAC,OAAA,EAAI,WAAU,oCAAoC,UAAA,QAAA,CAAQ;AAAA,YAAA;AAAA,UAAA;AAAA,QAAA;AAAA,QAG3E,oBAAC,SAAI,WAAW,GAAG,UAAU,6BAA6B,aAAa,GAAI,UAAS;AAAA,QACnF,UAAU,oBAAC,OAAA,EAAI,WAAU,oDAAoD,UAAA,OAAA,CAAO;AAAA,MAAA;AAAA,IAAA;AAAA,EAAA;AAG3F;"}
@@ -0,0 +1,36 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
+ const jsxRuntime = require("react/jsx-runtime");
4
+ const utils = require("../lib/utils.cjs");
5
+ const TabButton = require("../data/TabButton.cjs");
6
+ function Tabs({ tabs, value, onChange, variant = "underline", className }) {
7
+ return /* @__PURE__ */ jsxRuntime.jsx(
8
+ "div",
9
+ {
10
+ role: "tablist",
11
+ className: utils.cn(
12
+ "flex items-center gap-density-1",
13
+ variant === "underline" && "border-b border-border",
14
+ className
15
+ ),
16
+ children: tabs.map((tab) => /* @__PURE__ */ jsxRuntime.jsx(
17
+ TabButton.TabButton,
18
+ {
19
+ active: tab.id === value,
20
+ onClick: () => {
21
+ if (!tab.disabled) onChange(tab.id);
22
+ },
23
+ label: tab.label,
24
+ variant,
25
+ ...tab.icon !== void 0 ? { icon: tab.icon } : {},
26
+ ...tab.count !== void 0 ? { count: tab.count } : {},
27
+ ...tab.countColor !== void 0 ? { countColor: tab.countColor } : {},
28
+ ...tab.disabled ? { className: "opacity-50 pointer-events-none" } : {}
29
+ },
30
+ tab.id
31
+ ))
32
+ }
33
+ );
34
+ }
35
+ exports.Tabs = Tabs;
36
+ //# sourceMappingURL=Tabs.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Tabs.cjs","sources":["../../src/layout/Tabs.tsx"],"sourcesContent":["import type { ReactNode } from \"react\";\nimport { cn } from \"../lib/utils\";\nimport { TabButton, type TabButtonVariant } from \"../data/TabButton\";\nimport type { StaticIconComponent } from \"../data/Icon\";\n\nexport type TabItem = {\n /** Stable id; also the value compared against the selected `value`. */\n id: string;\n /** Visible label. */\n label: ReactNode;\n /** Optional leading icon (icon name or component). */\n icon?: string | StaticIconComponent;\n /** Optional count badge; hidden when zero. */\n count?: number;\n /** Classes for the count badge background. */\n countColor?: string;\n /** Disables selecting this tab. */\n disabled?: boolean;\n};\n\nexport type TabsProps = {\n /** Tabs to render, in order. */\n tabs: TabItem[];\n /** Currently selected tab id. */\n value: string;\n /** Called with the next tab id when a tab is clicked. */\n onChange: (id: string) => void;\n /** Visual style. Defaults to `underline`. */\n variant?: TabButtonVariant;\n /** Classes applied to the tablist row. */\n className?: string;\n};\n\n/**\n * A controlled tab strip built on {@link TabButton}. Defaults to the\n * `underline` variant — the row carries a bottom border and the active tab's\n * underline overlaps it. Render the matching panel yourself based on `value`.\n */\nexport function Tabs({ tabs, value, onChange, variant = \"underline\", className }: TabsProps) {\n return (\n <div\n role=\"tablist\"\n className={cn(\n \"flex items-center gap-density-1\",\n variant === \"underline\" && \"border-b border-border\",\n className,\n )}\n >\n {tabs.map((tab) => (\n <TabButton\n key={tab.id}\n active={tab.id === value}\n onClick={() => {\n if (!tab.disabled) onChange(tab.id);\n }}\n label={tab.label}\n variant={variant}\n {...(tab.icon !== undefined ? { icon: tab.icon } : {})}\n {...(tab.count !== undefined ? { count: tab.count } : {})}\n {...(tab.countColor !== undefined ? { countColor: tab.countColor } : {})}\n {...(tab.disabled ? { className: \"opacity-50 pointer-events-none\" } : {})}\n />\n ))}\n </div>\n );\n}\n"],"names":["jsx","cn","TabButton"],"mappings":";;;;;AAsCO,SAAS,KAAK,EAAE,MAAM,OAAO,UAAU,UAAU,aAAa,aAAwB;AAC3F,SACEA,2BAAAA;AAAAA,IAAC;AAAA,IAAA;AAAA,MACC,MAAK;AAAA,MACL,WAAWC,MAAAA;AAAAA,QACT;AAAA,QACA,YAAY,eAAe;AAAA,QAC3B;AAAA,MAAA;AAAA,MAGD,UAAA,KAAK,IAAI,CAAC,QACTD,2BAAAA;AAAAA,QAACE,UAAAA;AAAAA,QAAA;AAAA,UAEC,QAAQ,IAAI,OAAO;AAAA,UACnB,SAAS,MAAM;AACb,gBAAI,CAAC,IAAI,SAAU,UAAS,IAAI,EAAE;AAAA,UACpC;AAAA,UACA,OAAO,IAAI;AAAA,UACX;AAAA,UACC,GAAI,IAAI,SAAS,SAAY,EAAE,MAAM,IAAI,KAAA,IAAS,CAAA;AAAA,UAClD,GAAI,IAAI,UAAU,SAAY,EAAE,OAAO,IAAI,MAAA,IAAU,CAAA;AAAA,UACrD,GAAI,IAAI,eAAe,SAAY,EAAE,YAAY,IAAI,WAAA,IAAe,CAAA;AAAA,UACpE,GAAI,IAAI,WAAW,EAAE,WAAW,iCAAA,IAAqC,CAAA;AAAA,QAAC;AAAA,QAVlE,IAAI;AAAA,MAAA,CAYZ;AAAA,IAAA;AAAA,EAAA;AAGP;;"}
@@ -0,0 +1,36 @@
1
+ import { ReactNode } from 'react';
2
+ import { TabButtonVariant } from '../data/TabButton';
3
+ import { StaticIconComponent } from '../data/Icon';
4
+ export type TabItem = {
5
+ /** Stable id; also the value compared against the selected `value`. */
6
+ id: string;
7
+ /** Visible label. */
8
+ label: ReactNode;
9
+ /** Optional leading icon (icon name or component). */
10
+ icon?: string | StaticIconComponent;
11
+ /** Optional count badge; hidden when zero. */
12
+ count?: number;
13
+ /** Classes for the count badge background. */
14
+ countColor?: string;
15
+ /** Disables selecting this tab. */
16
+ disabled?: boolean;
17
+ };
18
+ export type TabsProps = {
19
+ /** Tabs to render, in order. */
20
+ tabs: TabItem[];
21
+ /** Currently selected tab id. */
22
+ value: string;
23
+ /** Called with the next tab id when a tab is clicked. */
24
+ onChange: (id: string) => void;
25
+ /** Visual style. Defaults to `underline`. */
26
+ variant?: TabButtonVariant;
27
+ /** Classes applied to the tablist row. */
28
+ className?: string;
29
+ };
30
+ /**
31
+ * A controlled tab strip built on {@link TabButton}. Defaults to the
32
+ * `underline` variant — the row carries a bottom border and the active tab's
33
+ * underline overlaps it. Render the matching panel yourself based on `value`.
34
+ */
35
+ export declare function Tabs({ tabs, value, onChange, variant, className }: TabsProps): import("react/jsx-runtime").JSX.Element;
36
+ //# sourceMappingURL=Tabs.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Tabs.d.ts","sourceRoot":"","sources":["../../src/layout/Tabs.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAEvC,OAAO,EAAa,KAAK,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AACrE,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,cAAc,CAAC;AAExD,MAAM,MAAM,OAAO,GAAG;IACpB,uEAAuE;IACvE,EAAE,EAAE,MAAM,CAAC;IACX,qBAAqB;IACrB,KAAK,EAAE,SAAS,CAAC;IACjB,sDAAsD;IACtD,IAAI,CAAC,EAAE,MAAM,GAAG,mBAAmB,CAAC;IACpC,8CAA8C;IAC9C,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,8CAA8C;IAC9C,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,mCAAmC;IACnC,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB,CAAC;AAEF,MAAM,MAAM,SAAS,GAAG;IACtB,gCAAgC;IAChC,IAAI,EAAE,OAAO,EAAE,CAAC;IAChB,iCAAiC;IACjC,KAAK,EAAE,MAAM,CAAC;IACd,yDAAyD;IACzD,QAAQ,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,CAAC;IAC/B,6CAA6C;IAC7C,OAAO,CAAC,EAAE,gBAAgB,CAAC;IAC3B,0CAA0C;IAC1C,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,CAAC;AAEF;;;;GAIG;AACH,wBAAgB,IAAI,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,QAAQ,EAAE,OAAqB,EAAE,SAAS,EAAE,EAAE,SAAS,2CA2B1F"}
@@ -0,0 +1,36 @@
1
+ import { jsx } from "react/jsx-runtime";
2
+ import { cn } from "../lib/utils.js";
3
+ import { TabButton } from "../data/TabButton.js";
4
+ function Tabs({ tabs, value, onChange, variant = "underline", className }) {
5
+ return /* @__PURE__ */ jsx(
6
+ "div",
7
+ {
8
+ role: "tablist",
9
+ className: cn(
10
+ "flex items-center gap-density-1",
11
+ variant === "underline" && "border-b border-border",
12
+ className
13
+ ),
14
+ children: tabs.map((tab) => /* @__PURE__ */ jsx(
15
+ TabButton,
16
+ {
17
+ active: tab.id === value,
18
+ onClick: () => {
19
+ if (!tab.disabled) onChange(tab.id);
20
+ },
21
+ label: tab.label,
22
+ variant,
23
+ ...tab.icon !== void 0 ? { icon: tab.icon } : {},
24
+ ...tab.count !== void 0 ? { count: tab.count } : {},
25
+ ...tab.countColor !== void 0 ? { countColor: tab.countColor } : {},
26
+ ...tab.disabled ? { className: "opacity-50 pointer-events-none" } : {}
27
+ },
28
+ tab.id
29
+ ))
30
+ }
31
+ );
32
+ }
33
+ export {
34
+ Tabs
35
+ };
36
+ //# sourceMappingURL=Tabs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Tabs.js","sources":["../../src/layout/Tabs.tsx"],"sourcesContent":["import type { ReactNode } from \"react\";\nimport { cn } from \"../lib/utils\";\nimport { TabButton, type TabButtonVariant } from \"../data/TabButton\";\nimport type { StaticIconComponent } from \"../data/Icon\";\n\nexport type TabItem = {\n /** Stable id; also the value compared against the selected `value`. */\n id: string;\n /** Visible label. */\n label: ReactNode;\n /** Optional leading icon (icon name or component). */\n icon?: string | StaticIconComponent;\n /** Optional count badge; hidden when zero. */\n count?: number;\n /** Classes for the count badge background. */\n countColor?: string;\n /** Disables selecting this tab. */\n disabled?: boolean;\n};\n\nexport type TabsProps = {\n /** Tabs to render, in order. */\n tabs: TabItem[];\n /** Currently selected tab id. */\n value: string;\n /** Called with the next tab id when a tab is clicked. */\n onChange: (id: string) => void;\n /** Visual style. Defaults to `underline`. */\n variant?: TabButtonVariant;\n /** Classes applied to the tablist row. */\n className?: string;\n};\n\n/**\n * A controlled tab strip built on {@link TabButton}. Defaults to the\n * `underline` variant — the row carries a bottom border and the active tab's\n * underline overlaps it. Render the matching panel yourself based on `value`.\n */\nexport function Tabs({ tabs, value, onChange, variant = \"underline\", className }: TabsProps) {\n return (\n <div\n role=\"tablist\"\n className={cn(\n \"flex items-center gap-density-1\",\n variant === \"underline\" && \"border-b border-border\",\n className,\n )}\n >\n {tabs.map((tab) => (\n <TabButton\n key={tab.id}\n active={tab.id === value}\n onClick={() => {\n if (!tab.disabled) onChange(tab.id);\n }}\n label={tab.label}\n variant={variant}\n {...(tab.icon !== undefined ? { icon: tab.icon } : {})}\n {...(tab.count !== undefined ? { count: tab.count } : {})}\n {...(tab.countColor !== undefined ? { countColor: tab.countColor } : {})}\n {...(tab.disabled ? { className: \"opacity-50 pointer-events-none\" } : {})}\n />\n ))}\n </div>\n );\n}\n"],"names":[],"mappings":";;;AAsCO,SAAS,KAAK,EAAE,MAAM,OAAO,UAAU,UAAU,aAAa,aAAwB;AAC3F,SACE;AAAA,IAAC;AAAA,IAAA;AAAA,MACC,MAAK;AAAA,MACL,WAAW;AAAA,QACT;AAAA,QACA,YAAY,eAAe;AAAA,QAC3B;AAAA,MAAA;AAAA,MAGD,UAAA,KAAK,IAAI,CAAC,QACT;AAAA,QAAC;AAAA,QAAA;AAAA,UAEC,QAAQ,IAAI,OAAO;AAAA,UACnB,SAAS,MAAM;AACb,gBAAI,CAAC,IAAI,SAAU,UAAS,IAAI,EAAE;AAAA,UACpC;AAAA,UACA,OAAO,IAAI;AAAA,UACX;AAAA,UACC,GAAI,IAAI,SAAS,SAAY,EAAE,MAAM,IAAI,KAAA,IAAS,CAAA;AAAA,UAClD,GAAI,IAAI,UAAU,SAAY,EAAE,OAAO,IAAI,MAAA,IAAU,CAAA;AAAA,UACrD,GAAI,IAAI,eAAe,SAAY,EAAE,YAAY,IAAI,WAAA,IAAe,CAAA;AAAA,UACpE,GAAI,IAAI,WAAW,EAAE,WAAW,iCAAA,IAAqC,CAAA;AAAA,QAAC;AAAA,QAVlE,IAAI;AAAA,MAAA,CAYZ;AAAA,IAAA;AAAA,EAAA;AAGP;"}