@flood-md/agent 0.2.10 → 0.2.11
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/DESIGN.md +8 -5
- package/README.md +1 -1
- package/SKILL.md +11 -0
- package/dist/catalog.json +1875 -295
- package/dist/materials/component-app-shell.md +47 -0
- package/dist/materials/component-app-top-bar.md +36 -0
- package/dist/materials/component-autocomplete.md +54 -0
- package/dist/materials/component-bulk-action-bar.md +52 -0
- package/dist/materials/component-button.md +1 -0
- package/dist/materials/component-card.md +2 -2
- package/dist/materials/component-collapsible.md +1 -0
- package/dist/materials/component-collection-toolbar.md +68 -0
- package/dist/materials/component-collection-view-switcher.md +49 -0
- package/dist/materials/component-combobox.md +10 -0
- package/dist/materials/component-command-palette.md +53 -0
- package/dist/materials/component-copy-button.md +1 -0
- package/dist/materials/component-data-table.md +14 -2
- package/dist/materials/component-date-picker.md +1 -0
- package/dist/materials/component-date-range-picker.md +1 -0
- package/dist/materials/component-date-time-picker.md +2 -0
- package/dist/materials/component-date-time-range-picker.md +4 -0
- package/dist/materials/component-drawer.md +1 -0
- package/dist/materials/component-figure.md +1 -1
- package/dist/materials/component-filter-condition.md +53 -0
- package/dist/materials/component-filter-group.md +55 -0
- package/dist/materials/component-icon-button.md +2 -0
- package/dist/materials/component-menubar.md +41 -0
- package/dist/materials/component-multi-select.md +10 -0
- package/dist/materials/component-navigation-group.md +51 -0
- package/dist/materials/component-navigation-item.md +59 -0
- package/dist/materials/component-navigation-tree.md +8 -0
- package/dist/materials/component-overflow-actions.md +47 -0
- package/dist/materials/component-page-header.md +40 -0
- package/dist/materials/component-popover.md +2 -1
- package/dist/materials/component-radio-card.md +2 -2
- package/dist/materials/component-segmented-control.md +6 -4
- package/dist/materials/component-sidebar.md +64 -0
- package/dist/materials/component-tabs.md +2 -0
- package/dist/materials/component-text.md +2 -2
- package/dist/materials/component-toggle.md +1 -0
- package/dist/materials/component-toolbar.md +4 -0
- package/dist/materials/component-tooltip.md +1 -1
- package/dist/materials/guide-agent-knowledge.md +13 -6
- package/dist/materials/guide-agents.md +8 -1
- package/dist/materials/guide-foundations.md +45 -2
- package/dist/materials/guide-installation.md +6 -6
- package/dist/materials/guide-interface-content.md +1 -1
- package/dist/materials/guide-introduction.md +4 -2
- package/dist/materials/guide-layout.md +39 -10
- package/dist/materials/guide-motion.md +69 -0
- package/dist/materials/guide-skills.md +9 -0
- package/dist/materials/recipe-collection-views.md +648 -0
- package/dist/materials/recipe-command-palette.md +31 -52
- package/dist/materials/recipe-filter-bar.md +12 -3
- package/dist/materials/recipe-grouped-form.md +5 -0
- package/dist/materials/recipe-navigation-workspace.md +222 -0
- package/dist/materials/recipe-record-management.md +296 -0
- package/dist/materials/recipe-record-workspace.md +207 -0
- package/dist/materials/recipe-settings-workspace.md +232 -0
- package/dist/materials/token-sys.color.context.base.action-disabled.json +41 -0
- package/dist/materials/token-sys.color.context.base.action-hover.json +41 -0
- package/dist/materials/token-sys.color.context.base.action-pressed.json +41 -0
- package/dist/materials/token-sys.color.context.base.action-rest.json +41 -0
- package/dist/materials/token-sys.color.context.base.avatar-background.json +41 -0
- package/dist/materials/token-sys.color.context.base.border-decorative.json +41 -0
- package/dist/materials/token-sys.color.context.base.divider-decorative.json +41 -0
- package/dist/materials/token-sys.color.context.base.field-background.json +41 -0
- package/dist/materials/token-sys.color.context.base.field-border.json +41 -0
- package/dist/materials/token-sys.color.context.base.field-disabled.json +41 -0
- package/dist/materials/token-sys.color.context.base.field-readonly.json +41 -0
- package/dist/materials/token-sys.color.context.base.indicator-track.json +41 -0
- package/dist/materials/token-sys.color.context.base.loading-placeholder.json +41 -0
- package/dist/materials/token-sys.color.context.base.muted-background.json +41 -0
- package/dist/materials/token-sys.color.context.base.on-surface-action-disabled.json +41 -0
- package/dist/materials/token-sys.color.context.base.on-surface-action-hover.json +41 -0
- package/dist/materials/token-sys.color.context.base.on-surface-action-pressed.json +41 -0
- package/dist/materials/token-sys.color.context.base.on-surface-action-rest.json +41 -0
- package/dist/materials/token-sys.color.context.base.selection-background.json +41 -0
- package/dist/materials/token-sys.color.context.base.supporting-background.json +41 -0
- package/dist/materials/token-sys.color.context.filled.action-disabled.json +41 -0
- package/dist/materials/token-sys.color.context.filled.action-hover.json +41 -0
- package/dist/materials/token-sys.color.context.filled.action-pressed.json +41 -0
- package/dist/materials/token-sys.color.context.filled.action-rest.json +41 -0
- package/dist/materials/token-sys.color.context.filled.avatar-background.json +41 -0
- package/dist/materials/token-sys.color.context.filled.border-decorative.json +41 -0
- package/dist/materials/token-sys.color.context.filled.divider-decorative.json +41 -0
- package/dist/materials/token-sys.color.context.filled.field-background.json +41 -0
- package/dist/materials/token-sys.color.context.filled.field-border.json +41 -0
- package/dist/materials/token-sys.color.context.filled.field-disabled.json +41 -0
- package/dist/materials/token-sys.color.context.filled.field-readonly.json +41 -0
- package/dist/materials/token-sys.color.context.filled.indicator-track.json +41 -0
- package/dist/materials/token-sys.color.context.filled.loading-placeholder.json +41 -0
- package/dist/materials/token-sys.color.context.filled.muted-background.json +41 -0
- package/dist/materials/token-sys.color.context.filled.selection-background.json +41 -0
- package/dist/materials/token-sys.color.context.filled.supporting-background.json +41 -0
- package/dist/materials/token-sys.color.field.readonly.json +41 -0
- package/dist/materials/token-sys.color.surface.avatar.json +41 -0
- package/dist/materials/token-sys.color.surface.current.json +41 -0
- package/dist/materials/token-sys.color.surface.muted.json +41 -0
- package/dist/materials/token-sys.color.surface.supporting.json +41 -0
- package/dist/materials/token-sys.motion.distance-panel.json +36 -0
- package/dist/materials/token-sys.motion.distance-small.json +36 -0
- package/dist/materials/token-sys.motion.duration-enter.json +36 -0
- package/dist/materials/token-sys.motion.duration-exit.json +36 -0
- package/dist/materials/token-sys.motion.duration-layer.json +36 -0
- package/dist/materials/token-sys.motion.duration-loop.json +36 -0
- package/dist/materials/token-sys.motion.duration-press.json +36 -0
- package/dist/materials/token-sys.motion.easing-enter.json +38 -0
- package/dist/materials/token-sys.motion.easing-exit.json +38 -0
- package/knowledge.json +115 -2
- package/manifest.json +11 -1
- package/package.json +9 -3
- package/references/component-families.md +146 -0
- package/references/composition.md +4 -4
- package/references/customization.md +14 -0
- package/references/flood-composition/SKILL.md +4 -0
- package/references/flood-data-display/SKILL.md +117 -0
- package/references/flood-feedback/SKILL.md +25 -0
- package/references/flood-forms/SKILL.md +31 -0
- package/references/flood-foundations/SKILL.md +36 -0
- package/references/flood-hierarchy/SKILL.md +23 -7
- package/references/flood-interface-content/SKILL.md +26 -5
- package/references/flood-motion/SKILL.md +44 -0
- package/references/flood-navigation/SKILL.md +195 -0
- package/references/flood-overlays/SKILL.md +35 -0
- package/references/flood-records/SKILL.md +42 -2
- package/references/flood-spacing/SKILL.md +38 -0
- package/references/flood-verification/SKILL.md +20 -0
- package/references/interface-content-examples.md +4 -0
- package/references/rendered-review.md +5 -0
- package/references/spacing.md +30 -0
- package/rules.json +94 -8
- package/workflows.json +28 -3
|
@@ -1,66 +1,45 @@
|
|
|
1
1
|
# Command palette
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
Use CommandPalette for a workspace-wide page finder or a set of searchable
|
|
4
|
+
commands. It shares filtering and keyboard behavior with CommandList and uses
|
|
5
|
+
Dialog for modal focus and dismissal. CommandPalette is available in the
|
|
6
|
+
unreleased 0.2.11 candidate.
|
|
6
7
|
|
|
7
8
|
```tsx
|
|
8
9
|
"use client";
|
|
9
10
|
|
|
10
|
-
import {
|
|
11
|
-
import { Button, CommandList, Dialog, FloodRoot } from "@flood-md/react";
|
|
12
|
-
|
|
13
|
-
export function CommandPalette({ onCommand }: { onCommand: (id: string) => void }) {
|
|
14
|
-
const [open, setOpen] = useState(false);
|
|
15
|
-
const [query, setQuery] = useState("");
|
|
16
|
-
|
|
17
|
-
function changeOpen(next: boolean) {
|
|
18
|
-
if (next) setQuery("");
|
|
19
|
-
setOpen(next);
|
|
20
|
-
}
|
|
11
|
+
import { Button, CommandPalette, FloodRoot } from "@flood-md/react";
|
|
21
12
|
|
|
13
|
+
export function WorkspaceSearch({ onNavigate }: { onNavigate: (id: string) => void }) {
|
|
22
14
|
return (
|
|
23
15
|
<FloodRoot>
|
|
24
|
-
<
|
|
25
|
-
title="
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
label
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
{ id: "recent-projects", label: "Recent projects", group: "Projects" },
|
|
38
|
-
{ id: "settings", label: "Open settings", group: "Workspace" },
|
|
39
|
-
]}
|
|
40
|
-
onAction={id => {
|
|
41
|
-
changeOpen(false);
|
|
42
|
-
onCommand(id);
|
|
43
|
-
}}
|
|
44
|
-
/>
|
|
45
|
-
</Dialog>
|
|
16
|
+
<CommandPalette
|
|
17
|
+
title="Go to page"
|
|
18
|
+
searchLabel="Search pages"
|
|
19
|
+
placeholder="Search pages…"
|
|
20
|
+
emptyMessage="No matching pages."
|
|
21
|
+
trigger={props => <Button {...props} variant="subtle">Go to page</Button>}
|
|
22
|
+
items={[
|
|
23
|
+
{ id: "projects", label: "All projects", group: "Projects" },
|
|
24
|
+
{ id: "assigned", label: "Assigned to me", group: "Projects", keywords: ["mine"] },
|
|
25
|
+
{ id: "inbox", label: "Inbox", group: "Workspace" },
|
|
26
|
+
]}
|
|
27
|
+
onAction={onNavigate}
|
|
28
|
+
/>
|
|
46
29
|
</FloodRoot>
|
|
47
30
|
);
|
|
48
31
|
}
|
|
49
32
|
```
|
|
50
33
|
|
|
51
|
-
Import the Flood stylesheet once
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
Add a global shortcut only at the application boundary, with your own editing,
|
|
64
|
-
permission and nested-layer policy. Do not display a shortcut hint until that
|
|
65
|
-
shortcut actually works. No shortcut, router, fuzzy-ranking service or command
|
|
66
|
-
permission engine is installed by this recipe.
|
|
34
|
+
Import the Flood stylesheet once. Keep the trigger inside the existing FloodRoot.
|
|
35
|
+
Translate the dialog title, search label, placeholder, close label and state
|
|
36
|
+
messages. The title and search label remain available to assistive technology.
|
|
37
|
+
Use comfortable density by default and compact density for denser workspaces.
|
|
38
|
+
|
|
39
|
+
Opening focuses the search input. Arrow keys move through available results.
|
|
40
|
+
Enter or click calls the host action and requests closure. Escape, the close
|
|
41
|
+
button and optional outside dismissal close without invoking an action. Use
|
|
42
|
+
open/onOpenChange when the host owns visibility. A local query starts from
|
|
43
|
+
defaultQuery on each opening. For a controlled query, the host owns persistence
|
|
44
|
+
and reset. Set loading while replacing results asynchronously. The application
|
|
45
|
+
owns requests, permissions, actions, routing and any global shortcut.
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
# Filter bar
|
|
2
2
|
|
|
3
|
+
For staged conditions and persisted query definitions, see
|
|
4
|
+
[Collection views](collection-views.md) and its two working consumers.
|
|
5
|
+
|
|
3
6
|
Compose existing Field, SearchInput, Select and Button. Filtering belongs to the
|
|
4
7
|
host collection, not to DataTable. This recipe filters the full local data set
|
|
5
8
|
before handing it to the table and resets page and selection when filters change.
|
|
@@ -54,6 +57,12 @@ values. Expose active filters as removable Tag components when the filter set
|
|
|
54
57
|
gets larger; do not add a duplicate generic FilterBar component just for layout.
|
|
55
58
|
|
|
56
59
|
For remote data, debounce or submit at the host boundary, cancel obsolete requests
|
|
57
|
-
and map loading/error states explicitly.
|
|
58
|
-
|
|
59
|
-
selection.
|
|
60
|
+
and map loading/error states explicitly. Use DataTable paginationMode="server" for an individual server page with explicit
|
|
61
|
+
controlled page/sort and totalRows; do not interpret its select-page checkbox as
|
|
62
|
+
server-wide selection. Table + Pagination remains available for a host-owned
|
|
63
|
+
contract. Follow recipe:record-workspace for request generations and recovery.
|
|
64
|
+
|
|
65
|
+
When reviewing a dense toolbar, compare the filled edges and the text inside
|
|
66
|
+
each control. Shared edges and consistent heights can communicate grouping
|
|
67
|
+
even with small gaps. Judge the applied criteria and the filter trigger with
|
|
68
|
+
long values and at the width where the toolbar wraps.
|
|
@@ -64,3 +64,8 @@ Independent sections use Fieldset legendSize="section" and keep one shared outer
|
|
|
64
64
|
Grid across peer groups. Field labels retain the compact inner gap; Stack owns
|
|
65
65
|
row separation. The hierarchy workflow explains why section titles and choice
|
|
66
66
|
captions are different roles.
|
|
67
|
+
|
|
68
|
+
Compare a short and a wrapped legend with the same fields. Check the visible
|
|
69
|
+
gap from the legend to its first field and from the previous group to the
|
|
70
|
+
legend. Use the [visible weight review](../layout.md#review-visible-weight)
|
|
71
|
+
before changing the shared grid or a group gap.
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
# Navigation workspace
|
|
2
|
+
|
|
3
|
+
[Open the working workspace](/playground/navigation).
|
|
4
|
+
|
|
5
|
+
The workspace compares a standalone Sidebar with a GlobalRail + ContextBar
|
|
6
|
+
composition. Both use the same Sidebar and NavigationTree owners. GlobalRail and
|
|
7
|
+
ContextBar describe their responsibilities here; they are not additional package
|
|
8
|
+
exports. The example's project, inbox and file records live in memory.
|
|
9
|
+
Use the account menu in the footer to switch between Flood Studio and Flood Labs.
|
|
10
|
+
The navigation header shares the height of the workspace toolbar. The expanded
|
|
11
|
+
Sidebar puts the service mark at the start and the collapse control at the end.
|
|
12
|
+
On the compact rail, the toggle replaces the mark on hover or keyboard focus.
|
|
13
|
+
On touch, the toggle stays visible.
|
|
14
|
+
Labs adds nested project collections for testing long labels and scrolling.
|
|
15
|
+
The host remembers the last route and expanded branches per workspace during
|
|
16
|
+
the session. It validates a remembered route against the target workspace and
|
|
17
|
+
falls back to the project overview if that destination is unavailable.
|
|
18
|
+
The **Go to page** action uses CommandPalette. It searches the
|
|
19
|
+
current workspace and expands the selected destination's parent branches.
|
|
20
|
+
On navigation, the host moves focus to the destination heading.
|
|
21
|
+
|
|
22
|
+
## Choose the hierarchy
|
|
23
|
+
|
|
24
|
+
If the sidebar has one group whose title repeats the app or workspace context,
|
|
25
|
+
use `hideLabel` and keep the group's accessible name. When showing a group title,
|
|
26
|
+
align it with the labels of its destinations.
|
|
27
|
+
|
|
28
|
+
Use one Sidebar when the application has a small destination hierarchy. Separate
|
|
29
|
+
global and contextual navigation when switching domains changes the available
|
|
30
|
+
destinations. The global rail stays flat and stable; its active module is a
|
|
31
|
+
location, while the contextual destination is a page. An open ContextBar replaces
|
|
32
|
+
the need for a second copy of its destinations in a global flyout.
|
|
33
|
+
|
|
34
|
+
On narrow hosts, limit the expanded pane to a share of available width and keep
|
|
35
|
+
the working heading and primary controls reachable. The playground condenses
|
|
36
|
+
the toolbar and keeps wide table content in the table's own scroll area.
|
|
37
|
+
|
|
38
|
+
Keep module, route, expansion and context visibility independent. Anchor the
|
|
39
|
+
visibility control in the persistent rail header. When the context is hidden,
|
|
40
|
+
hover a rail module to preview its contextual links in a flyout that fills the
|
|
41
|
+
available rail height with top and bottom insets, even for short lists.
|
|
42
|
+
The preview preserves focus and stays open while the pointer crosses into the
|
|
43
|
+
panel. Click or use the keyboard to open it for interaction. Escape returns
|
|
44
|
+
focus to the module when it owned focus. Choosing a destination closes the flyout and keeps the
|
|
45
|
+
context docked or hidden as the user last set it. Browser Back restores the route.
|
|
46
|
+
Use real links and host routing. A modified click must remain a browser action
|
|
47
|
+
rather than changing the current tab's module through an unconditional handler.
|
|
48
|
+
|
|
49
|
+
## Compose the panes
|
|
50
|
+
|
|
51
|
+
| Region or behavior | Flood owner |
|
|
52
|
+
| --- | --- |
|
|
53
|
+
| Workspace landmarks, skip target and scroll boundary | AppShell, SkipLink and ScrollArea |
|
|
54
|
+
| Shared identity, breadcrumbs and global commands | AppTopBar, Breadcrumb, Button and CommandPalette |
|
|
55
|
+
| Current page title and page actions | PageHeader |
|
|
56
|
+
| Docked rail and grouped destinations | Sidebar, NavigationGroup and NavigationTree |
|
|
57
|
+
| Temporary navigation on a narrow host | Drawer with the same destination definitions |
|
|
58
|
+
| Account commands and identity | Menu, Avatar and Button |
|
|
59
|
+
|
|
60
|
+
Use AppShell's default content scrolling for a bounded workspace. Give an
|
|
61
|
+
embedded shell an explicit block size. Choose `scroll="page"` for natural
|
|
62
|
+
document scrolling. Place a persistent top bar and footer in the shell slots,
|
|
63
|
+
and PageHeader inside main. The shell has a skip target and owns the scroll
|
|
64
|
+
boundary; the host owns routes and persisted choices.
|
|
65
|
+
|
|
66
|
+
For a single Sidebar, opt into `navigationDrawer` with a width threshold in CSS
|
|
67
|
+
pixels and a `renderContent(close)` function. AppShell observes its own width.
|
|
68
|
+
The playground uses 480 px for the classic Sidebar profile. Pass a `topBar`
|
|
69
|
+
render function to place the named trigger with the existing toolbar controls.
|
|
70
|
+
The Rail + Context example keeps its separate pane visibility and hover flyout.
|
|
71
|
+
|
|
72
|
+
Share accepted route and branch state between Sidebar and Drawer. Close the
|
|
73
|
+
Drawer on an accepted destination and focus the destination heading. Escape
|
|
74
|
+
or explicit dismissal restores focus to the trigger. When a resize removes
|
|
75
|
+
the focused dock or Drawer, AppShell transfers focus to the new trigger or
|
|
76
|
+
the work region, respectively.
|
|
77
|
+
|
|
78
|
+
This host-owned layout uses Flood components. In the 0.2.11 source candidate,
|
|
79
|
+
NavigationItem owns the shared row, NavigationGroup owns related items and their
|
|
80
|
+
optional heading, and NavigationTree composes rows into a destination hierarchy.
|
|
81
|
+
Sidebar places these owners between its fixed header and footer. The caller
|
|
82
|
+
supplies icons, identity controls and accepted route state; keep layout CSS in the
|
|
83
|
+
host and import the matching Flood stylesheet once. These candidate APIs require
|
|
84
|
+
matching installed declarations and are not added to an older npm release by
|
|
85
|
+
reading this recipe.
|
|
86
|
+
|
|
87
|
+
```tsx
|
|
88
|
+
"use client";
|
|
89
|
+
import type { ReactNode } from "react";
|
|
90
|
+
import { AppShell, AppTopBar, FloodRoot, Sidebar, type SidebarProps } from "@flood-md/react";
|
|
91
|
+
import "@flood-md/react/styles.css";
|
|
92
|
+
|
|
93
|
+
type Props = {
|
|
94
|
+
global: SidebarProps;
|
|
95
|
+
context: SidebarProps;
|
|
96
|
+
contextVisible: boolean;
|
|
97
|
+
header: ReactNode;
|
|
98
|
+
children: ReactNode;
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
export function NavigationWorkspace({
|
|
102
|
+
global, context, contextVisible, header, children,
|
|
103
|
+
}: Props) {
|
|
104
|
+
return (
|
|
105
|
+
<FloodRoot>
|
|
106
|
+
<AppShell
|
|
107
|
+
navigation={<>
|
|
108
|
+
<Sidebar {...global} collapsed collapsible={false} currentKind="location" />
|
|
109
|
+
{contextVisible && <Sidebar {...context} currentKind="page" />}
|
|
110
|
+
</>}
|
|
111
|
+
topBar={<AppTopBar start={header} />}
|
|
112
|
+
scrollOrientation="vertical"
|
|
113
|
+
>
|
|
114
|
+
{children}
|
|
115
|
+
</AppShell>
|
|
116
|
+
</FloodRoot>
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
A bounded parent can supply the height instead of `100dvh`. Reserve remaining
|
|
122
|
+
height with flex/grid rather than subtracting an assumed toolbar height: labels
|
|
123
|
+
and tooling can wrap at enlarged text. Sidebar owns the middle navigation scroll
|
|
124
|
+
viewport; AppShell owns the main content viewport. Table overflow remains inside
|
|
125
|
+
the table's owner. Header and footer do not scroll with their navigation lists.
|
|
126
|
+
|
|
127
|
+
## Compose the row before the shell
|
|
128
|
+
|
|
129
|
+
Use NavigationItem for a destination, command or disclosure rather than a local
|
|
130
|
+
button-shaped row. A destination has an href and keeps native Link behavior. A
|
|
131
|
+
command has onAction and uses Button. A branch without a destination uses one
|
|
132
|
+
full-row disclosure target; a destination with children keeps the link and the
|
|
133
|
+
disclosure as separate sibling controls inside one coherent row surface. Clicking
|
|
134
|
+
the link must not also change the branch's expansion. A disclosure must not be
|
|
135
|
+
nested inside the link.
|
|
136
|
+
|
|
137
|
+
Current destination, ancestor of the current destination, accepted expansion and
|
|
138
|
+
keyboard focus are distinct states. The row owns its visual surface across the
|
|
139
|
+
label and disclosure; do not paint a shorter selected pill only behind the link
|
|
140
|
+
or add the trailing inset twice. Leading art, label and real trailing anatomy
|
|
141
|
+
share deliberate tracks. A supporting chevron can use a smaller IconButton
|
|
142
|
+
iconSize while retaining the normal control target.
|
|
143
|
+
|
|
144
|
+
NavigationTree remains the recursive owner: it composes NavigationItem, manages
|
|
145
|
+
the accepted expandedValues set and presents compact branches through Popover.
|
|
146
|
+
The popup can include a branch's own destination and its children. A standalone
|
|
147
|
+
compact NavigationItem does not create a hidden child hierarchy on its own.
|
|
148
|
+
|
|
149
|
+
## Configure groups and fixed regions
|
|
150
|
+
|
|
151
|
+
NavigationGroup can omit its label, hide a supplied visual label, add a meaningful
|
|
152
|
+
headingAction, show a separator or expose its own disclosure. Its expanded state
|
|
153
|
+
is independent of NavigationTree branch expansion and Sidebar width. A collapsible
|
|
154
|
+
group needs a nonempty label; hideLabel keeps the toggle's accessible name even
|
|
155
|
+
when the visual title is omitted. collapseDisabled affects that group disclosure,
|
|
156
|
+
not arbitrary child links or actions.
|
|
157
|
+
|
|
158
|
+
In compact presentation, group contents stay reachable and the group toggle is
|
|
159
|
+
hidden. This presentation does not overwrite the accepted expanded state: returning
|
|
160
|
+
to expanded navigation restores the group's prior choice. A supplied heading
|
|
161
|
+
action remains available on the compact axis rather than disappearing with text.
|
|
162
|
+
|
|
163
|
+
SidebarGroup exposes the group label, hideLabel, headingAction, separator and
|
|
164
|
+
collapsible/expanded configuration when Sidebar assembles the hierarchy. Start
|
|
165
|
+
these optional additions empty. Use a section title only when it distinguishes
|
|
166
|
+
related destinations; a title already provided by the pane need not be repeated.
|
|
167
|
+
Use branchGuides within a group when several levels of nesting need a visible
|
|
168
|
+
guide. Use trailingAction for an independent action alongside row metadata.
|
|
169
|
+
headerSeparator and footerSeparator are independent, opt-in Sidebar choices,
|
|
170
|
+
not mandatory lines around every fixed region.
|
|
171
|
+
|
|
172
|
+
Header and footer content must follow the same mode contract as the body. In
|
|
173
|
+
expanded navigation, identity and utility rows use the agreed icon/text tracks.
|
|
174
|
+
In compact navigation, measure the actual brand, toggle, destination and account
|
|
175
|
+
hit-area centers against the full rail center. A centered footer wrapper is not
|
|
176
|
+
proof that its Menu trigger or Avatar is centered. Adapt the identity content
|
|
177
|
+
deliberately while preserving its accessible name and useful recognition cue.
|
|
178
|
+
|
|
179
|
+
## Presentation and content
|
|
180
|
+
|
|
181
|
+
Sidebar's quiet recipe uses restrained inactive text and rectangular current
|
|
182
|
+
rows; standard retains stronger pill geometry. Compare these against the main
|
|
183
|
+
work area, not only in isolation. Preserve explicit icon sizes, shared inline
|
|
184
|
+
tracks and a common top anchor across adjacent panes. Omitting a collapse button
|
|
185
|
+
must not shorten one header and lift its entire navigation above the other.
|
|
186
|
+
|
|
187
|
+
Use `SidebarGroup.hideLabel` when the pane header already names the only group.
|
|
188
|
+
Its navigation landmark keeps its label. Add badges only for meaningful counts or
|
|
189
|
+
states, not to fill every destination. Account utilities and workspace settings
|
|
190
|
+
belong in deliberate fixed slots rather than a second contextual destination list.
|
|
191
|
+
|
|
192
|
+
## State and focus ownership
|
|
193
|
+
|
|
194
|
+
Keep search and filters in their applicable module. An inactive Projects filter
|
|
195
|
+
must not create a Clear filters action in Inbox, and clearing Inbox search must
|
|
196
|
+
not modify the accepted Projects query. Validate the entire route, including
|
|
197
|
+
extra or missing path segments, before deriving a title or current destination.
|
|
198
|
+
|
|
199
|
+
Preserve surviving focus. When a focused record, action or pane disappears and
|
|
200
|
+
focus falls to the document body, the host restores it to an intentional stable
|
|
201
|
+
destination, such as the main heading. Do not refocus the heading on every data
|
|
202
|
+
render or steal focus from another surviving control. A visibility toggle can
|
|
203
|
+
keep focus on itself before removing its pane. A dialog whose original trigger
|
|
204
|
+
disappeared needs the same explicit fallback when it closes.
|
|
205
|
+
|
|
206
|
+
## Verify the workspace
|
|
207
|
+
|
|
208
|
+
Check both layouts, module/page current semantics, hidden context through module
|
|
209
|
+
changes and Back, modified links, branch disclosure and independent scrolling.
|
|
210
|
+
Exercise both a full-row branch toggle and a destination-plus-disclosure row;
|
|
211
|
+
include a host refusing an expansion request. Configure group headings, actions,
|
|
212
|
+
separators and collapse independently, then switch compact mode without losing
|
|
213
|
+
the accepted group choice or destinations. Compare actual header/footer trigger
|
|
214
|
+
centers with body controls, including the account trigger, rather than inspecting
|
|
215
|
+
the middle list alone. Check current/hover/focus surfaces across the complete row.
|
|
216
|
+
Compare visible glyphs as well as their slots. If the same glyph reads off-axis
|
|
217
|
+
across several states, review its artwork before moving the shared hit area.
|
|
218
|
+
Exercise a filter in one module followed by search in another; remove the focused
|
|
219
|
+
row; close a dialog after its original row disappears. Review normal desktop,
|
|
220
|
+
short desktop windows, 1280 px with a 24 px root, RTL and forced colors. Visual
|
|
221
|
+
review and executed checks are separate evidence; the guidance itself is not a
|
|
222
|
+
pass certificate.
|
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
# Record management
|
|
2
|
+
|
|
3
|
+
[Open the working composition](/playground/records).
|
|
4
|
+
|
|
5
|
+
Search requests, narrow the list by status, open one request, and edit it in a
|
|
6
|
+
Drawer. Saving applies the accepted changes and returns to the same list.
|
|
7
|
+
The example uses in-memory fixtures that reset on reload.
|
|
8
|
+
|
|
9
|
+
## Choose where details open
|
|
10
|
+
|
|
11
|
+
Use a Drawer for a bounded inspection or edit that starts and ends in the same
|
|
12
|
+
list. Use a destination page for details that need a stable URL, substantial
|
|
13
|
+
room, or several independent tasks. This example uses a named Open button
|
|
14
|
+
because opening its Drawer is an action on the current screen.
|
|
15
|
+
|
|
16
|
+
When users compare several attributes, DataTable can use a `primaryAction`
|
|
17
|
+
in the identity column. Pass the Drawer trigger handlers and ref to that
|
|
18
|
+
action. Check the row background, title and description, checkbox and any
|
|
19
|
+
secondary menu independently. Use `toolbar` and `inset="roomy"` for a heading
|
|
20
|
+
and filters that share the table surface.
|
|
21
|
+
|
|
22
|
+
```tsx
|
|
23
|
+
<Drawer title="Review workspace access" size="lg"
|
|
24
|
+
trigger={buttonProps => (
|
|
25
|
+
<DataTable caption="Requests" hideCaption inset="roomy"
|
|
26
|
+
toolbar={<Heading level={2} size="section">Requests</Heading>}
|
|
27
|
+
columns={[{id: "request", label: "Request"}, {id: "status", label: "Status"}]}
|
|
28
|
+
rows={[{
|
|
29
|
+
id: "REQ-108",
|
|
30
|
+
cells: {status: "New"},
|
|
31
|
+
primaryAction: {
|
|
32
|
+
column: "request",
|
|
33
|
+
title: "Review workspace access",
|
|
34
|
+
description: "REQ-108 · Operations",
|
|
35
|
+
buttonProps,
|
|
36
|
+
},
|
|
37
|
+
}]} />
|
|
38
|
+
)}>
|
|
39
|
+
<Stack gap="6">
|
|
40
|
+
<Section title="Customer message">
|
|
41
|
+
<Text as="p">Confirm who should manage member permissions.</Text>
|
|
42
|
+
</Section>
|
|
43
|
+
<Section title="Assignment">
|
|
44
|
+
<Field label="Owner">{field => <Input {...field} />}</Field>
|
|
45
|
+
</Section>
|
|
46
|
+
</Stack>
|
|
47
|
+
</Drawer>
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Use `size="lg"` when long messages and several field groups need the wider
|
|
51
|
+
reading area. Compose the body from Section for separately named content,
|
|
52
|
+
DescriptionList for facts and Fieldset for related controls. Check wrapping and
|
|
53
|
+
scrolling with the longest title and a real footer action before selecting a size.
|
|
54
|
+
|
|
55
|
+
## Keep browsing state and edit state separate
|
|
56
|
+
|
|
57
|
+
Keep search, filter, accepted records and the open record ID in the host.
|
|
58
|
+
Keep the list and its ScrollArea mounted while the Drawer is open. Reset the
|
|
59
|
+
scroll position only when the user changes the query or filter.
|
|
60
|
+
|
|
61
|
+
Entering edit mode copies the selected record. Cancel edit returns to details
|
|
62
|
+
without changing accepted data. Saving replaces the record by stable ID.
|
|
63
|
+
Recompute matches using the existing search and filter. If the updated record
|
|
64
|
+
leaves the results, explain why and preserve the query. For a remote save, close
|
|
65
|
+
on accepted data and retain the draft with actionable feedback on failure.
|
|
66
|
+
|
|
67
|
+
## Restore focus to a useful place
|
|
68
|
+
|
|
69
|
+
Forward the Drawer trigger props from the single overlay to the row action
|
|
70
|
+
that opens it. Focus Edit request when details open, Subject when editing starts,
|
|
71
|
+
and Edit request after Cancel edit. On close, return focus to the same row
|
|
72
|
+
action without scrolling. If the row left the results, focus Search requests
|
|
73
|
+
without clearing it.
|
|
74
|
+
|
|
75
|
+
Keep subject and ID together in the list. Status earns a stable trailing slot
|
|
76
|
+
because users filter and compare by it. Put owner and the longer summary in the
|
|
77
|
+
Drawer, where they support a decision about a single record. Put the summary
|
|
78
|
+
in the Drawer description slot so it stays grouped with the title. Keep the
|
|
79
|
+
named values together in the body below it.
|
|
80
|
+
|
|
81
|
+
Review the Drawer with a wrapped title, then with its description omitted.
|
|
82
|
+
The heading and summary should read as one group while the values begin a
|
|
83
|
+
separate block. Check the visible copy and the scrollable area above the action.
|
|
84
|
+
|
|
85
|
+
Give each DescriptionList item a stable string `id`, such as `"owner"`.
|
|
86
|
+
Open a real row during browser verification. A build alone never exercises the
|
|
87
|
+
Drawer's conditional detail content.
|
|
88
|
+
|
|
89
|
+
```tsx
|
|
90
|
+
"use client";
|
|
91
|
+
|
|
92
|
+
import {useEffect, useId, useLayoutEffect, useRef, useState, type FormEvent} from "react";
|
|
93
|
+
import {
|
|
94
|
+
Alert, Button, DataList, DataListItem, DescriptionList, Drawer, EmptyState,
|
|
95
|
+
Field, Grid, Heading, Inline, Input, ScrollArea, SearchInput, Select,
|
|
96
|
+
Stack, StatusIndicator, Surface,
|
|
97
|
+
} from "@flood-md/react";
|
|
98
|
+
|
|
99
|
+
const statuses = {
|
|
100
|
+
new: {label: "New", tone: "info"},
|
|
101
|
+
active: {label: "In progress", tone: "warning"},
|
|
102
|
+
resolved: {label: "Resolved", tone: "success"},
|
|
103
|
+
} as const;
|
|
104
|
+
|
|
105
|
+
type Status = keyof typeof statuses;
|
|
106
|
+
type RequestRecord = {
|
|
107
|
+
id: string;
|
|
108
|
+
title: string;
|
|
109
|
+
owner: string;
|
|
110
|
+
status: Status;
|
|
111
|
+
summary: string;
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
const fixtures: RequestRecord[] = [
|
|
115
|
+
{id: "REQ-104", title: "Update billing address", owner: "Mina Chen", status: "new", summary: "Use the new address on future invoices."},
|
|
116
|
+
{id: "REQ-105", title: "Restore workspace access", owner: "Alex Morgan", status: "active", summary: "A teammate cannot sign in after changing email."},
|
|
117
|
+
{id: "REQ-106", title: "Export April invoices", owner: "Noah Park", status: "resolved", summary: "Provide a copy for the finance team."},
|
|
118
|
+
{id: "REQ-107", title: "Add a backup contact", owner: "Mina Chen", status: "active", summary: "Route account notices to a second contact."},
|
|
119
|
+
{id: "REQ-108", title: "Review workspace roles", owner: "Alex Morgan", status: "new", summary: "Confirm who should manage member permissions."},
|
|
120
|
+
{id: "REQ-109", title: "Correct the renewal date", owner: "Noah Park", status: "active", summary: "The displayed date differs from the order confirmation."},
|
|
121
|
+
{id: "REQ-110", title: "Remove a duplicate invitation", owner: "Mina Chen", status: "resolved", summary: "Keep the invitation sent to the current address."},
|
|
122
|
+
{id: "REQ-111", title: "Check a missing delivery notification", owner: "Alex Morgan", status: "active", summary: "A shipping update did not reach the assigned contact."},
|
|
123
|
+
{id: "REQ-112", title: "Change invoice recipient", owner: "Noah Park", status: "new", summary: "Send future copies to the accounts inbox."},
|
|
124
|
+
{id: "REQ-113", title: "Transfer workspace ownership", owner: "Mina Chen", status: "active", summary: "The previous owner has moved to another team."},
|
|
125
|
+
{id: "REQ-114", title: "Confirm a duplicate charge", owner: "Alex Morgan", status: "resolved", summary: "Compare the two entries against the receipt."},
|
|
126
|
+
{id: "REQ-115", title: "Recover archived access settings", owner: "Noah Park", status: "active", summary: "Review the settings before the workspace is reopened."},
|
|
127
|
+
{id: "REQ-116", title: "Replace the emergency contact", owner: "Mina Chen", status: "active", summary: "Update the contact listed for urgent account issues."},
|
|
128
|
+
{id: "REQ-117", title: "Review regional account permissions", owner: "Alex Morgan", status: "active", summary: "Check access before the next regional rollout."},
|
|
129
|
+
];
|
|
130
|
+
|
|
131
|
+
function matchesRequest(record: RequestRecord, query: string, status: Status | "all") {
|
|
132
|
+
const needle = query.trim().toLowerCase();
|
|
133
|
+
const searchable = `${record.id} ${record.title} ${record.summary}`.toLowerCase();
|
|
134
|
+
return (status === "all" || record.status === status) && searchable.includes(needle);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const useBrowserLayoutEffect = typeof window === "undefined" ? useEffect : useLayoutEffect;
|
|
138
|
+
|
|
139
|
+
export function RecordManagement() {
|
|
140
|
+
const formId = useId();
|
|
141
|
+
const [records, setRecords] = useState(fixtures);
|
|
142
|
+
const [query, setQuery] = useState("");
|
|
143
|
+
const [status, setStatus] = useState<Status | "all">("all");
|
|
144
|
+
const [recordId, setRecordId] = useState<string | null>(null);
|
|
145
|
+
const [draft, setDraft] = useState<RequestRecord | null>(null);
|
|
146
|
+
const [notice, setNotice] = useState("");
|
|
147
|
+
const search = useRef<HTMLInputElement>(null);
|
|
148
|
+
const list = useRef<HTMLDivElement>(null);
|
|
149
|
+
const subject = useRef<HTMLInputElement>(null);
|
|
150
|
+
const edit = useRef<HTMLButtonElement>(null);
|
|
151
|
+
const opener = useRef<HTMLButtonElement>(null);
|
|
152
|
+
const restore = useRef(false);
|
|
153
|
+
const returnToEdit = useRef(false);
|
|
154
|
+
const selected = records.find(record => record.id === recordId);
|
|
155
|
+
const results = records.filter(record => matchesRequest(record, query, status));
|
|
156
|
+
const editing = draft !== null;
|
|
157
|
+
const open = selected !== undefined;
|
|
158
|
+
|
|
159
|
+
useBrowserLayoutEffect(() => {
|
|
160
|
+
if (!open && restore.current) {
|
|
161
|
+
restore.current = false;
|
|
162
|
+
const target = opener.current?.isConnected ? opener.current : search.current;
|
|
163
|
+
target?.focus({preventScroll: true});
|
|
164
|
+
} else if (editing) {
|
|
165
|
+
subject.current?.focus({preventScroll: true});
|
|
166
|
+
} else if (open && returnToEdit.current) {
|
|
167
|
+
returnToEdit.current = false;
|
|
168
|
+
edit.current?.focus({preventScroll: true});
|
|
169
|
+
}
|
|
170
|
+
}, [open, editing]);
|
|
171
|
+
|
|
172
|
+
function changeQuery(nextQuery: string, nextStatus: Status | "all") {
|
|
173
|
+
setQuery(nextQuery);
|
|
174
|
+
setStatus(nextStatus);
|
|
175
|
+
setNotice("");
|
|
176
|
+
if (list.current) list.current.scrollTop = 0;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function closeRecord() {
|
|
180
|
+
restore.current = true;
|
|
181
|
+
returnToEdit.current = false;
|
|
182
|
+
setDraft(null);
|
|
183
|
+
setRecordId(null);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function save(event: FormEvent<HTMLFormElement>) {
|
|
187
|
+
event.preventDefault();
|
|
188
|
+
if (!draft) return;
|
|
189
|
+
const title = draft.title.trim();
|
|
190
|
+
if (!title) {
|
|
191
|
+
subject.current?.setCustomValidity("Enter a subject.");
|
|
192
|
+
subject.current?.reportValidity();
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
const updated = {...draft, title, owner: draft.owner.trim()};
|
|
196
|
+
setRecords(current => current.map(record => record.id === updated.id ? updated : record));
|
|
197
|
+
setNotice(matchesRequest(updated, query, status)
|
|
198
|
+
? `${updated.id} updated.`
|
|
199
|
+
: `${updated.id} updated and no longer matches the current search or filter.`);
|
|
200
|
+
closeRecord();
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function cancelEdit() {
|
|
204
|
+
returnToEdit.current = true;
|
|
205
|
+
setDraft(null);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const drawerTitle = draft ? `Edit ${draft.id}` : selected?.title ?? "Request details";
|
|
209
|
+
|
|
210
|
+
return <section aria-label="Request workspace">
|
|
211
|
+
<Stack gap="6">
|
|
212
|
+
<Heading level={1} size="page">Requests</Heading>
|
|
213
|
+
<Grid gap="4" minItemWidth="12rem">
|
|
214
|
+
<Field label="Search requests">
|
|
215
|
+
{field => <SearchInput {...field} ref={search} value={query} clearLabel="Clear search"
|
|
216
|
+
placeholder="Subject, summary or request ID"
|
|
217
|
+
onChange={event => changeQuery(event.target.value, status)} />}
|
|
218
|
+
</Field>
|
|
219
|
+
<Field label="Status">
|
|
220
|
+
{field => <Select {...field} value={status}
|
|
221
|
+
onChange={event => changeQuery(query, event.target.value as Status | "all")}>
|
|
222
|
+
<option value="all">All statuses</option>
|
|
223
|
+
{Object.entries(statuses).map(([value, option]) => <option key={value} value={value}>{option.label}</option>)}
|
|
224
|
+
</Select>}
|
|
225
|
+
</Field>
|
|
226
|
+
</Grid>
|
|
227
|
+
<Drawer
|
|
228
|
+
title={drawerTitle}
|
|
229
|
+
size="lg"
|
|
230
|
+
description={editing ? undefined : selected?.summary}
|
|
231
|
+
open={open}
|
|
232
|
+
onOpenChange={open => { if (!open) closeRecord(); }}
|
|
233
|
+
initialFocusRef={edit}
|
|
234
|
+
trigger={({ref, onClick, "aria-expanded": expanded, ...trigger}) =>
|
|
235
|
+
<Surface elevation="raised" padding="4">
|
|
236
|
+
<ScrollArea ref={list} role="region" aria-label="Request results" style={{maxBlockSize: "22rem"}}>
|
|
237
|
+
{results.length > 0
|
|
238
|
+
? <DataList aria-label="Requests">
|
|
239
|
+
{results.map(record => <DataListItem
|
|
240
|
+
key={record.id}
|
|
241
|
+
title={record.title}
|
|
242
|
+
description={record.id}
|
|
243
|
+
metadata={<StatusIndicator {...statuses[record.status]} size="sm" />}
|
|
244
|
+
actions={<Button {...trigger} aria-expanded={Boolean(expanded && record.id === recordId)}
|
|
245
|
+
aria-label={`Open ${record.id} ${record.title}`}
|
|
246
|
+
variant="subtle" size="sm" onClick={event => {
|
|
247
|
+
opener.current = event.currentTarget;
|
|
248
|
+
ref(event.currentTarget);
|
|
249
|
+
setRecordId(record.id);
|
|
250
|
+
onClick?.(event);
|
|
251
|
+
}}>Open</Button>}
|
|
252
|
+
/>)}
|
|
253
|
+
</DataList>
|
|
254
|
+
: <EmptyState title="No matching requests" actions={<Button variant="subtle" onClick={() => {
|
|
255
|
+
search.current?.focus({preventScroll: true});
|
|
256
|
+
changeQuery("", "all");
|
|
257
|
+
}}>Clear filters</Button>} />}
|
|
258
|
+
</ScrollArea>
|
|
259
|
+
</Surface>}
|
|
260
|
+
footer={draft
|
|
261
|
+
? <Inline gap="2"><Button type="submit" form={formId}>Save changes</Button>
|
|
262
|
+
<Button variant="subtle" onClick={cancelEdit}>Cancel edit</Button></Inline>
|
|
263
|
+
: <Button ref={edit} onClick={() => { if (selected) setDraft({...selected}); }}>Edit request</Button>}
|
|
264
|
+
>
|
|
265
|
+
{draft
|
|
266
|
+
? <form id={formId} onSubmit={save}>
|
|
267
|
+
<Stack gap="4">
|
|
268
|
+
<Field label="Subject" required>
|
|
269
|
+
{field => <Input {...field} ref={subject} value={draft.title} onChange={event => {
|
|
270
|
+
event.currentTarget.setCustomValidity("");
|
|
271
|
+
setDraft({...draft, title: event.target.value});
|
|
272
|
+
}} />}
|
|
273
|
+
</Field>
|
|
274
|
+
<Field label="Owner">
|
|
275
|
+
{field => <Input {...field} value={draft.owner}
|
|
276
|
+
onChange={event => setDraft({...draft, owner: event.target.value})} />}
|
|
277
|
+
</Field>
|
|
278
|
+
<Field label="Status">
|
|
279
|
+
{field => <Select {...field} value={draft.status}
|
|
280
|
+
onChange={event => setDraft({...draft, status: event.target.value as Status})}>
|
|
281
|
+
{Object.entries(statuses).map(([value, option]) => <option key={value} value={value}>{option.label}</option>)}
|
|
282
|
+
</Select>}
|
|
283
|
+
</Field>
|
|
284
|
+
</Stack>
|
|
285
|
+
</form>
|
|
286
|
+
: selected && <DescriptionList items={[
|
|
287
|
+
{id: "request-id", term: "Request ID", description: selected.id},
|
|
288
|
+
{id: "owner", term: "Owner", description: selected.owner || "Unassigned"},
|
|
289
|
+
{id: "status", term: "Status", description: <StatusIndicator {...statuses[selected.status]} />},
|
|
290
|
+
]} />}
|
|
291
|
+
</Drawer>
|
|
292
|
+
{notice && <Alert tone="success" announcement="polite">{notice}</Alert>}
|
|
293
|
+
</Stack>
|
|
294
|
+
</section>;
|
|
295
|
+
}
|
|
296
|
+
```
|