@loykin/designkit 0.0.3 → 0.0.5

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.
@@ -2,11 +2,15 @@
2
2
 
3
3
  The Playground's **Guides** group contains complete product flows. Existing template groups remain visual API references; they do not receive an AI guide merely because a workflow uses that template.
4
4
 
5
- | Workflow | Pattern ID | Connected destinations | Contract |
6
- | ------------------- | --------------------- | -------------------------------------- | ----------------------------------------------- |
7
- | Resource Management | `managed-table` | table → detail Sheet; create/edit page | [Managed Table](./managed-table.md) |
8
- | Publishing | `publishing-workflow` | blog collection → article route | [Publishing Workflow](./publishing-workflow.md) |
9
- | Commerce | `commerce-workflow` | filtered catalog product route | [Commerce Workflow](./commerce-workflow.md) |
5
+ Read [AI UI Implementation Contract](./ai-ui-implementation-contract.md) before implementing any Guide screen — it defines the mandatory build order (template → slot → component → variant → minimal custom CSS) that every Guide below assumes.
6
+
7
+ | Workflow | Pattern ID | Connected destinations | Contract |
8
+ | ------------------- | ---------------------- | -------------------------------------- | ------------------------------------------------- |
9
+ | Resource Management | `managed-table` | tabledetail SidePanel; create/edit page | [Managed Table](./managed-table.md) |
10
+ | Operations | `kubernetes-workspace` | Pod table → detail panel → tool dock | [Kubernetes Workspace](./kubernetes-workspace.md) |
11
+ | Forms | `form-workflow` | create, edit, and settings pages | [Stacked Form](./form-workflow.md) |
12
+ | Publishing | `publishing-workflow` | blog collection → article route | [Publishing Workflow](./publishing-workflow.md) |
13
+ | Commerce | `commerce-workflow` | filtered catalog → product route | [Commerce Workflow](./commerce-workflow.md) |
10
14
 
11
15
  ## How to use these guides
12
16
 
@@ -0,0 +1,52 @@
1
+ # AI UI Implementation Contract
2
+
3
+ This contract applies to every Guide in `docs/guides/`. Read it once, then read the specific Guide for the workflow you are building. It exists because an executable Guide demo teaches by example — an AI session copies what the code *does* more strongly than what the prose *says*. If a Guide's TSX is full of one-off Tailwind, that reads as "styling is done ad hoc here," no matter what the Markdown claims.
4
+
5
+ ## Mandatory implementation order
6
+
7
+ 1. Select the closest executable Guide (`docs/guides/manifest.json`).
8
+ 2. Use the page template that Guide documents.
9
+ 3. Fill the template's documented slots before creating wrapper layouts.
10
+ 4. Use existing DesignKit or bundled shadcn components.
11
+ 5. Use an existing named variant for a supported visual difference.
12
+ 6. If a visual difference is reusable, add or extend a named component or variant — see `AGENTS.md`'s Implementation Priority.
13
+ 7. Use custom CSS only when the requirement is content-specific, integration-specific, and cannot be represented by an existing template, slot, component, or variant.
14
+
15
+ Custom CSS is not forbidden. It must not read as the default way to build a screen.
16
+
17
+ ## Forbidden patterns
18
+
19
+ - Reimplementing page-level layout (reading width, article typography, card elevation, section spacing) with raw `<div>` + Tailwind when a DesignKit component or variant already expresses it.
20
+ - Overriding padding, width, typography hierarchy, or responsive behavior that a template already owns.
21
+ - Storing Tailwind utility strings in domain/sample data (e.g. `accent: 'from-violet-500/80 ...'`). Store a semantic value (`accent: 'violet'`) and map it through a component or variant instead — an AI session should never need to invent gradient utility combinations.
22
+ - Copying the same hand-rolled visual pattern into more than one Guide. The second occurrence is the signal to promote it to a component (see `InteractiveCard`, born from the same hover/elevation block appearing in four demos).
23
+
24
+ ## Classifying a `className`/`style` you're about to write
25
+
26
+ - **Promote to a DesignKit template-level component** if the pattern is reusable across pages regardless of domain: reading width, article typography, metadata rows, table of contents, loading skeletons, elevation/hover behavior, cover/hero layout.
27
+ - **Promote to a named variant** if it's a limited, enumerable visual choice on an existing component: tone, density, elevation, content width, presentation mode.
28
+ - **Keep as local custom CSS** only when all of the following hold: it is content- or integration-specific; it cannot be expressed by an existing template, slot, component, or variant; it does not repeat in another Guide; it does not touch template-owned layout. Justify it inline:
29
+
30
+ ```tsx
31
+ /**
32
+ * Guide exception:
33
+ * This third-party renderer requires an explicit container height.
34
+ * Page spacing, typography, and responsive layout remain owned by DesignKit.
35
+ */
36
+ ```
37
+
38
+ Custom styling with no such comment should be removed or promoted the next time it's touched.
39
+
40
+ ## Reconstruction checklist (add to every Guide)
41
+
42
+ - [ ] Existing template slots are used before custom wrapper layouts
43
+ - [ ] Existing DesignKit or shadcn variants/components are used before custom classes
44
+ - [ ] Reusable visual differences are implemented as named components or variants, not repeated `className` blocks
45
+ - [ ] Custom CSS is local, minimal, and explicitly justified with a `Guide exception` comment
46
+ - [ ] No Tailwind utility strings are stored in domain data
47
+ - [ ] Template-owned spacing, width, typography, and responsive behavior are not overridden
48
+
49
+ ## Precedent
50
+
51
+ - `ArticleCover`, `ArticleByline`, `ArticleToc`, `ArticleBody`, `ArticleBodySkeleton` (`@loykin/designkit`) — extracted from the Publishing guide's hand-rolled article markup; a `tone: ArticleTone` prop replaces raw gradient strings in sample data.
52
+ - `InteractiveCard` (`@loykin/designkit`) — the hover-lift-card treatment duplicated across the Publishing, Commerce, Browse, and Blog Feed demos, promoted to one component.
@@ -12,6 +12,8 @@ Use this guide to build a discovery-to-decision commerce experience. The executa
12
12
 
13
13
  The existing **Browse / Catalog** and product detail template demos remain visual references. This Guide defines their behavioral connection.
14
14
 
15
+ Read [AI UI Implementation Contract](./ai-ui-implementation-contract.md) first. Product cards use DesignKit `InteractiveCard` for the clickable, hover-lifting shell — the same component the Publishing guide uses — not a hand-rolled `<div className="... hover:shadow-md ...">`.
16
+
15
17
  ## Route contract
16
18
 
17
19
  ```text
@@ -45,3 +47,4 @@ Selecting a product card navigates to the product route. Browser Back and the pr
45
47
  - [ ] Product breadcrumb returns to catalog
46
48
  - [ ] Purchase action exists only on the product route
47
49
  - [ ] No nested page templates and no product Sheet
50
+ - [ ] Product cards use `InteractiveCard`, not a hand-rolled hover/elevation `className` block
@@ -0,0 +1,202 @@
1
+ # Stacked Form Workflow Contract for AI
2
+
3
+ Use this contract for create, edit, and settings forms. It standardizes the visual structure and component boundaries. It does not prescribe a form-state, validation, routing, or data-fetching library.
4
+
5
+ ## Canonical reference
6
+
7
+ - Pattern ID: `form-workflow`
8
+ - Playground: **Guides / Forms / Stacked Form**
9
+ - Page template: `DataBodyTemplate`
10
+ - Section primitive: `DataBodyTemplate.Group layout="stacked"`
11
+
12
+ The existing **DataBodyTemplate / Form / Horizontal**, **Stacked**, and **Inline** entries are visual API references. When generating a product form, follow this guide instead of selecting among those demos.
13
+
14
+ Read [AI UI Implementation Contract](./ai-ui-implementation-contract.md) first.
15
+
16
+ The executable Playground example uses React Hook Form to demonstrate shared state across modular section components, controlled DesignKit inputs, and field-level errors. React Hook Form is a Playground implementation choice, not a DesignKit dependency or part of this visual contract.
17
+
18
+ ## Non-negotiable visual contract
19
+
20
+ 1. Render the form as a full route page by default. Do not use a Sheet for ordinary create, edit, or settings work.
21
+ 2. Keep the route hierarchy in `PageTopBar`: for example, `Resources / Members / Add member`.
22
+ 3. Use one `DataBodyTemplate` page root.
23
+ 4. Use the stacked form shape for create, edit, and settings pages. Do not switch to `horizontal` or `inline` because the domain changed.
24
+ 5. Divide the form into one or more `DataBodyTemplate.Group layout="stacked"` sections according to meaning, not visual preference.
25
+ 6. Wrap each field in `FormField` (label, control, and `error`/`helperText`) instead of hand-rolling a `space-y-1.5` + `Label` block; keep the fields inside a group at `space-y-3`.
26
+ 7. Use DesignKit controls and the established compact control sizing: `h-8 text-sm` for inputs and selects.
27
+ 8. End the form in `FormActions` (`status`, `submitLabel`, `onCancel`) instead of hand-rolling the bottom action row — it already puts Cancel before the right-aligned primary submit action and renders the divider above it. Do not add a second divider around it.
28
+ 9. Pass validation text to `FormField`'s `error` prop; pass form-level failure to `FormActions`' `status` prop, inside the same form boundary.
29
+ 10. A submitting, refreshing, or validation state must not replace or flash the page header or unrelated groups.
30
+
31
+ ## Module boundary contract
32
+
33
+ Treat each semantic group like tab-scoped content: implement it as a named component instead of one large page function.
34
+
35
+ - The route component owns only `DataBodyTemplate`, breadcrumb, title, and route-level description.
36
+ - A form component owns the submit boundary and bottom actions.
37
+ - Each section component returns one stacked `DataBodyTemplate.Group`.
38
+ - Section-only state and asynchronous work stay in that section component when possible.
39
+ - Shared form state may be provided by the form component through props, context, React Hook Form, or another application-selected mechanism.
40
+ - Do not lift section-only pending, error, or refresh state into the page header.
41
+ - Do not nest another page-level template inside the form or a Group.
42
+
43
+ Separating components is a behavior boundary, not permission to change their layout. Every section still uses the same stacked Group contract.
44
+
45
+ ## Save boundaries
46
+
47
+ The visual system remains the same; only form ownership changes.
48
+
49
+ | Workflow | Form ownership | Actions |
50
+ | -------------------------------------------- | ---------------------------------------- | --------------------------------------------------------------- |
51
+ | Create or edit one record | One form owns all stacked sections | One bottom action row after the final section |
52
+ | Settings saved as one document | One form owns all stacked sections | One bottom action row after the final section |
53
+ | Settings with independently saved categories | Each section component owns its own form | Each stacked section ends with its own right-aligned action row |
54
+
55
+ Do not create separate forms merely because the screen contains multiple Groups. Split forms only when the server-side save boundaries are genuinely independent.
56
+
57
+ ## Canonical composition
58
+
59
+ The following is the concrete React Hook Form composition used by the Playground. Applications may replace React Hook Form, but must preserve the same component and visual boundaries.
60
+
61
+ ```tsx
62
+ import { Controller, FormProvider, useForm, useFormContext } from 'react-hook-form'
63
+
64
+ interface MemberFormValues {
65
+ name: string
66
+ email: string
67
+ role: string
68
+ active: boolean
69
+ }
70
+
71
+ function IdentitySection() {
72
+ const {
73
+ register,
74
+ formState: { errors },
75
+ } = useFormContext<MemberFormValues>()
76
+
77
+ return (
78
+ <DataBodyTemplate.Group
79
+ layout="stacked"
80
+ title="Identity"
81
+ description="Basic account information."
82
+ >
83
+ <div className="space-y-3">
84
+ <FormField label="Name" htmlFor="member-name" error={errors.name?.message}>
85
+ <Input
86
+ id="member-name"
87
+ {...register('name', { required: 'Enter a member name.' })}
88
+ aria-invalid={Boolean(errors.name)}
89
+ className="h-8 text-sm"
90
+ />
91
+ </FormField>
92
+ </div>
93
+ </DataBodyTemplate.Group>
94
+ )
95
+ }
96
+
97
+ function AccessSection() {
98
+ const { control } = useFormContext<MemberFormValues>()
99
+
100
+ return (
101
+ <DataBodyTemplate.Group
102
+ layout="stacked"
103
+ title="Role & access"
104
+ description="Default workspace permissions."
105
+ >
106
+ <div className="space-y-3">
107
+ <FormField label="Role" htmlFor="member-role">
108
+ <Controller
109
+ control={control}
110
+ name="role"
111
+ render={({ field }) => (
112
+ <Select value={field.value} onValueChange={field.onChange}>
113
+ <SelectTrigger id="member-role" className="h-8 text-sm">
114
+ <SelectValue />
115
+ </SelectTrigger>
116
+ <SelectContent>{/* roles */}</SelectContent>
117
+ </Select>
118
+ )}
119
+ />
120
+ </FormField>
121
+ <Controller
122
+ control={control}
123
+ name="active"
124
+ render={({ field }) => <Switch checked={field.value} onCheckedChange={field.onChange} />}
125
+ />
126
+ </div>
127
+ </DataBodyTemplate.Group>
128
+ )
129
+ }
130
+
131
+ function MemberForm() {
132
+ const form = useForm<MemberFormValues>({
133
+ defaultValues: {
134
+ name: '',
135
+ email: '',
136
+ role: 'viewer',
137
+ active: true,
138
+ },
139
+ })
140
+
141
+ const submitMember = (values: MemberFormValues) => {
142
+ // Application-owned mutation or submit behavior.
143
+ }
144
+
145
+ return (
146
+ <FormProvider {...form}>
147
+ <form className="contents" onSubmit={form.handleSubmit(submitMember)}>
148
+ <IdentitySection />
149
+ <AccessSection />
150
+ <FormActions submitLabel="Save" />
151
+ </form>
152
+ </FormProvider>
153
+ )
154
+ }
155
+
156
+ export function MemberCreatePage() {
157
+ return (
158
+ <DataBodyTemplate
159
+ topBar={<PageTopBar left="Resources / Members / Add member" />}
160
+ title="Add member"
161
+ description="Create a workspace member."
162
+ >
163
+ <MemberForm />
164
+ </DataBodyTemplate>
165
+ )
166
+ }
167
+ ```
168
+
169
+ React Hook Form is used here to prove that independently implemented Group components can share one form boundary. It remains a Playground-only dependency. Replacing it with ordinary React state, Formik, or another form tool must not change the rendered structure.
170
+
171
+ ## Settings with independent saves
172
+
173
+ When settings categories save independently, retain the same stacked appearance and move each form boundary into its section component.
174
+
175
+ ```tsx
176
+ function ProfileSettingsSection() {
177
+ return (
178
+ <DataBodyTemplate.Group layout="stacked" title="Profile">
179
+ <form className="space-y-3" onSubmit={saveProfile}>
180
+ {/* profile fields */}
181
+ <div className="flex justify-end">
182
+ <Button type="submit" size="sm" className="h-8 text-xs">
183
+ Save profile
184
+ </Button>
185
+ </div>
186
+ </form>
187
+ </DataBodyTemplate.Group>
188
+ )
189
+ }
190
+ ```
191
+
192
+ ## Review checklist
193
+
194
+ - Create, edit, and settings screens all use the stacked shape.
195
+ - The page is a route rather than a Sheet unless the task explicitly requires a constrained secondary edit.
196
+ - Breadcrumb, title, Groups, fields, and actions follow the same vertical order.
197
+ - Groups are named components and unrelated section state is not owned by the route header.
198
+ - Group count follows the information model; it does not select a different layout.
199
+ - Form and validation libraries remain application choices.
200
+ - Bottom actions belong to the form that they submit.
201
+ - Fields use `FormField`, not a hand-rolled `space-y-1.5` + `Label` block.
202
+ - The bottom action row uses `FormActions`, not a hand-rolled Cancel/submit `div`.
@@ -0,0 +1,169 @@
1
+ # Kubernetes Workspace Contract for AI
2
+
3
+ Use this contract for operational resource browsers where people repeatedly inspect a collection, compare resource state, and open logs or a shell without losing list context.
4
+
5
+ The executable reference is available at:
6
+
7
+ - `/sidebar/kubernetes-workspace-guide`
8
+ - `/header/kubernetes-workspace-guide`
9
+
10
+ The `DataBodyTemplate / Kubernetes` entry is a visual API reference backed by the same composition. The **Guides / Operations / Kubernetes Workspace** entry is the normative workflow contract.
11
+
12
+ Read [AI UI Implementation Contract](./ai-ui-implementation-contract.md) first.
13
+
14
+ ## 1. Workflow identity
15
+
16
+ This is one workspace with three distinct interaction layers:
17
+
18
+ 1. The resource table owns discovery, search, filters, refresh, sorting, and selection context.
19
+ 2. A right SidePanel owns concise inspection of the selected resource.
20
+ 3. A bottom Control Dock owns persistent work such as logs, shell sessions, and cluster events.
21
+
22
+ Do not flatten these concerns into one table component. Keep query state, selected-resource inspection, and long-running tools independently replaceable.
23
+
24
+ ## 2. Required composition
25
+
26
+ - Use one `DataBodyTemplate` as the only page-level template.
27
+ - Put GridKit `GlobalSearch` and a BaseKit `FilterInput` (`type: 'select'`, keyed to the namespace column) in the DataGrid header slots. GridKit's own `SelectFilter`/`MultiSelectFilter` do not expose a `size`/`className` prop, so they cannot be sized to match `GlobalSearch`; `FilterInput` can.
28
+ - Use BaseKit `SidePanelProvider` with DesignKit `PanelTemplate` for row inspection. Put `Shell`/`Logs` (and any other panel-opening actions) in `PanelTemplate`'s `actions` slot next to the title, not in `footer` — footer is for closing/committing the panel, not for opening other tools. Render `Close` as a plain icon button (no confirmation) since dismissing a read-only inspection panel is not a destructive action.
29
+ - Use BaseKit `ControlBarProvider` as the tab and persistence model for the bottom dock.
30
+ - Keep Kubernetes API clients, authorization, watches, and mutations in the consuming application.
31
+ - Keep GridKit, `FilterInput`, SidePanel, and Control Bar as application or Playground integrations; they are not DesignKit runtime dependencies.
32
+
33
+ ## 3. Resource collection boundary
34
+
35
+ Create a named resource component such as `PodsResource`. It owns:
36
+
37
+ - collection query parameters and result rendering;
38
+ - search and filter state;
39
+ - column definitions and row actions;
40
+ - opening the selected resource in the SidePanel;
41
+ - opening resource tools in the Control Dock.
42
+
43
+ The route or workspace component owns only cluster context, page hierarchy, providers, and layout. It must not absorb every table and panel implementation detail.
44
+
45
+ For server-backed data, put search, namespace, sorting, pagination or cursor, and refresh inputs into one query model. Preserve the previous successful result during background refresh when possible. Show initial loading, empty, filtered-empty, permission failure, and refresh failure as distinct states.
46
+
47
+ ## 4. Row interaction contract
48
+
49
+ Clicking a Pod row opens a read-oriented detail SidePanel. It must not implicitly open logs or start a shell.
50
+
51
+ The detail panel should show enough information to decide the next action:
52
+
53
+ - status, readiness, restarts, and age;
54
+ - node, owner/controller, and QoS class;
55
+ - namespace and stable resource name;
56
+ - explicit `Logs` and `Shell` actions.
57
+
58
+ Use a full detail route only when the resource has deep navigation, editable configuration, history, or a stable URL that users must share. Use the SidePanel for quick inspection that should preserve table position and filters.
59
+
60
+ Row-level Logs and Shell icon buttons may remain for expert access. They must stop row-click propagation and have accessible names.
61
+
62
+ ## 5. Control Dock contract
63
+
64
+ Logs, shell sessions, and cluster events are persistent workspace tools, not transient popovers.
65
+
66
+ - Keep the dock visible at the bottom even with zero tabs.
67
+ - The empty dock is 36px tall and communicates `No active resource panels`.
68
+ - Opening a tool expands the dock to its working height.
69
+ - The dock participates in the workspace flex layout. It must reduce the table region rather than overlap or cover rows.
70
+ - Closing the last tab returns to the empty dock; it does not remove the dock.
71
+ - Deduplicate tools by tool type and resource identity. Reopening an existing Logs or Shell tool activates its tab.
72
+ - Collapsing preserves tabs and active state.
73
+ - Events are cluster-scoped; Logs and Shell are resource-scoped.
74
+
75
+ The Control Bar package supplies both the tab model (`useControlBar`) and the tab strip chrome (`ControlBar`) — render `<ControlBar />` for the dock header and content instead of hand-building a tab strip. The application still owns domain-specific tool renderers (via `registerTabType`) and any dock action beyond the package's own collapse/expand/fullscreen controls (e.g. an "Events" trigger).
76
+
77
+ Pass `alwaysVisible` and `emptyState` so the dock stays mounted at zero tabs instead of unmounting: `<ControlBar alwaysVisible emptyState="No active resource panels" />`. Do not hand-build an empty-state wrapper around `ControlBar` — the package owns this directly.
78
+
79
+ ## 6. Spacing and resizing
80
+
81
+ - The table region must remain `min-height: 0` so it can shrink when the dock expands.
82
+ - The SidePanel must be mounted inside the workspace provider boundary, not around the app shell.
83
+ - The content area's bottom padding must equal its horizontal page padding. A table border touching the viewport edge reads as clipped even when it technically fits.
84
+ - Do not add a second page shell, header, or navigation sidebar inside the demo.
85
+ - Verify the same composition in both SidebarShell and HeaderShell.
86
+
87
+ ## 7. Actions and state
88
+
89
+ Refresh belongs in the table header because it reloads the collection. Search and namespace filtering belong together without decorative separators that imply unrelated groups.
90
+
91
+ Do not add aggregate status counters merely to fill header space. Add a summary only when it supports an operational decision and is driven by the same query scope as the table.
92
+
93
+ For real clusters:
94
+
95
+ - disable or hide Shell when the user lacks permission;
96
+ - make container selection explicit for multi-container Pods;
97
+ - stream Logs and Shell through cancellable connections;
98
+ - announce connection, reconnecting, and terminal states;
99
+ - clean up watches and streams when their dock tab closes;
100
+ - avoid persisting credentials or log contents in local storage.
101
+
102
+ ## 8. Reference shape
103
+
104
+ ```tsx
105
+ function PodsResource() {
106
+ const controlBar = useControlBar()
107
+ const sidePanel = useSidePanel()
108
+ const query = usePodsQuery()
109
+
110
+ const inspectPod = (pod: Pod) => {
111
+ sidePanel.open(
112
+ <PodDetailPanel
113
+ pod={pod}
114
+ onOpenLogs={() => openPodTool(controlBar, 'logs', pod)}
115
+ onOpenShell={() => openPodTool(controlBar, 'shell', pod)}
116
+ />,
117
+ { side: 'right', size: 420, resizable: true },
118
+ )
119
+ }
120
+
121
+ return (
122
+ <DataGrid
123
+ data={query.data}
124
+ columns={columns}
125
+ onRowClick={inspectPod}
126
+ headerLeft={(table) => (
127
+ <>
128
+ <GlobalSearch table={table} />
129
+ <FilterInput
130
+ config={namespaceFilterConfig}
131
+ value={(table.getColumn('namespace')?.getFilterValue() as string) ?? null}
132
+ onChange={(next) => table.getColumn('namespace')?.setFilterValue(next ?? undefined)}
133
+ />
134
+ </>
135
+ )}
136
+ />
137
+ )
138
+ }
139
+
140
+ function KubernetesWorkspace() {
141
+ return (
142
+ <ControlBarProvider persistKey="kubernetes-workspace">
143
+ <SidePanelProvider className="h-full min-h-0">
144
+ <div className="flex h-full min-h-0 flex-col overflow-hidden">
145
+ <DataBodyTemplate>{/* PodsResource */}</DataBodyTemplate>
146
+ <ControlBar alwaysVisible emptyState="No active resource panels" />
147
+ </div>
148
+ </SidePanelProvider>
149
+ </ControlBarProvider>
150
+ )
151
+ }
152
+ ```
153
+
154
+ ## 9. Review checklist
155
+
156
+ - [ ] There is exactly one page-level template and one app shell.
157
+ - [ ] Search is GridKit `GlobalSearch` and the namespace control is a BaseKit `FilterInput`, both in the table header.
158
+ - [ ] `GlobalSearch`, `FilterInput`, and any header `Button`s render at the same height and corner radius — verify computed style, not just class names, since GridKit/BaseKit and DesignKit resolve `--radius` independently.
159
+ - [ ] Row click opens a Pod detail SidePanel.
160
+ - [ ] Logs and Shell are explicit actions, not implicit row-click behavior.
161
+ - [ ] Logs, Shell, and any other panel-opening actions live in `PanelTemplate`'s `actions` slot next to the title; `footer` is reserved for closing/committing, and `Close` is a plain icon button, not a confirmation.
162
+ - [ ] Row action buttons stop propagation and have accessible names.
163
+ - [ ] Detail, collection query, and dock tools have separate component/state boundaries.
164
+ - [ ] Tool tabs deduplicate by type and resource identity.
165
+ - [ ] The empty Control Dock remains visible after the final tab closes.
166
+ - [ ] Expanding the dock reflows the table and never overlays it.
167
+ - [ ] Bottom content padding equals horizontal content padding.
168
+ - [ ] Loading, empty, permission, refresh failure, and stream failure states are defined.
169
+ - [ ] The SidebarShell and HeaderShell routes both show exactly one navigation shell.