@zvk/composite 0.1.3 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
1
1
  # @zvk/composite Changelog
2
2
 
3
+ ## [0.1.4](https://github.com/brandon-schabel/zvk/compare/composite-v0.1.3...composite-v0.1.4) (2026-06-25)
4
+
5
+
6
+ ### Features
7
+
8
+ * improve out-of-box DX and UI components ([#24](https://github.com/brandon-schabel/zvk/issues/24)) ([b01272a](https://github.com/brandon-schabel/zvk/commit/b01272a2b35099f5e1fa1dea5883687a0fb4c2e6))
9
+
3
10
  ## [0.1.3](https://github.com/brandon-schabel/zvk/compare/composite-v0.1.2...composite-v0.1.3) (2026-06-20)
4
11
 
5
12
 
package/README.md CHANGED
@@ -18,11 +18,11 @@ Composite typography inherits the shared `@zvk/ui` font-family slots: primary fo
18
18
  The package is organized around product-surface domains so repeated shell structure stays readable without creating new public import groups. This map is for internal ownership and review; it does not add domain barrel imports to the public API.
19
19
 
20
20
  - `shell`: `AppWorkspaceShell`, `PageScaffold`, `FeatureShell`
21
- - `navigation`: `WorkspaceHeader`, `BreadcrumbPageHeader`, `SectionedWorkspaceShell`, `EntitySwitcherMenu`, `CommandPaletteShell`, `LinkAction`
21
+ - `navigation`: `WorkspaceHeader`, `BreadcrumbPageHeader`, `SimpleWorkspaceShell`, `SplitWorkspaceShell`, `SectionedWorkspaceShell`, `ResourceExplorerShell`, `EntitySwitcherMenu`, `CommandPaletteShell`, `LinkAction`
22
22
  - `state`: `StateSurface`, `RouteStateFrame`
23
23
  - `forms`: `FormSurface`, `ConfirmActionDialog`
24
24
  - `data`: `EntityCard`, `EntityListSection`, `SummaryMetricGrid`, `DataTablePageFrame`, `DataTableControlBar`
25
- - `workflow`: `IntegrationStatusCard`, `WorkflowStatusCard`, `ProcessListPanel`
25
+ - `workflow`: `IntegrationStatusCard`, `WorkflowStatusCard`, `ProcessListPanel`, `WizardShell`
26
26
  - `settings`: `SettingsHubList`, `ParameterEditor`, `SettingsSectionStack`
27
27
  - `ai`: `ProviderModelSelector`, `ConversationDirectory`, `StickyComposer`
28
28
  - `activity`: `ActivityFeed`
@@ -49,7 +49,7 @@ Keep these in the consuming app or a more specific package:
49
49
  - `./styles.css` is a stable stylesheet export.
50
50
  - `./package.json` is stable metadata and tooling export, not a component surface.
51
51
 
52
- Watched surfaces: `EntitySwitcherMenu`, `CommandPaletteShell`, `ConfirmActionDialog`, `IntegrationStatusCard`, `WorkflowStatusCard`, `ProcessListPanel`, `ActivityFeed`, `ParameterEditor`, `ProviderModelSelector`, `ConversationDirectory`, `StickyComposer`, and `DataTableControlBar`.
52
+ Watched surfaces: `SimpleWorkspaceShell`, `SplitWorkspaceShell`, `ResourceExplorerShell`, `WizardShell`, `EntitySwitcherMenu`, `CommandPaletteShell`, `ConfirmActionDialog`, `IntegrationStatusCard`, `WorkflowStatusCard`, `ProcessListPanel`, `ActivityFeed`, `ParameterEditor`, `ProviderModelSelector`, `ConversationDirectory`, `StickyComposer`, and `DataTableControlBar`.
53
53
 
54
54
  ## App Surface Exports
55
55
 
@@ -62,7 +62,10 @@ App surfaces are intentionally router-agnostic and UI-only:
62
62
  | `CommandPaletteShell` | `@zvk/composite/command-palette-shell` | Controlled command palette dialog with grouped commands and app-owned command execution. |
63
63
  | `WorkspaceHeader` | `@zvk/composite/workspace-header` | Page header with title, description, status, metadata, and actions. |
64
64
  | `BreadcrumbPageHeader` | `@zvk/composite/breadcrumb-page-header` | Breadcrumb-aware page header for nested workspace routes. |
65
+ | `SimpleWorkspaceShell` | `@zvk/composite/simple-workspace-shell` | Generic workspace frame with optional navigation, header, toolbar, actions, aside, footer, and content slots. |
66
+ | `SplitWorkspaceShell` | `@zvk/composite/split-workspace-shell` | Resizable two-pane workspace shell built on `@zvk/ui/splitter` for app-owned resource/detail or editor/preview layouts. |
65
67
  | `SectionedWorkspaceShell` | `@zvk/composite/sectioned-workspace-shell` | Sectioned navigation shell for dense workspace panels. |
68
+ | `ResourceExplorerShell` | `@zvk/composite/resource-explorer-shell` | Tree plus result-list explorer shell for app-owned repositories, files, records, resources, or docs. |
66
69
  | `PageScaffold` | `@zvk/composite/page-scaffold` | Page layout with toolbar, aside, constrained width, and content slots. |
67
70
  | `FeatureShell` | `@zvk/composite/feature-shell` | Reusable feature page layout with title, description, actions, metadata, and toolbar slots. |
68
71
  | `StateSurface` | `@zvk/composite/state-surface` | Empty, loading, error, permission, success, and search-empty state panels. |
@@ -76,6 +79,7 @@ App surfaces are intentionally router-agnostic and UI-only:
76
79
  | `IntegrationStatusCard` | `@zvk/composite/integration-status-card` | Provider/API/service connection status card with status, badges, metadata, metrics, actions, and errors. |
77
80
  | `WorkflowStatusCard` | `@zvk/composite/workflow-status-card` | Workflow, run, import, queue, deploy, or parser status card with progress and current-step slots. |
78
81
  | `ProcessListPanel` | `@zvk/composite/process-list-panel` | List panel for app-owned background work items, rendered by default through workflow status cards. |
82
+ | `WizardShell` | `@zvk/composite/wizard-shell` | Step-by-step workflow frame built on `@zvk/ui/stepper`, with app-owned validation, submission, and persistence. |
79
83
  | `ActivityFeed` | `@zvk/composite/activity-feed` | Ordered activity, audit, workflow, comment, or notification feed with app-owned ordering and timestamps. |
80
84
  | `ParameterEditor` | `@zvk/composite/parameter-editor` | Grouped parameter/settings rows where controls, validation, persistence, and reset actions stay in the app. |
81
85
  | `SettingsSectionStack` | `@zvk/composite/settings-section-stack` | Settings page section stack with controlled dirty/save presentation and app-owned forms. |
@@ -135,11 +139,14 @@ Migrate repeated app-owned shells into `@zvk/composite` only when the repeated p
135
139
  | Repeated app surface | Composite target | Keep in the app |
136
140
  | --- | --- | --- |
137
141
  | Product workspace chrome with brand, account, nav, switcher, command, and content slots | `AppWorkspaceShell` | route tree, auth context, workspace persistence, account menu actions |
142
+ | Generic workspace pages with navigation, actions, sidebars, and footer chrome | `SimpleWorkspaceShell` | route tree, permissions, data loading, selected-record state |
143
+ | Split resource/detail, editor/preview, or list/inspector workspaces | `SplitWorkspaceShell` | persistence of pane sizes, route params, selected entity fetching, mobile drawer behavior |
138
144
  | Tab-with-sidebar or dense route workspace wrappers | `SectionedWorkspaceShell` | active route, URL updates, permission gating, tab persistence |
145
+ | Tree plus result-list explorers | `ResourceExplorerShell` | search/filter algorithms, repository adapters, file I/O, routing, permissions, preview data fetching |
139
146
  | Route/page loading, empty, permission, error, and ready wrappers | `RouteStateFrame` or `StateSurface` | loader state, retry mutations, fetch errors, domain-specific empty data |
140
147
  | Page headers and breadcrumb headers | `WorkspaceHeader` or `BreadcrumbPageHeader` | router links, breadcrumb construction, page-level data policy |
141
148
  | Settings index pages and settings detail pages | `SettingsHubList` or `SettingsSectionStack` | route anchors, dirty-state calculation, validation, persistence, permission decisions |
142
- | Provider/API status cards, workflow cards, and process lists | `IntegrationStatusCard`, `WorkflowStatusCard`, or `ProcessListPanel` | health checks, polling, process control, retries, logs, provider credentials |
149
+ | Provider/API status cards, workflow cards, process lists, and setup flows | `IntegrationStatusCard`, `WorkflowStatusCard`, `ProcessListPanel`, or `WizardShell` | health checks, polling, process control, retries, logs, provider credentials, validation, submission |
143
150
  | Audit logs, run histories, review threads, and notification feeds | `ActivityFeed` | event fetching, ordering, timestamps, subscriptions, comment mutations, infinite scroll |
144
151
  | AI model choices, conversation sidebars, and sticky prompt forms | `ProviderModelSelector`, `ConversationDirectory`, or `StickyComposer` | catalogs, compatibility rules, message fetching, prompt validation, streaming, tools, persistence |
145
152
  | Table page wrappers and table control bars | `DataTablePageFrame` or `DataTableControlBar` | table engine adapters, query state, sorting, filters, pagination, row selection, saved views |
@@ -457,6 +464,116 @@ import { DataTableControlBar } from "@zvk/composite/data-table-control-bar";
457
464
  />;
458
465
  ```
459
466
 
467
+ ## Local-First Workspace Recipe
468
+
469
+ For a local-first todo or list workspace, reach for `AppWorkspaceShell` first, then use `FeatureShell` or `PageScaffold` for the page body, `SummaryMetricGrid` for counts, `EntityListSection` for the repeated list, `FormSurface` for the native form, and `StateSurface` for empty/loading/error presentation.
470
+
471
+ Keep the app-owned parts small and explicit:
472
+
473
+ - route state, persistence, and server actions stay in the app;
474
+ - fields, details, and validation stay in app-owned primitives or local helpers;
475
+ - shell chrome stays in `@zvk/composite`, not custom page CSS;
476
+ - aim for under 120 lines of app-local CSS before you add another shell wrapper.
477
+
478
+ ```tsx
479
+ import "@zvk/ui/styles.css";
480
+ import "@zvk/composite/styles.css";
481
+
482
+ import {
483
+ AppWorkspaceShell,
484
+ EntityListSection,
485
+ FeatureShell,
486
+ FormSurface,
487
+ StateSurface,
488
+ SummaryMetricGrid
489
+ } from "@zvk/composite";
490
+
491
+ type Task = {
492
+ id: string;
493
+ title: string;
494
+ status: "open" | "doing" | "done";
495
+ dueLabel: string;
496
+ };
497
+
498
+ async function createTask(formData: FormData) {
499
+ "use server";
500
+
501
+ // App-owned mutation, persistence, and revalidation.
502
+ void formData;
503
+ }
504
+
505
+ export function TasksPage({ tasks }: { tasks: readonly Task[] }) {
506
+ return (
507
+ <AppWorkspaceShell
508
+ brand={<strong>Northline</strong>}
509
+ navigation={<nav aria-label="Workspace navigation">...</nav>}
510
+ >
511
+ <FeatureShell
512
+ actions={<button type="button">Sync</button>}
513
+ description="Keep the workspace local-first and fast to scan."
514
+ title="Tasks"
515
+ >
516
+ <SummaryMetricGrid
517
+ columns={3}
518
+ metrics={[
519
+ { id: "open", label: "Open", value: 12 },
520
+ { id: "doing", label: "In progress", value: 4 },
521
+ { id: "done", label: "Done", value: 31 }
522
+ ]}
523
+ title="Today"
524
+ />
525
+
526
+ {tasks.length > 0 ? (
527
+ <EntityListSection
528
+ description="App-owned data, routes, and filters stay outside the composite."
529
+ getItemKey={(task) => task.id}
530
+ items={tasks}
531
+ renderItem={(task) => (
532
+ <article>
533
+ <h3>{task.title}</h3>
534
+ <p>
535
+ {task.status} · {task.dueLabel}
536
+ </p>
537
+ </article>
538
+ )}
539
+ title="Backlog"
540
+ />
541
+ ) : (
542
+ <StateSurface
543
+ description="Add the first task with the form below."
544
+ state="empty"
545
+ title="No tasks yet"
546
+ />
547
+ )}
548
+ </FeatureShell>
549
+
550
+ <FormSurface
551
+ action={createTask}
552
+ description="Native inputs and server actions stay app-owned."
553
+ footer={<button type="submit">Create task</button>}
554
+ method="post"
555
+ title="Add task"
556
+ >
557
+ <label>
558
+ Title
559
+ <input name="title" required />
560
+ </label>
561
+ <label>
562
+ Notes
563
+ <textarea name="notes" rows={4} />
564
+ </label>
565
+ </FormSurface>
566
+ </AppWorkspaceShell>
567
+ );
568
+ }
569
+ ```
570
+
571
+ Checklist:
572
+
573
+ - use `@zvk/composite` before adding custom shell CSS;
574
+ - keep primitives for fields and details only;
575
+ - prefer native forms and app-owned server actions over custom mutation plumbing.
576
+
460
577
  ## Repo Skill
461
578
 
462
579
  Use `.codex/skills/use-zvk-composite/SKILL.md` when maintaining this package.
@@ -6,5 +6,11 @@ export { EntitySwitcherMenu } from "../navigation/entity-switcher-menu.js";
6
6
  export type { EntitySwitcherGroup, EntitySwitcherItem, EntitySwitcherMenuProps, EntitySwitcherMenuSize } from "../navigation/entity-switcher-menu.js";
7
7
  export { LinkAction } from "../navigation/link-action.js";
8
8
  export type { LinkActionProps } from "../navigation/link-action.js";
9
+ export { ResourceExplorerShell } from "../navigation/resource-explorer-shell.js";
10
+ export type { ResourceExplorerShellProps, ResourceExplorerShellResource } from "../navigation/resource-explorer-shell.js";
9
11
  export { SectionedWorkspaceShell } from "../navigation/sectioned-workspace-shell.js";
10
12
  export type { SectionedWorkspaceItem, SectionedWorkspaceSection, SectionedWorkspaceShellProps } from "../navigation/sectioned-workspace-shell.js";
13
+ export { SimpleWorkspaceShell } from "../navigation/simple-workspace-shell.js";
14
+ export type { SimpleWorkspaceShellProps } from "../navigation/simple-workspace-shell.js";
15
+ export { SplitWorkspaceShell } from "../navigation/split-workspace-shell.js";
16
+ export type { SplitWorkspaceShellProps } from "../navigation/split-workspace-shell.js";
@@ -2,4 +2,7 @@ export { BreadcrumbPageHeader, WorkspaceHeader } from "../navigation/workspace-h
2
2
  export { CommandPaletteShell } from "../navigation/command-palette-shell.js";
3
3
  export { EntitySwitcherMenu } from "../navigation/entity-switcher-menu.js";
4
4
  export { LinkAction } from "../navigation/link-action.js";
5
+ export { ResourceExplorerShell } from "../navigation/resource-explorer-shell.js";
5
6
  export { SectionedWorkspaceShell } from "../navigation/sectioned-workspace-shell.js";
7
+ export { SimpleWorkspaceShell } from "../navigation/simple-workspace-shell.js";
8
+ export { SplitWorkspaceShell } from "../navigation/split-workspace-shell.js";
@@ -2,5 +2,7 @@ export { IntegrationStatusCard } from "../integrations/integration-status-card.j
2
2
  export type { IntegrationStatusCardProps, IntegrationStatusTone } from "../integrations/integration-status-card.js";
3
3
  export { ProcessListPanel } from "../workflows/process-list-panel.js";
4
4
  export type { ProcessListPanelItem, ProcessListPanelProps } from "../workflows/process-list-panel.js";
5
+ export { WizardShell } from "../workflows/wizard-shell.js";
6
+ export type { WizardShellProps, WizardShellStep, WizardShellStepStatus } from "../workflows/wizard-shell.js";
5
7
  export { WorkflowStatusCard } from "../workflows/workflow-status-card.js";
6
8
  export type { WorkflowStatusCardProps, WorkflowStatusTone } from "../workflows/workflow-status-card.js";
@@ -1,3 +1,4 @@
1
1
  export { IntegrationStatusCard } from "../integrations/integration-status-card.js";
2
2
  export { ProcessListPanel } from "../workflows/process-list-panel.js";
3
+ export { WizardShell } from "../workflows/wizard-shell.js";
3
4
  export { WorkflowStatusCard } from "../workflows/workflow-status-card.js";
@@ -1,19 +1,27 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import * as React from "react";
3
3
  import { cn } from "../utils/cn.js";
4
+ import { hasRenderableShellSlot, ShellHeaderChrome } from "../navigation/shell-slot-rendering.js";
4
5
  import { WorkspaceHeader } from "../navigation/workspace-header.js";
5
6
  export function PageScaffold({ actions, aside, children, className, description, eyebrow, footer, headerAlign, headingLevel, maxWidth = "xl", ref, state, status, title, toolbar, ...props }) {
6
- const hasTitle = title !== undefined && title !== null && title !== false;
7
- const hasHeader = Boolean(hasTitle || actions || toolbar);
8
- const hasAside = Boolean(aside);
7
+ const hasActions = hasRenderableShellSlot(actions);
8
+ const hasAside = hasRenderableShellSlot(aside);
9
+ const hasDescription = hasRenderableShellSlot(description);
10
+ const hasEyebrow = hasRenderableShellSlot(eyebrow);
11
+ const hasFooter = hasRenderableShellSlot(footer);
12
+ const hasState = hasRenderableShellSlot(state);
13
+ const hasStatus = hasRenderableShellSlot(status);
14
+ const hasTitle = hasRenderableShellSlot(title);
15
+ const hasToolbar = hasRenderableShellSlot(toolbar);
16
+ const hasHeader = hasTitle || hasEyebrow || hasDescription || hasStatus || hasToolbar || hasActions;
9
17
  const headerProps = {
10
- ...(actions === undefined ? {} : { actions }),
11
- ...(description === undefined ? {} : { description }),
12
- ...(eyebrow === undefined ? {} : { eyebrow }),
18
+ ...(hasActions ? { actions } : {}),
19
+ ...(hasDescription ? { description } : {}),
20
+ ...(hasEyebrow ? { eyebrow } : {}),
13
21
  ...(headerAlign === undefined ? {} : { align: headerAlign }),
14
22
  ...(headingLevel === undefined ? {} : { headingLevel }),
15
- ...(status === undefined ? {} : { status }),
16
- ...(toolbar === undefined ? {} : { toolbar })
23
+ ...(hasStatus ? { status } : {}),
24
+ ...(hasToolbar ? { toolbar } : {})
17
25
  };
18
- return (_jsxs("div", { ...props, ref: ref, className: cn("zvk-composite-page-scaffold", className), "data-max-width": maxWidth, "data-with-aside": hasAside ? "true" : "false", children: [hasTitle ? (_jsx(WorkspaceHeader, { ...headerProps, className: "zvk-composite-page-scaffold__header", title: title })) : hasHeader ? (_jsxs("div", { className: "zvk-composite-page-scaffold__header zvk-composite-page-scaffold__header--controls-only", children: [toolbar ? _jsx("div", { className: "zvk-composite-page-scaffold__toolbar", children: toolbar }) : null, actions ? _jsx("div", { className: "zvk-composite-page-scaffold__actions", children: actions }) : null] })) : null, state ? _jsx("div", { className: "zvk-composite-page-scaffold__state", children: state }) : null, _jsxs("div", { className: "zvk-composite-page-scaffold__grid", children: [_jsx("main", { className: "zvk-composite-page-scaffold__main", children: children }), hasAside ? _jsx("aside", { className: "zvk-composite-page-scaffold__aside", children: aside }) : null] }), footer ? _jsx("footer", { className: "zvk-composite-page-scaffold__footer", children: footer }) : null] }));
26
+ return (_jsxs("div", { ...props, ref: ref, className: cn("zvk-composite-page-scaffold", className), "data-max-width": maxWidth, "data-with-aside": hasAside ? "true" : "false", children: [hasHeader && hasTitle ? (_jsx(WorkspaceHeader, { ...headerProps, className: "zvk-composite-page-scaffold__header", title: title })) : hasHeader ? (_jsx(ShellHeaderChrome, { actions: actions, align: headerAlign, className: "zvk-composite-page-scaffold__header", description: description, eyebrow: eyebrow, status: status, toolbar: toolbar })) : null, hasState ? _jsx("div", { className: "zvk-composite-page-scaffold__state", children: state }) : null, _jsxs("div", { className: "zvk-composite-page-scaffold__grid", children: [_jsx("main", { className: "zvk-composite-page-scaffold__main", children: children }), hasAside ? _jsx("aside", { className: "zvk-composite-page-scaffold__aside", children: aside }) : null] }), hasFooter ? _jsx("footer", { className: "zvk-composite-page-scaffold__footer", children: footer }) : null] }));
19
27
  }
@@ -20,6 +20,7 @@ export interface CommandPaletteShellProps extends Omit<React.HTMLAttributes<HTML
20
20
  groups: readonly CommandPaletteGroup[];
21
21
  label?: React.ReactNode | undefined;
22
22
  loading?: boolean | undefined;
23
+ loadingLabel?: React.ReactNode | undefined;
23
24
  onCommandSelect?: ((command: CommandPaletteCommand) => void) | undefined;
24
25
  onOpenChange: (open: boolean) => void;
25
26
  onQueryChange?: ((query: string) => void) | undefined;
@@ -28,4 +29,4 @@ export interface CommandPaletteShellProps extends Omit<React.HTMLAttributes<HTML
28
29
  query?: string | undefined;
29
30
  ref?: React.Ref<HTMLDivElement> | undefined;
30
31
  }
31
- export declare function CommandPaletteShell({ className, defaultQuery, empty, footer, groups, label, loading, onCommandSelect, onOpenChange, onQueryChange, open, placeholder, query, ref, ...props }: CommandPaletteShellProps): React.JSX.Element;
32
+ export declare function CommandPaletteShell({ className, defaultQuery, empty, footer, groups, label, loading, loadingLabel, onCommandSelect, onOpenChange, onQueryChange, open, placeholder, query, ref, ...props }: CommandPaletteShellProps): React.JSX.Element;
@@ -5,18 +5,8 @@ import * as React from "react";
5
5
  import { Command } from "@zvk/ui/command";
6
6
  import { Dialog } from "@zvk/ui/dialog";
7
7
  import { cn } from "../utils/cn.js";
8
- function useControllableValue({ defaultValue, onChange, value }) {
9
- const [uncontrolledValue, setUncontrolledValue] = React.useState(defaultValue);
10
- const resolvedValue = value ?? uncontrolledValue;
11
- const setValue = React.useCallback((nextValue) => {
12
- if (value === undefined) {
13
- setUncontrolledValue(nextValue);
14
- }
15
- onChange?.(nextValue);
16
- }, [onChange, value]);
17
- return [resolvedValue, setValue];
18
- }
19
- export function CommandPaletteShell({ className, defaultQuery = "", empty = "No commands found.", footer, groups, label = "Command menu", loading = false, onCommandSelect, onOpenChange, onQueryChange, open, placeholder = "Search commands...", query, ref, ...props }) {
8
+ import { useControllableValue } from "../utils/use-controllable-value.js";
9
+ export function CommandPaletteShell({ className, defaultQuery = "", empty = "No commands found.", footer, groups, label = "Command menu", loading = false, loadingLabel = "Loading...", onCommandSelect, onOpenChange, onQueryChange, open, placeholder = "Search commands...", query, ref, ...props }) {
20
10
  const [resolvedQuery, setResolvedQuery] = useControllableValue({
21
11
  defaultValue: defaultQuery,
22
12
  ...(onQueryChange ? { onChange: onQueryChange } : {}),
@@ -30,7 +20,7 @@ export function CommandPaletteShell({ className, defaultQuery = "", empty = "No
30
20
  onCommandSelect?.(command);
31
21
  onOpenChange(false);
32
22
  }, [onCommandSelect, onOpenChange]);
33
- return (_jsx(Dialog, { ...props, ...dialogProps, className: cn("zvk-composite-command-palette-shell", className), onOpenChange: onOpenChange, open: open, children: _jsxs(Dialog.Content, { className: "zvk-composite-command-palette-shell__content", children: [_jsx(Dialog.Title, { className: "zvk-composite-command-palette-shell__title", children: label }), _jsxs(Command, { className: "zvk-composite-command-palette-shell__command", onValueChange: setResolvedQuery, value: resolvedQuery, children: [_jsx(Command.Input, { className: "zvk-composite-command-palette-shell__search", placeholder: placeholder }), _jsxs(Command.List, { className: "zvk-composite-command-palette-shell__list", children: [loading ? (_jsx("div", { className: "zvk-composite-command-palette-shell__state", "data-state": "loading", children: "Loading..." })) : null, !loading && !hasCommands ? (_jsx("div", { className: "zvk-composite-command-palette-shell__state", "data-state": "empty", children: empty })) : null, !loading && hasCommands ? _jsx(Command.Empty, { children: empty }) : null, !loading
23
+ return (_jsx(Dialog, { ...props, ...dialogProps, className: cn("zvk-composite-command-palette-shell", className), onOpenChange: onOpenChange, open: open, children: _jsxs(Dialog.Content, { className: "zvk-composite-command-palette-shell__content", children: [_jsx(Dialog.Title, { className: "zvk-composite-command-palette-shell__title", children: label }), _jsxs(Command, { className: "zvk-composite-command-palette-shell__command", onValueChange: setResolvedQuery, value: resolvedQuery, children: [_jsx(Command.Input, { className: "zvk-composite-command-palette-shell__search", placeholder: placeholder }), _jsxs(Command.List, { className: "zvk-composite-command-palette-shell__list", children: [loading ? (_jsx("div", { className: "zvk-composite-command-palette-shell__state", "data-state": "loading", children: loadingLabel })) : null, !loading && !hasCommands ? (_jsx("div", { className: "zvk-composite-command-palette-shell__state", "data-state": "empty", children: empty })) : null, !loading && hasCommands ? _jsx(Command.Empty, { children: empty }) : null, !loading
34
24
  ? groups.map((group) => (_jsx(Command.Group, { className: "zvk-composite-command-palette-shell__group", heading: group.label, children: group.commands.map((command) => {
35
25
  const commandProps = {
36
26
  ...(command.disabled !== undefined ? { disabled: command.disabled } : {}),
@@ -6,17 +6,7 @@ import { Button } from "@zvk/ui/button";
6
6
  import { Command } from "@zvk/ui/command";
7
7
  import { Popover } from "@zvk/ui/popover";
8
8
  import { cn } from "../utils/cn.js";
9
- function useControllableValue({ defaultValue, onChange, value }) {
10
- const [uncontrolledValue, setUncontrolledValue] = React.useState(defaultValue);
11
- const resolvedValue = value ?? uncontrolledValue;
12
- const setValue = React.useCallback((nextValue) => {
13
- if (value === undefined) {
14
- setUncontrolledValue(nextValue);
15
- }
16
- onChange?.(nextValue);
17
- }, [onChange, value]);
18
- return [resolvedValue, setValue];
19
- }
9
+ import { useControllableValue } from "../utils/use-controllable-value.js";
20
10
  function hasRenderableNode(value) {
21
11
  return value !== undefined && value !== null && value !== false;
22
12
  }
@@ -0,0 +1,31 @@
1
+ import * as React from "react";
2
+ export interface ResourceExplorerShellResource {
3
+ badge?: React.ReactNode | undefined;
4
+ children?: readonly ResourceExplorerShellResource[] | undefined;
5
+ description?: React.ReactNode | undefined;
6
+ disabled?: boolean | undefined;
7
+ icon?: React.ReactNode | undefined;
8
+ id: string;
9
+ label: React.ReactNode;
10
+ meta?: React.ReactNode | undefined;
11
+ textValue: string;
12
+ }
13
+ export interface ResourceExplorerShellProps extends Omit<React.HTMLAttributes<HTMLDivElement>, "onSelect"> {
14
+ activeKey?: string | undefined;
15
+ empty?: React.ReactNode | undefined;
16
+ expandedKeys?: readonly string[] | undefined;
17
+ footer?: React.ReactNode | undefined;
18
+ label: string;
19
+ loading?: boolean | undefined;
20
+ onExpandedKeysChange?: ((keys: string[]) => void) | undefined;
21
+ onItemAction?: ((id: string, item: ResourceExplorerShellResource) => void) | undefined;
22
+ onSelectedKeysChange?: ((keys: string[]) => void) | undefined;
23
+ preview?: React.ReactNode | undefined;
24
+ ref?: React.Ref<HTMLDivElement> | undefined;
25
+ resultItems?: readonly ResourceExplorerShellResource[] | undefined;
26
+ search?: React.ReactNode | undefined;
27
+ selectedKeys?: readonly string[] | undefined;
28
+ toolbar?: React.ReactNode | undefined;
29
+ treeItems: readonly ResourceExplorerShellResource[];
30
+ }
31
+ export declare function ResourceExplorerShell({ activeKey, className, empty, expandedKeys, footer, label, loading, onExpandedKeysChange, onItemAction, onSelectedKeysChange, preview, ref, resultItems, search, selectedKeys, toolbar, treeItems, ...props }: ResourceExplorerShellProps): React.JSX.Element;
@@ -0,0 +1,30 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import * as React from "react";
4
+ import { GridList } from "@zvk/ui/grid-list";
5
+ import { ListRow } from "@zvk/ui/list-row";
6
+ import { TreeView } from "@zvk/ui/tree-view";
7
+ import { cn } from "../utils/cn.js";
8
+ function renderResourceRow(item, selected) {
9
+ return (_jsxs(ListRow, { selected: selected, variant: "plain", children: [item.icon ? _jsx(ListRow.Leading, { children: item.icon }) : null, _jsxs(ListRow.Content, { children: [_jsx(ListRow.Title, { children: item.label }), item.description ? _jsx(ListRow.Description, { children: item.description }) : null] }), item.meta ? _jsx(ListRow.Meta, { children: item.meta }) : null, item.badge ? _jsx(ListRow.Trailing, { children: item.badge }) : null] }));
10
+ }
11
+ export function ResourceExplorerShell({ activeKey, className, empty = "No resources found.", expandedKeys, footer, label, loading = false, onExpandedKeysChange, onItemAction, onSelectedKeysChange, preview, ref, resultItems, search, selectedKeys, toolbar, treeItems, ...props }) {
12
+ const records = resultItems ?? treeItems;
13
+ const selectedProps = {
14
+ ...(selectedKeys === undefined ? {} : { selectedKeys }),
15
+ ...(onSelectedKeysChange === undefined ? {} : { onSelectedKeysChange })
16
+ };
17
+ const treeProps = {
18
+ ...(activeKey === undefined ? {} : { activeKey }),
19
+ ...(expandedKeys === undefined ? { defaultExpandedKeys: treeItems.slice(0, 1).map((item) => item.id) } : {}),
20
+ ...(expandedKeys === undefined ? {} : { expandedKeys }),
21
+ ...(onExpandedKeysChange === undefined ? {} : { onExpandedKeysChange }),
22
+ ...(onItemAction === undefined ? {} : { onItemAction }),
23
+ ...selectedProps
24
+ };
25
+ const gridProps = {
26
+ ...(onItemAction === undefined ? {} : { onAction: onItemAction }),
27
+ ...selectedProps
28
+ };
29
+ return (_jsxs("div", { ...props, ref: ref, className: cn("zvk-composite-resource-explorer-shell", className), "data-with-preview": preview ? "true" : undefined, children: [_jsxs("aside", { className: "zvk-composite-resource-explorer-shell__tree", children: [_jsx("div", { className: "zvk-composite-resource-explorer-shell__tree-header", children: _jsx("strong", { children: label }) }), search ? _jsx("div", { className: "zvk-composite-resource-explorer-shell__search", children: search }) : null, _jsx(TreeView, { ...treeProps, "aria-label": `${label} tree`, getItemChildren: (item) => item.children, getItemId: (item) => item.id, getItemLabel: (item) => item.textValue, isItemDisabled: (item) => item.disabled === true, items: treeItems, renderItem: (item) => (_jsxs("span", { className: "zvk-composite-resource-explorer-shell__tree-item", children: [item.icon ? _jsx("span", { className: "zvk-composite-resource-explorer-shell__tree-item-icon", children: item.icon }) : null, _jsx("span", { children: item.label })] })), selectionMode: "single" }), footer ? _jsx("div", { className: "zvk-composite-resource-explorer-shell__footer", children: footer }) : null] }), _jsxs("section", { className: "zvk-composite-resource-explorer-shell__results", "aria-label": `${label} results`, children: [toolbar ? _jsx("div", { className: "zvk-composite-resource-explorer-shell__toolbar", children: toolbar }) : null, _jsx(GridList, { ...gridProps, "aria-label": `${label} resources`, emptyState: empty, getItemId: (item) => item.id, getItemTextValue: (item) => item.textValue, isItemDisabled: (item) => item.disabled === true, items: records, loading: loading, loadingState: "Loading resources...", renderItem: (item, state) => renderResourceRow(item, state.selected), selectionMode: "single" })] }), preview ? _jsx("aside", { className: "zvk-composite-resource-explorer-shell__preview", children: preview }) : null] }));
30
+ }
@@ -2,26 +2,33 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import * as React from "react";
3
3
  import { SectionedSidebarNav } from "@zvk/ui/sectioned-sidebar-nav";
4
4
  import { cn } from "../utils/cn.js";
5
+ import { hasRenderableShellSlot, ShellHeaderChrome } from "./shell-slot-rendering.js";
5
6
  import { WorkspaceHeader } from "./workspace-header.js";
6
7
  export function SectionedWorkspaceShell({ actions, activeItemId, children, className, description, footer, headingLevel, navLabel = "Workspace navigation", onItemSelect, ref, sections, sidebarFooter, title, toolbar, ...props }) {
7
- const hasHeader = Boolean(title || description || actions || toolbar);
8
+ const hasActions = hasRenderableShellSlot(actions);
9
+ const hasDescription = hasRenderableShellSlot(description);
10
+ const hasFooter = hasRenderableShellSlot(footer);
11
+ const hasSidebarFooter = hasRenderableShellSlot(sidebarFooter);
12
+ const hasTitle = hasRenderableShellSlot(title);
13
+ const hasToolbar = hasRenderableShellSlot(toolbar);
14
+ const hasHeader = hasTitle || hasDescription || hasActions || hasToolbar;
8
15
  const navigationProps = {
9
16
  ...(activeItemId === undefined ? {} : { activeItemId }),
10
17
  ...(onItemSelect === undefined ? {} : { onItemSelect })
11
18
  };
12
19
  const headerProps = {
13
- ...(actions === undefined ? {} : { actions }),
14
- ...(description === undefined ? {} : { description }),
20
+ ...(hasActions ? { actions } : {}),
21
+ ...(hasDescription ? { description } : {}),
15
22
  ...(headingLevel === undefined ? {} : { headingLevel }),
16
- ...(toolbar === undefined ? {} : { toolbar })
23
+ ...(hasToolbar ? { toolbar } : {})
17
24
  };
18
- return (_jsxs("div", { ...props, ref: ref, className: cn("zvk-composite-sectioned-workspace-shell", className), children: [_jsxs("aside", { className: "zvk-composite-sectioned-workspace-shell__sidebar", children: [_jsx(SectionedWorkspaceNavigation, { ...navigationProps, navLabel: navLabel, sections: sections }), sidebarFooter ? (_jsx("div", { className: "zvk-composite-sectioned-workspace-shell__sidebar-footer", children: sidebarFooter })) : null] }), _jsxs("div", { className: "zvk-composite-sectioned-workspace-shell__body", children: [hasHeader ? (_jsx(WorkspaceHeader, { ...headerProps, className: "zvk-composite-sectioned-workspace-shell__header", title: title ?? "" })) : null, _jsx("main", { className: "zvk-composite-sectioned-workspace-shell__content", children: children }), footer ? _jsx("footer", { className: "zvk-composite-sectioned-workspace-shell__footer", children: footer }) : null] })] }));
25
+ return (_jsxs("div", { ...props, ref: ref, className: cn("zvk-composite-sectioned-workspace-shell", className), children: [_jsxs("aside", { className: "zvk-composite-sectioned-workspace-shell__sidebar", children: [_jsx(SectionedWorkspaceNavigation, { ...navigationProps, navLabel: navLabel, sections: sections }), hasSidebarFooter ? (_jsx("div", { className: "zvk-composite-sectioned-workspace-shell__sidebar-footer", children: sidebarFooter })) : null] }), _jsxs("div", { className: "zvk-composite-sectioned-workspace-shell__body", children: [hasHeader && hasTitle ? (_jsx(WorkspaceHeader, { ...headerProps, className: "zvk-composite-sectioned-workspace-shell__header", title: title })) : hasHeader ? (_jsx(ShellHeaderChrome, { actions: actions, className: "zvk-composite-sectioned-workspace-shell__header", description: description, toolbar: toolbar })) : null, _jsx("main", { className: "zvk-composite-sectioned-workspace-shell__content", children: children }), hasFooter ? _jsx("footer", { className: "zvk-composite-sectioned-workspace-shell__footer", children: footer }) : null] })] }));
19
26
  }
20
27
  function SectionedWorkspaceNavigation({ activeItemId, navLabel, onItemSelect, sections }) {
21
28
  return (_jsx(SectionedSidebarNav, { className: "zvk-composite-sectioned-workspace-shell__nav", label: navLabel, children: sections.map((section) => (_jsxs(SectionedSidebarNav.Section, { children: [_jsx(SectionedSidebarNav.SectionTitle, { children: section.label }), _jsx(SectionedSidebarNav.List, { children: section.items.map((item) => (_jsx(SectionedWorkspaceNavigationItem, { active: item.id === activeItemId, item: item, onItemSelect: onItemSelect }, item.id))) })] }, section.id))) }));
22
29
  }
23
30
  function SectionedWorkspaceNavigationItem({ active, item, onItemSelect }) {
24
- const content = (_jsxs("span", { className: "zvk-composite-sectioned-workspace-shell__nav-item-content", children: [_jsx("span", { className: "zvk-composite-sectioned-workspace-shell__nav-item-label", children: item.label }), item.description ? (_jsx("span", { className: "zvk-composite-sectioned-workspace-shell__nav-item-description", children: item.description })) : null] }));
31
+ const content = (_jsxs("span", { className: "zvk-composite-sectioned-workspace-shell__nav-item-content", children: [_jsx("span", { className: "zvk-composite-sectioned-workspace-shell__nav-item-label", children: item.label }), hasRenderableShellSlot(item.description) ? (_jsx("span", { className: "zvk-composite-sectioned-workspace-shell__nav-item-description", children: item.description })) : null] }));
25
32
  const handleSelect = () => onItemSelect?.(item.id);
26
33
  const itemProps = {
27
34
  ...(item.badge === undefined ? {} : { badge: item.badge }),
@@ -0,0 +1,13 @@
1
+ import * as React from "react";
2
+ import type { WorkspaceHeaderAlign } from "./workspace-header.js";
3
+ export declare function hasRenderableShellSlot(value: React.ReactNode): boolean;
4
+ export interface ShellHeaderChromeProps {
5
+ actions?: React.ReactNode | undefined;
6
+ align?: WorkspaceHeaderAlign | undefined;
7
+ className?: string | undefined;
8
+ description?: React.ReactNode | undefined;
9
+ eyebrow?: React.ReactNode | undefined;
10
+ status?: React.ReactNode | undefined;
11
+ toolbar?: React.ReactNode | undefined;
12
+ }
13
+ export declare function ShellHeaderChrome({ actions, align, className, description, eyebrow, status, toolbar }: ShellHeaderChromeProps): React.JSX.Element | null;
@@ -0,0 +1,25 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import * as React from "react";
3
+ import { cn } from "../utils/cn.js";
4
+ export function hasRenderableShellSlot(value) {
5
+ if (value === undefined || value === null || typeof value === "boolean") {
6
+ return false;
7
+ }
8
+ if (typeof value === "string") {
9
+ return value.trim().length > 0;
10
+ }
11
+ return true;
12
+ }
13
+ export function ShellHeaderChrome({ actions, align = "start", className, description, eyebrow, status, toolbar }) {
14
+ const hasActions = hasRenderableShellSlot(actions);
15
+ const hasDescription = hasRenderableShellSlot(description);
16
+ const hasEyebrow = hasRenderableShellSlot(eyebrow);
17
+ const hasStatus = hasRenderableShellSlot(status);
18
+ const hasToolbar = hasRenderableShellSlot(toolbar);
19
+ const hasContent = hasEyebrow || hasStatus || hasDescription;
20
+ const hasControls = hasToolbar || hasActions;
21
+ if (!hasContent && !hasControls) {
22
+ return null;
23
+ }
24
+ return (_jsxs("div", { className: cn("zvk-composite-workspace-header", className), "data-align": align, children: [hasContent ? (_jsxs("div", { className: "zvk-composite-workspace-header__content", children: [hasEyebrow || hasStatus ? (_jsxs("div", { className: "zvk-composite-workspace-header__kicker", children: [hasEyebrow ? _jsx("span", { className: "zvk-composite-workspace-header__eyebrow", children: eyebrow }) : null, hasStatus ? _jsx("span", { className: "zvk-composite-workspace-header__status", children: status }) : null] })) : null, hasDescription ? (_jsx("div", { className: "zvk-composite-workspace-header__body", children: _jsx("p", { className: "zvk-composite-workspace-header__description", children: description }) })) : null] })) : (_jsx("div", { className: "zvk-composite-workspace-header__content", "aria-hidden": "true" })), hasControls ? (_jsxs("div", { className: "zvk-composite-workspace-header__controls", children: [hasToolbar ? _jsx("div", { className: "zvk-composite-workspace-header__toolbar", children: toolbar }) : null, hasActions ? _jsx("div", { className: "zvk-composite-workspace-header__actions", children: actions }) : null] })) : null] }));
25
+ }
@@ -0,0 +1,16 @@
1
+ import * as React from "react";
2
+ import type { WorkspaceHeaderHeadingLevel } from "./workspace-header.js";
3
+ export interface SimpleWorkspaceShellProps extends Omit<React.HTMLAttributes<HTMLDivElement>, "title"> {
4
+ actions?: React.ReactNode | undefined;
5
+ aside?: React.ReactNode | undefined;
6
+ children?: React.ReactNode | undefined;
7
+ description?: React.ReactNode | undefined;
8
+ footer?: React.ReactNode | undefined;
9
+ headingLevel?: WorkspaceHeaderHeadingLevel | undefined;
10
+ navigation?: React.ReactNode | undefined;
11
+ ref?: React.Ref<HTMLDivElement> | undefined;
12
+ status?: React.ReactNode | undefined;
13
+ title?: React.ReactNode | undefined;
14
+ toolbar?: React.ReactNode | undefined;
15
+ }
16
+ export declare function SimpleWorkspaceShell({ actions, aside, children, className, description, footer, headingLevel, navigation, ref, status, title, toolbar, ...props }: SimpleWorkspaceShellProps): React.JSX.Element;
@@ -0,0 +1,25 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import * as React from "react";
4
+ import { cn } from "../utils/cn.js";
5
+ import { hasRenderableShellSlot, ShellHeaderChrome } from "./shell-slot-rendering.js";
6
+ import { WorkspaceHeader } from "./workspace-header.js";
7
+ export function SimpleWorkspaceShell({ actions, aside, children, className, description, footer, headingLevel, navigation, ref, status, title, toolbar, ...props }) {
8
+ const hasActions = hasRenderableShellSlot(actions);
9
+ const hasAside = hasRenderableShellSlot(aside);
10
+ const hasDescription = hasRenderableShellSlot(description);
11
+ const hasFooter = hasRenderableShellSlot(footer);
12
+ const hasNavigation = hasRenderableShellSlot(navigation);
13
+ const hasStatus = hasRenderableShellSlot(status);
14
+ const hasTitle = hasRenderableShellSlot(title);
15
+ const hasToolbar = hasRenderableShellSlot(toolbar);
16
+ const hasHeader = hasTitle || hasDescription || hasStatus || hasToolbar || hasActions;
17
+ const headerProps = {
18
+ ...(hasActions ? { actions } : {}),
19
+ ...(hasDescription ? { description } : {}),
20
+ ...(headingLevel === undefined ? {} : { headingLevel }),
21
+ ...(hasStatus ? { status } : {}),
22
+ ...(hasToolbar ? { toolbar } : {})
23
+ };
24
+ return (_jsxs("div", { ...props, ref: ref, className: cn("zvk-composite-simple-workspace-shell", className), "data-with-aside": hasAside ? "true" : undefined, "data-with-navigation": hasNavigation ? "true" : undefined, children: [hasNavigation ? (_jsx("nav", { className: "zvk-composite-simple-workspace-shell__navigation", "aria-label": "Workspace navigation", children: navigation })) : null, _jsxs("div", { className: "zvk-composite-simple-workspace-shell__body", children: [hasHeader && hasTitle ? (_jsx(WorkspaceHeader, { ...headerProps, className: "zvk-composite-simple-workspace-shell__header", title: title })) : hasHeader ? (_jsx(ShellHeaderChrome, { actions: actions, className: "zvk-composite-simple-workspace-shell__header", description: description, status: status, toolbar: toolbar })) : null, _jsxs("div", { className: "zvk-composite-simple-workspace-shell__grid", children: [_jsx("main", { className: "zvk-composite-simple-workspace-shell__main", children: children }), hasAside ? _jsx("aside", { className: "zvk-composite-simple-workspace-shell__aside", children: aside }) : null] }), hasFooter ? (_jsx("footer", { className: "zvk-composite-simple-workspace-shell__footer", children: footer })) : null] })] }));
25
+ }
@@ -0,0 +1,24 @@
1
+ import * as React from "react";
2
+ import type { SplitterValue } from "@zvk/ui/splitter";
3
+ import type { WorkspaceHeaderHeadingLevel } from "./workspace-header.js";
4
+ export interface SplitWorkspaceShellProps extends Omit<React.HTMLAttributes<HTMLDivElement>, "defaultValue" | "title"> {
5
+ actions?: React.ReactNode | undefined;
6
+ collapsiblePrimary?: boolean | undefined;
7
+ defaultValue?: SplitterValue | undefined;
8
+ description?: React.ReactNode | undefined;
9
+ footer?: React.ReactNode | undefined;
10
+ headingLevel?: WorkspaceHeaderHeadingLevel | undefined;
11
+ maxPrimarySize?: number | undefined;
12
+ minPrimarySize?: number | undefined;
13
+ onResizeEnd?: ((value: SplitterValue) => void) | undefined;
14
+ onValueChange?: ((value: SplitterValue) => void) | undefined;
15
+ primary: React.ReactNode;
16
+ primaryLabel?: string | undefined;
17
+ ref?: React.Ref<HTMLDivElement> | undefined;
18
+ secondary: React.ReactNode;
19
+ secondaryLabel?: string | undefined;
20
+ title?: React.ReactNode | undefined;
21
+ toolbar?: React.ReactNode | undefined;
22
+ value?: SplitterValue | undefined;
23
+ }
24
+ export declare function SplitWorkspaceShell({ actions, className, collapsiblePrimary, defaultValue, description, footer, headingLevel, maxPrimarySize, minPrimarySize, onResizeEnd, onValueChange, primary, primaryLabel, ref, secondary, secondaryLabel, title, toolbar, value, ...props }: SplitWorkspaceShellProps): React.JSX.Element;
@@ -0,0 +1,32 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import * as React from "react";
4
+ import { Splitter } from "@zvk/ui/splitter";
5
+ import { cn } from "../utils/cn.js";
6
+ import { hasRenderableShellSlot, ShellHeaderChrome } from "./shell-slot-rendering.js";
7
+ import { WorkspaceHeader } from "./workspace-header.js";
8
+ export function SplitWorkspaceShell({ actions, className, collapsiblePrimary = false, defaultValue = [32, 68], description, footer, headingLevel, maxPrimarySize, minPrimarySize = 18, onResizeEnd, onValueChange, primary, primaryLabel = "Primary workspace pane", ref, secondary, secondaryLabel = "Secondary workspace pane", title, toolbar, value, ...props }) {
9
+ const hasActions = hasRenderableShellSlot(actions);
10
+ const hasDescription = hasRenderableShellSlot(description);
11
+ const hasFooter = hasRenderableShellSlot(footer);
12
+ const hasTitle = hasRenderableShellSlot(title);
13
+ const hasToolbar = hasRenderableShellSlot(toolbar);
14
+ const hasHeader = hasTitle || hasDescription || hasToolbar || hasActions;
15
+ const splitterProps = {
16
+ defaultValue,
17
+ ...(onResizeEnd === undefined ? {} : { onResizeEnd }),
18
+ ...(onValueChange === undefined ? {} : { onValueChange }),
19
+ ...(value === undefined ? {} : { value })
20
+ };
21
+ const primaryPanelProps = {
22
+ ...(maxPrimarySize === undefined ? {} : { maxSize: maxPrimarySize }),
23
+ minSize: minPrimarySize
24
+ };
25
+ const headerProps = {
26
+ ...(hasActions ? { actions } : {}),
27
+ ...(hasDescription ? { description } : {}),
28
+ ...(headingLevel === undefined ? {} : { headingLevel }),
29
+ ...(hasToolbar ? { toolbar } : {})
30
+ };
31
+ return (_jsxs("div", { ...props, ref: ref, className: cn("zvk-composite-split-workspace-shell", className), children: [hasHeader && hasTitle ? (_jsx(WorkspaceHeader, { ...headerProps, className: "zvk-composite-split-workspace-shell__header", title: title })) : hasHeader ? (_jsx(ShellHeaderChrome, { actions: actions, className: "zvk-composite-split-workspace-shell__header", description: description, toolbar: toolbar })) : null, _jsxs(Splitter, { ...splitterProps, className: "zvk-composite-split-workspace-shell__splitter", children: [_jsx(Splitter.Panel, { ...primaryPanelProps, "aria-label": primaryLabel, className: "zvk-composite-split-workspace-shell__pane", collapsible: collapsiblePrimary, children: primary }), _jsx(Splitter.Handle, { "aria-label": `Resize ${primaryLabel}` }), _jsx(Splitter.Panel, { "aria-label": secondaryLabel, className: "zvk-composite-split-workspace-shell__pane zvk-composite-split-workspace-shell__pane--secondary", children: secondary })] }), hasFooter ? (_jsx("footer", { className: "zvk-composite-split-workspace-shell__footer", children: footer })) : null] }));
32
+ }
package/dist/styles.css CHANGED
@@ -666,6 +666,95 @@
666
666
  }
667
667
 
668
668
 
669
+ /* src/navigation/resource-explorer-shell.css */
670
+ @layer zvk-composite-components {
671
+ :where(.zvk-composite-resource-explorer-shell) {
672
+ color: var(--zvk-ui-color-foreground);
673
+ container: zvk-composite-resource-explorer-shell / inline-size;
674
+ display: grid;
675
+ font-family: var(--zvk-ui-font-family-primary);
676
+ gap: var(--zvk-ui-space-4);
677
+ grid-template-columns: minmax(14rem, 18rem) minmax(0, 1fr) minmax(16rem, 22rem);
678
+ min-block-size: 0;
679
+ min-inline-size: 0;
680
+ }
681
+
682
+ :where(.zvk-composite-resource-explorer-shell:not([data-with-preview="true"])) {
683
+ grid-template-columns: minmax(14rem, 18rem) minmax(0, 1fr);
684
+ }
685
+
686
+ :where(.zvk-composite-resource-explorer-shell__tree),
687
+ :where(.zvk-composite-resource-explorer-shell__results),
688
+ :where(.zvk-composite-resource-explorer-shell__preview) {
689
+ background: var(--zvk-ui-color-surface);
690
+ border: 1px solid var(--zvk-ui-color-border);
691
+ border-radius: var(--zvk-ui-radius-md);
692
+ box-shadow: var(--zvk-ui-shadow-xs);
693
+ display: grid;
694
+ gap: var(--zvk-ui-space-3);
695
+ min-block-size: 0;
696
+ min-inline-size: 0;
697
+ padding: var(--zvk-ui-space-3);
698
+ }
699
+
700
+ :where(.zvk-composite-resource-explorer-shell__tree-header),
701
+ :where(.zvk-composite-resource-explorer-shell__toolbar),
702
+ :where(.zvk-composite-resource-explorer-shell__footer) {
703
+ color: var(--zvk-ui-color-muted-foreground);
704
+ font-size: var(--zvk-ui-font-size-sm);
705
+ }
706
+
707
+ :where(.zvk-composite-resource-explorer-shell__tree-header) {
708
+ color: var(--zvk-ui-color-foreground);
709
+ font-family: var(--zvk-ui-font-family-secondary);
710
+ }
711
+
712
+ :where(.zvk-composite-resource-explorer-shell__search) {
713
+ align-items: center;
714
+ display: flex;
715
+ flex-wrap: wrap;
716
+ gap: var(--zvk-ui-space-2);
717
+ min-inline-size: min(100%, 12rem);
718
+ }
719
+
720
+ :where(.zvk-composite-resource-explorer-shell__search > *) {
721
+ flex: 1 1 12rem;
722
+ min-inline-size: 0;
723
+ }
724
+
725
+ :where(.zvk-composite-resource-explorer-shell__search :is(input, select, textarea, .zvk-ui-input, .zvk-ui-select__trigger)) {
726
+ inline-size: 100%;
727
+ max-inline-size: 100%;
728
+ }
729
+
730
+ :where(.zvk-composite-resource-explorer-shell__tree-item) {
731
+ align-items: center;
732
+ display: inline-flex;
733
+ gap: var(--zvk-ui-space-2);
734
+ min-inline-size: 0;
735
+ }
736
+
737
+ :where(.zvk-composite-resource-explorer-shell__tree-item-icon) {
738
+ color: var(--zvk-ui-color-muted-foreground);
739
+ display: inline-flex;
740
+ }
741
+
742
+ @container zvk-composite-resource-explorer-shell (max-width: 72rem) {
743
+ :where(.zvk-composite-resource-explorer-shell),
744
+ :where(.zvk-composite-resource-explorer-shell:not([data-with-preview="true"])) {
745
+ grid-template-columns: 1fr;
746
+ }
747
+ }
748
+
749
+ @media (max-width: 72rem) {
750
+ :where(.zvk-composite-resource-explorer-shell),
751
+ :where(.zvk-composite-resource-explorer-shell:not([data-with-preview="true"])) {
752
+ grid-template-columns: 1fr;
753
+ }
754
+ }
755
+ }
756
+
757
+
669
758
  /* src/navigation/sectioned-workspace-shell.css */
670
759
  @layer zvk-composite-components {
671
760
  :where(.zvk-composite-sectioned-workspace-shell) {
@@ -786,6 +875,130 @@
786
875
  }
787
876
 
788
877
 
878
+ /* src/navigation/simple-workspace-shell.css */
879
+ @layer zvk-composite-components {
880
+ :where(.zvk-composite-simple-workspace-shell) {
881
+ color: var(--zvk-ui-color-foreground);
882
+ container: zvk-composite-simple-workspace-shell / inline-size;
883
+ display: grid;
884
+ font-family: var(--zvk-ui-font-family-primary);
885
+ gap: var(--zvk-ui-space-5);
886
+ grid-template-columns: minmax(12rem, 16rem) minmax(0, 1fr);
887
+ min-inline-size: 0;
888
+ }
889
+
890
+ :where(.zvk-composite-simple-workspace-shell:not([data-with-navigation="true"])) {
891
+ grid-template-columns: minmax(0, 1fr);
892
+ }
893
+
894
+ :where(.zvk-composite-simple-workspace-shell__navigation),
895
+ :where(.zvk-composite-simple-workspace-shell__aside) {
896
+ background: var(--zvk-ui-color-surface);
897
+ border: 1px solid var(--zvk-ui-color-border);
898
+ border-radius: var(--zvk-ui-radius-md);
899
+ box-shadow: var(--zvk-ui-shadow-xs);
900
+ min-inline-size: 0;
901
+ padding: var(--zvk-ui-space-3);
902
+ }
903
+
904
+ :where(.zvk-composite-simple-workspace-shell__body) {
905
+ display: grid;
906
+ gap: var(--zvk-ui-space-5);
907
+ min-inline-size: 0;
908
+ }
909
+
910
+ :where(.zvk-composite-simple-workspace-shell__header) {
911
+ border-block-end: 1px solid var(--zvk-ui-color-border);
912
+ padding-block-end: var(--zvk-ui-space-4);
913
+ }
914
+
915
+ :where(.zvk-composite-simple-workspace-shell__grid) {
916
+ align-items: start;
917
+ display: grid;
918
+ gap: var(--zvk-ui-space-5);
919
+ grid-template-columns: minmax(0, 1fr);
920
+ min-inline-size: 0;
921
+ }
922
+
923
+ :where(.zvk-composite-simple-workspace-shell[data-with-aside="true"] .zvk-composite-simple-workspace-shell__grid) {
924
+ grid-template-columns: minmax(0, 1fr) minmax(15rem, 22rem);
925
+ }
926
+
927
+ :where(.zvk-composite-simple-workspace-shell__main) {
928
+ display: grid;
929
+ gap: var(--zvk-ui-space-4);
930
+ min-inline-size: 0;
931
+ }
932
+
933
+ :where(.zvk-composite-simple-workspace-shell__footer) {
934
+ border-block-start: 1px solid var(--zvk-ui-color-border);
935
+ color: var(--zvk-ui-color-muted-foreground);
936
+ font-size: var(--zvk-ui-font-size-sm);
937
+ padding-block-start: var(--zvk-ui-space-4);
938
+ }
939
+
940
+ @container zvk-composite-simple-workspace-shell (max-width: 64rem) {
941
+ :where(.zvk-composite-simple-workspace-shell),
942
+ :where(.zvk-composite-simple-workspace-shell[data-with-aside="true"] .zvk-composite-simple-workspace-shell__grid) {
943
+ grid-template-columns: 1fr;
944
+ }
945
+ }
946
+
947
+ @media (max-width: 64rem) {
948
+ :where(.zvk-composite-simple-workspace-shell),
949
+ :where(.zvk-composite-simple-workspace-shell[data-with-aside="true"] .zvk-composite-simple-workspace-shell__grid) {
950
+ grid-template-columns: 1fr;
951
+ }
952
+ }
953
+ }
954
+
955
+
956
+ /* src/navigation/split-workspace-shell.css */
957
+ @layer zvk-composite-components {
958
+ :where(.zvk-composite-split-workspace-shell) {
959
+ color: var(--zvk-ui-color-foreground);
960
+ display: grid;
961
+ font-family: var(--zvk-ui-font-family-primary);
962
+ gap: var(--zvk-ui-space-5);
963
+ min-block-size: 0;
964
+ min-inline-size: 0;
965
+ }
966
+
967
+ :where(.zvk-composite-split-workspace-shell__header) {
968
+ border-block-end: 1px solid var(--zvk-ui-color-border);
969
+ padding-block-end: var(--zvk-ui-space-4);
970
+ }
971
+
972
+ :where(.zvk-composite-split-workspace-shell__splitter) {
973
+ min-block-size: 28rem;
974
+ }
975
+
976
+ :where(.zvk-composite-split-workspace-shell__pane) {
977
+ background: var(--zvk-ui-color-surface);
978
+ border: 1px solid var(--zvk-ui-color-border);
979
+ border-radius: var(--zvk-ui-radius-md);
980
+ box-shadow: var(--zvk-ui-shadow-xs);
981
+ min-block-size: 0;
982
+ min-inline-size: 0;
983
+ overflow: auto;
984
+ padding: var(--zvk-ui-space-4);
985
+ }
986
+
987
+ :where(.zvk-composite-split-workspace-shell__pane--secondary) {
988
+ background:
989
+ linear-gradient(180deg, color-mix(in srgb, var(--zvk-ui-color-primary) 5%, transparent), transparent 12rem),
990
+ var(--zvk-ui-color-surface);
991
+ }
992
+
993
+ :where(.zvk-composite-split-workspace-shell__footer) {
994
+ border-block-start: 1px solid var(--zvk-ui-color-border);
995
+ color: var(--zvk-ui-color-muted-foreground);
996
+ font-size: var(--zvk-ui-font-size-sm);
997
+ padding-block-start: var(--zvk-ui-space-4);
998
+ }
999
+ }
1000
+
1001
+
789
1002
  /* src/navigation/workspace-header.css */
790
1003
  @layer zvk-composite-components {
791
1004
  :where(.zvk-composite-workspace-header) {
@@ -2009,6 +2222,45 @@
2009
2222
  }
2010
2223
 
2011
2224
 
2225
+ /* src/workflows/wizard-shell.css */
2226
+ @layer zvk-composite-components {
2227
+ :where(.zvk-composite-wizard-shell) {
2228
+ color: var(--zvk-ui-color-foreground);
2229
+ display: grid;
2230
+ font-family: var(--zvk-ui-font-family-primary);
2231
+ gap: var(--zvk-ui-space-5);
2232
+ min-inline-size: 0;
2233
+ }
2234
+
2235
+ :where(.zvk-composite-wizard-shell__header),
2236
+ :where(.zvk-composite-wizard-shell__progress),
2237
+ :where(.zvk-composite-wizard-shell__footer) {
2238
+ border-block-end: 1px solid var(--zvk-ui-color-border);
2239
+ padding-block-end: var(--zvk-ui-space-4);
2240
+ }
2241
+
2242
+ :where(.zvk-composite-wizard-shell__content) {
2243
+ background: var(--zvk-ui-color-surface);
2244
+ border: 1px solid var(--zvk-ui-color-border);
2245
+ border-radius: var(--zvk-ui-radius-md);
2246
+ box-shadow: var(--zvk-ui-shadow-xs);
2247
+ display: grid;
2248
+ gap: var(--zvk-ui-space-4);
2249
+ min-inline-size: 0;
2250
+ padding: var(--zvk-ui-space-5);
2251
+ }
2252
+
2253
+ :where(.zvk-composite-wizard-shell__footer) {
2254
+ border-block-end: 0;
2255
+ border-block-start: 1px solid var(--zvk-ui-color-border);
2256
+ color: var(--zvk-ui-color-muted-foreground);
2257
+ font-size: var(--zvk-ui-font-size-sm);
2258
+ padding-block-end: 0;
2259
+ padding-block-start: var(--zvk-ui-space-4);
2260
+ }
2261
+ }
2262
+
2263
+
2012
2264
  /* src/settings/parameter-editor.css */
2013
2265
  @layer zvk-composite-components {
2014
2266
  :where(.zvk-composite-parameter-editor) {
@@ -0,0 +1,7 @@
1
+ interface UseControllableValueOptions<Value> {
2
+ defaultValue: Value;
3
+ onChange?: ((value: Value) => void) | undefined;
4
+ value?: Value | undefined;
5
+ }
6
+ export declare function useControllableValue<Value>({ defaultValue, onChange, value }: UseControllableValueOptions<Value>): readonly [Value, (nextValue: Value) => void];
7
+ export {};
@@ -0,0 +1,12 @@
1
+ import * as React from "react";
2
+ export function useControllableValue({ defaultValue, onChange, value }) {
3
+ const [uncontrolledValue, setUncontrolledValue] = React.useState(defaultValue);
4
+ const resolvedValue = value ?? uncontrolledValue;
5
+ const setValue = React.useCallback((nextValue) => {
6
+ if (value === undefined) {
7
+ setUncontrolledValue(nextValue);
8
+ }
9
+ onChange?.(nextValue);
10
+ }, [onChange, value]);
11
+ return [resolvedValue, setValue];
12
+ }
@@ -0,0 +1,24 @@
1
+ import * as React from "react";
2
+ import type { WorkspaceHeaderHeadingLevel } from "../navigation/workspace-header.js";
3
+ export type WizardShellStepStatus = "idle" | "current" | "complete" | "error" | "disabled";
4
+ export interface WizardShellStep {
5
+ description?: React.ReactNode | undefined;
6
+ disabled?: boolean | undefined;
7
+ id: string;
8
+ label: React.ReactNode;
9
+ status?: WizardShellStepStatus | undefined;
10
+ }
11
+ export interface WizardShellProps extends Omit<React.HTMLAttributes<HTMLDivElement>, "onSubmit" | "title"> {
12
+ actions?: React.ReactNode | undefined;
13
+ children: React.ReactNode;
14
+ currentStepId: string;
15
+ description?: React.ReactNode | undefined;
16
+ footer?: React.ReactNode | undefined;
17
+ headingLevel?: WorkspaceHeaderHeadingLevel | undefined;
18
+ onStepChange?: ((stepId: string) => void) | undefined;
19
+ progress?: React.ReactNode | undefined;
20
+ ref?: React.Ref<HTMLDivElement> | undefined;
21
+ steps: readonly WizardShellStep[];
22
+ title: React.ReactNode;
23
+ }
24
+ export declare function WizardShell({ actions, children, className, currentStepId, description, footer, headingLevel, onStepChange, progress, ref, steps, title, ...props }: WizardShellProps): React.JSX.Element;
@@ -0,0 +1,32 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import * as React from "react";
4
+ import { Stepper } from "@zvk/ui/stepper";
5
+ import { cn } from "../utils/cn.js";
6
+ import { WorkspaceHeader } from "../navigation/workspace-header.js";
7
+ function toStepperItem(step) {
8
+ return {
9
+ description: step.description,
10
+ disabled: step.disabled === true || step.status === "disabled",
11
+ id: step.id,
12
+ label: step.label
13
+ };
14
+ }
15
+ function isLinear(steps, currentStepId) {
16
+ const currentIndex = steps.findIndex((step) => step.id === currentStepId);
17
+ return steps.every((step, index) => {
18
+ if (step.status === "complete" || step.status === "current") {
19
+ return index <= currentIndex;
20
+ }
21
+ return true;
22
+ });
23
+ }
24
+ export function WizardShell({ actions, children, className, currentStepId, description, footer, headingLevel, onStepChange, progress, ref, steps, title, ...props }) {
25
+ const stepperProps = {
26
+ ...(onStepChange === undefined ? {} : { onValueChange: onStepChange })
27
+ };
28
+ const headerProps = {
29
+ ...(headingLevel === undefined ? {} : { headingLevel })
30
+ };
31
+ return (_jsxs("div", { ...props, ref: ref, className: cn("zvk-composite-wizard-shell", className), children: [_jsx(WorkspaceHeader, { ...headerProps, actions: actions, className: "zvk-composite-wizard-shell__header", description: description, title: title }), _jsx("div", { className: "zvk-composite-wizard-shell__progress", children: progress ?? (_jsx(Stepper, { ...stepperProps, "aria-label": "Wizard progress", interactive: Boolean(onStepChange), items: steps.map(toStepperItem), linear: isLinear(steps, currentStepId), value: currentStepId })) }), _jsx("main", { className: "zvk-composite-wizard-shell__content", children: children }), footer ? _jsx("footer", { className: "zvk-composite-wizard-shell__footer", children: footer }) : null] }));
32
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zvk/composite",
3
- "version": "0.1.3",
3
+ "version": "0.1.4",
4
4
  "description": "Drop-in composite React components built from @zvk/ui primitives for product workspaces.",
5
5
  "private": false,
6
6
  "type": "module",
@@ -49,11 +49,26 @@
49
49
  "import": "./dist/navigation/command-palette-shell.js",
50
50
  "default": "./dist/navigation/command-palette-shell.js"
51
51
  },
52
+ "./resource-explorer-shell": {
53
+ "types": "./dist/navigation/resource-explorer-shell.d.ts",
54
+ "import": "./dist/navigation/resource-explorer-shell.js",
55
+ "default": "./dist/navigation/resource-explorer-shell.js"
56
+ },
52
57
  "./sectioned-workspace-shell": {
53
58
  "types": "./dist/navigation/sectioned-workspace-shell.d.ts",
54
59
  "import": "./dist/navigation/sectioned-workspace-shell.js",
55
60
  "default": "./dist/navigation/sectioned-workspace-shell.js"
56
61
  },
62
+ "./simple-workspace-shell": {
63
+ "types": "./dist/navigation/simple-workspace-shell.d.ts",
64
+ "import": "./dist/navigation/simple-workspace-shell.js",
65
+ "default": "./dist/navigation/simple-workspace-shell.js"
66
+ },
67
+ "./split-workspace-shell": {
68
+ "types": "./dist/navigation/split-workspace-shell.d.ts",
69
+ "import": "./dist/navigation/split-workspace-shell.js",
70
+ "default": "./dist/navigation/split-workspace-shell.js"
71
+ },
57
72
  "./workspace-header": {
58
73
  "types": "./dist/navigation/workspace-header.d.ts",
59
74
  "import": "./dist/navigation/workspace-header.js",
@@ -129,6 +144,11 @@
129
144
  "import": "./dist/workflows/process-list-panel.js",
130
145
  "default": "./dist/workflows/process-list-panel.js"
131
146
  },
147
+ "./wizard-shell": {
148
+ "types": "./dist/workflows/wizard-shell.d.ts",
149
+ "import": "./dist/workflows/wizard-shell.js",
150
+ "default": "./dist/workflows/wizard-shell.js"
151
+ },
132
152
  "./parameter-editor": {
133
153
  "types": "./dist/settings/parameter-editor.d.ts",
134
154
  "import": "./dist/settings/parameter-editor.js",
@@ -197,7 +217,7 @@
197
217
  "react-dom": "^19.0.0"
198
218
  },
199
219
  "dependencies": {
200
- "@zvk/ui": "^0.1.11"
220
+ "@zvk/ui": "^0.1.12"
201
221
  },
202
222
  "tsd": {
203
223
  "directory": "tests/types"