@flood-md/agent 0.2.11 → 0.2.13
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 +10 -1
- package/README.md +9 -4
- package/SKILL.md +16 -5
- package/dist/catalog.json +816 -232
- package/dist/materials/component-alert-dialog.md +1 -0
- package/dist/materials/component-app-shell.md +3 -1
- package/dist/materials/component-bar-chart.md +52 -0
- package/dist/materials/component-bar-list.md +41 -0
- package/dist/materials/component-box-plot.md +46 -0
- package/dist/materials/component-bullet-chart.md +43 -0
- package/dist/materials/component-code-snippet.md +3 -1
- package/dist/materials/component-collection-toolbar.md +4 -1
- package/dist/materials/component-command-palette.md +1 -0
- package/dist/materials/component-copy-button.md +2 -0
- package/dist/materials/component-data-table.md +21 -3
- package/dist/materials/component-dialog.md +2 -0
- package/dist/materials/component-donut-chart.md +46 -0
- package/dist/materials/component-drawer.md +2 -0
- package/dist/materials/component-dumbbell-chart.md +46 -0
- package/dist/materials/component-editable.md +4 -0
- package/dist/materials/component-heading.md +1 -1
- package/dist/materials/component-heatmap.md +45 -0
- package/dist/materials/component-histogram.md +46 -0
- package/dist/materials/component-image-preview.md +1 -0
- package/dist/materials/component-line-chart.md +50 -0
- package/dist/materials/component-menu.md +2 -0
- package/dist/materials/component-range-chart.md +46 -0
- package/dist/materials/component-scatter-chart.md +48 -0
- package/dist/materials/component-section.md +1 -1
- package/dist/materials/component-sheet.md +50 -0
- package/dist/materials/component-sparkline.md +41 -0
- package/dist/materials/component-table.md +3 -1
- package/dist/materials/component-toolbar.md +1 -0
- package/dist/materials/component-waterfall-chart.md +47 -0
- package/dist/materials/guide-agent-knowledge.md +5 -5
- package/dist/materials/guide-agents.md +35 -2
- package/dist/materials/guide-charts.md +34 -0
- package/dist/materials/guide-foundations.md +2 -2
- package/dist/materials/guide-installation.md +6 -6
- package/dist/materials/guide-introduction.md +2 -1
- package/dist/materials/guide-motion.md +19 -10
- package/dist/materials/guide-skills.md +10 -0
- package/dist/materials/recipe-collection-views.md +72 -47
- package/dist/materials/recipe-navigation-workspace.md +3 -0
- package/dist/materials/recipe-record-management.md +109 -61
- package/knowledge.json +16 -0
- package/lib/inspect.d.ts +14 -2
- package/lib/inspect.mjs +65 -0
- package/manifest.json +11 -1
- package/package.json +1 -1
- package/references/component-families.md +14 -0
- package/references/composition.md +12 -0
- package/references/flood-composition/SKILL.md +7 -0
- package/references/flood-data-display/SKILL.md +114 -0
- package/references/flood-feedback/SKILL.md +12 -0
- package/references/flood-forms/SKILL.md +5 -0
- package/references/flood-hierarchy/SKILL.md +4 -0
- package/references/flood-interface-content/SKILL.md +7 -0
- package/references/flood-motion/SKILL.md +3 -0
- package/references/flood-overlays/SKILL.md +11 -0
- package/references/flood-records/SKILL.md +32 -0
- package/references/flood-screen-composition/SKILL.md +48 -0
- package/references/flood-spacing/SKILL.md +7 -0
- package/references/flood-verification/SKILL.md +32 -0
- package/references/flood-visual-review/SKILL.md +59 -0
- package/references/rendered-review.md +49 -3
- package/references/routing.md +2 -1
- package/rules.json +37 -1
- package/workflows.json +22 -0
|
@@ -61,7 +61,9 @@ checkboxes with no resulting action.
|
|
|
61
61
|
|
|
62
62
|
Selecting projects reveals one shared action bar floating at the viewport bottom. It shows the
|
|
63
63
|
selected count, changes status, confirms archiving and clears the
|
|
64
|
-
selection.
|
|
64
|
+
selection. DataTable uses `showSelection={false}` to keep its result range and
|
|
65
|
+
pagination while announcing the selected count without displaying it again.
|
|
66
|
+
Selection spans the filtered client collection across its pages.
|
|
65
67
|
Changing the query resets it. Archiving captures IDs and names before opening
|
|
66
68
|
the confirmation. Cancel keeps the selection and records intact.
|
|
67
69
|
|
|
@@ -70,6 +72,10 @@ records after the optional onUpdateStatus callback accepts the whole operation.
|
|
|
70
72
|
Reload returns to the declared fixtures. A rejection keeps accepted records and
|
|
71
73
|
selection intact. Retry uses the failed operation's captured IDs and status.
|
|
72
74
|
Dismiss or a deliberate query/selection change abandons its feedback.
|
|
75
|
+
Report results in a floating ToastRegion so the table stays in place.
|
|
76
|
+
Use the default bottom-end placement. Success expires automatically. A failed
|
|
77
|
+
update offers a persistent Retry action. While the selection rail is visible,
|
|
78
|
+
raise the toast above it without changing the table's scroll clearance.
|
|
73
79
|
|
|
74
80
|
Only one update can run at a time. Pending actions are disabled. If the user
|
|
75
81
|
changes the query or selection while an update is pending, its result still
|
|
@@ -113,7 +119,7 @@ export function ProjectSelectionActions({ projects, pending, onUpdate, onClear }
|
|
|
113
119
|
<VisuallyHidden role="status">{pending ? 'Updating…' : ''}</VisuallyHidden>
|
|
114
120
|
{archive && <AlertDialog
|
|
115
121
|
title={`Archive ${archive.length} ${archive.length === 1 ? 'project' : 'projects'}?`}
|
|
116
|
-
description={`${archive.map(project => project.name).join(', ')} will move to Archived.`}
|
|
122
|
+
description={`${archive.map(project => project.name).join(', ')}\n${archive.length === 1 ? 'It' : 'They'} will move to Archived.`}
|
|
117
123
|
confirmLabel="Archive projects"
|
|
118
124
|
cancelLabel="Keep projects"
|
|
119
125
|
open
|
|
@@ -129,29 +135,52 @@ export function ProjectSelectionActions({ projects, pending, onUpdate, onClear }
|
|
|
129
135
|
|
|
130
136
|
The panel uses the existing floating-surface tokens and a viewport-safe inset.
|
|
131
137
|
It leaves normal document flow and stays centered when the document scrolls.
|
|
138
|
+
Check the table header position and scroll dimensions with zero, one and
|
|
139
|
+
several selected records. A collection that fits should keep fitting on selection.
|
|
132
140
|
BulkActionBar keeps a single pill-shaped row with at most two visible commands
|
|
133
141
|
by default. Lower-priority commands move into More as space shrinks; the host
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
scroll
|
|
142
|
+
keeps the commands on one row. Budget bottom clearance in the base layout
|
|
143
|
+
using the control height, the panel chrome and the viewport inset. At the end
|
|
144
|
+
of a scroll, the final rows and pagination should be reachable above the panel.
|
|
145
|
+
Keep the scroll area inside the bordered Surface and clip the frame at its
|
|
146
|
+
rounded corners so the table footer cannot cover the outline.
|
|
137
147
|
|
|
138
148
|
```css
|
|
139
149
|
.collection-examples { padding-block:var(--flood-sys-space-8); min-inline-size:0; }
|
|
140
150
|
.collection-view-controls { min-inline-size:0; }
|
|
141
151
|
.flood-root .collection-filter-editor { inline-size:min(42rem,calc(100vw - 2rem)); }
|
|
142
152
|
|
|
153
|
+
.collection-projects {
|
|
154
|
+
--collection-selection-inset: max(var(--flood-sys-space-4), env(safe-area-inset-bottom));
|
|
155
|
+
--collection-selection-control-height: var(--flood-sys-control-sm-height);
|
|
156
|
+
--collection-selection-height: calc(var(--collection-selection-control-height) + 2 * (var(--flood-sys-space-1) + var(--flood-sys-stroke-width)));
|
|
157
|
+
padding-block-end: calc(var(--collection-selection-inset) + var(--collection-selection-height) + var(--flood-sys-space-2));
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
@media (pointer: coarse) {
|
|
161
|
+
.collection-projects {
|
|
162
|
+
--collection-selection-control-height: max(var(--flood-sys-control-sm-height), var(--flood-sys-size-touch));
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
.collection-table-frame {
|
|
167
|
+
border: var(--flood-sys-stroke-width) solid var(--flood-sys-color-border-decorative);
|
|
168
|
+
overflow: clip;
|
|
169
|
+
}
|
|
170
|
+
|
|
143
171
|
.collection-selection-panel {
|
|
144
172
|
position: fixed;
|
|
145
173
|
inset-inline: 0;
|
|
146
|
-
inset-block-end:
|
|
174
|
+
inset-block-end: var(--collection-selection-inset);
|
|
147
175
|
margin-inline: auto;
|
|
148
176
|
inline-size: min(26rem, calc(100% - 2 * var(--flood-sys-space-4)));
|
|
149
177
|
max-inline-size: calc(100% - 2 * var(--flood-sys-space-4));
|
|
150
178
|
z-index: var(--flood-sys-layer-sticky);
|
|
151
179
|
}
|
|
152
180
|
|
|
153
|
-
.collection-
|
|
154
|
-
|
|
181
|
+
.flood-root .collection-selection-panel + .flood-toast-region {
|
|
182
|
+
inset-block-end: calc(var(--collection-selection-inset) + var(--collection-selection-height) + var(--flood-sys-space-2));
|
|
183
|
+
max-block-size: calc(100dvh - var(--collection-selection-inset) - var(--collection-selection-height) - var(--flood-sys-space-6));
|
|
155
184
|
}
|
|
156
185
|
```
|
|
157
186
|
|
|
@@ -210,6 +239,12 @@ defines reset scope and supplies canClear for any state held by quickFilters.
|
|
|
210
239
|
At most two task-important quick properties remain inline; additional properties
|
|
211
240
|
belong inside Filters. Summaries outside the panel name the property and preserve
|
|
212
241
|
any-match meaning. Container width, rather than viewport size, governs reflow.
|
|
242
|
+
When search and filters share a wide row with a status or view selector, use
|
|
243
|
+
`align="end"` on CollectionToolbar. Check that the final control reaches the
|
|
244
|
+
row's trailing content edge and that the row wraps with enlarged text.
|
|
245
|
+
The Projects table places its Columns control in CollectionToolbar's `actions`
|
|
246
|
+
slot via DataTable's `toolbar` render function. It shares the tool row with
|
|
247
|
+
search and filters, and the commands wrap as a group in a narrow container.
|
|
213
248
|
|
|
214
249
|
CollectionViewSwitcher owns the separate accepted-view choice and adaptive
|
|
215
250
|
commands. It reuses Field/Select and Button/OverflowActions with the same size.
|
|
@@ -222,7 +257,7 @@ SearchInput, Select, Menu, MultiSelect and SegmentedControl own input/choice beh
|
|
|
222
257
|
Popover contains the condition draft and Tag removes applied conditions. Button
|
|
223
258
|
and IconButton perform actions. Several view commands use OverflowActions; a
|
|
224
259
|
single command uses Button directly. Field/Fieldset, Grid/Stack/Inline, Dialog,
|
|
225
|
-
AlertDialog and
|
|
260
|
+
AlertDialog and ToastRegion retain their own roles. DataTable/StatusIndicator and
|
|
226
261
|
FileList/FileItem/FileMeta demonstrate two result structures. No owner is inserted
|
|
227
262
|
merely to increase its usage count, and no new package atom is needed for layout.
|
|
228
263
|
|
|
@@ -293,7 +328,7 @@ export function readCollectionViews(key:string,schema:CollectionSchema,storage:V
|
|
|
293
328
|
|
|
294
329
|
```tsx
|
|
295
330
|
"use client";
|
|
296
|
-
import {useEffect,useRef,useState,type FormEvent} from 'react';
|
|
331
|
+
import {useEffect,useRef,useState,type FormEvent,type ReactNode} from 'react';
|
|
297
332
|
import {Alert,AlertDialog,Button,CollectionToolbar,CollectionViewSwitcher,Dialog,Field,FilterGroup,IconSlot,Inline,Input,Menu,Select,Stack,Tag,type OverflowAction} from '@flood-md/react';
|
|
298
333
|
import {collectionQueryKey,copyCollectionQuery,parseCollectionQuery,readCollectionViews,type CollectionQuery,type CollectionSchema,type CollectionView,type FilterProperty,type ViewRead,type ViewStorage} from './collection-views-model';
|
|
299
334
|
|
|
@@ -340,13 +375,13 @@ export function AppliedFilterControl({ property, value, onChange }: {
|
|
|
340
375
|
|
|
341
376
|
interface CollectionViewControlsProps {
|
|
342
377
|
label:string;schema:CollectionSchema;presets:readonly CollectionView[];
|
|
343
|
-
value:CollectionQuery;onValueChange:(query:CollectionQuery)=>void;storageKey:string;storage?:ViewStorage;
|
|
378
|
+
value:CollectionQuery;onValueChange:(query:CollectionQuery)=>void;storageKey:string;storage?:ViewStorage;toolbarActions?:ReactNode;
|
|
344
379
|
}
|
|
345
380
|
type NameDraft={kind:'create'|'rename';id?:string;name:string;snapshot:CollectionQuery;error?:string;invalid?:string};
|
|
346
381
|
const inlineFilterLimit = 2;
|
|
347
382
|
|
|
348
383
|
/** Host recipe: shared behaviour, not a second Flood component implementation. */
|
|
349
|
-
export function CollectionViewControls({label,schema,presets,value,onValueChange,storageKey,storage}:CollectionViewControlsProps){
|
|
384
|
+
export function CollectionViewControls({label,schema,presets,value,onValueChange,storageKey,storage,toolbarActions}:CollectionViewControlsProps){
|
|
350
385
|
const [saved,setSaved]=useState<ViewRead|{state:'loading';views:[]}>({state:'loading',views:[]}),[selectedId,setSelectedId]=useState(presets[0].id);
|
|
351
386
|
const [editing,setEditing]=useState(false),[draft,setDraft]=useState(()=>copyCollectionQuery(value)),[draftError,setDraftError]=useState('');
|
|
352
387
|
const [advancedEditor, setAdvancedEditor] = useState(false);
|
|
@@ -435,6 +470,7 @@ export function CollectionViewControls({label,schema,presets,value,onValueChange
|
|
|
435
470
|
{saved.state==='error'&&<Stack gap="3"><Alert tone="error">Saved views could not be read.</Alert><Inline gap="3"><Button size="sm" variant="subtle" onClick={()=>setReadAttempt(attempt=>attempt+1)}>Retry saved views</Button>{saved.raw!==null&&<Button size="sm" variant="subtle" onClick={exportSnapshot}>Export snapshot</Button>}<Button size="sm" variant="subtle" tone="danger" onClick={()=>{viewInput.current?.focus({preventScroll:true});setConfirmation({kind:'replace'});}}>Replace saved views</Button></Inline></Stack>}
|
|
436
471
|
{operationError&&<Alert tone="error" announcement="polite"><Inline gap="3">{operationError.message}<Button size="sm" variant="ghost" onClick={()=>setOperationError(null)}>Dismiss</Button></Inline></Alert>}
|
|
437
472
|
<CollectionToolbar label={label+' collection toolbar'} searchValue={value.search}
|
|
473
|
+
actions={toolbarActions}
|
|
438
474
|
onSearchValueChange={search=>onValueChange({...value,search})}
|
|
439
475
|
searchLabel={'Search '+label.toLowerCase()} searchPlaceholder={'Search '+label.toLowerCase()+'…'} clearSearchLabel={'Clear '+label.toLowerCase()+' search'}
|
|
440
476
|
appliedFiltersLabel={label+' applied filters'}
|
|
@@ -496,8 +532,8 @@ chosen consumer with actual data and their own chrome.
|
|
|
496
532
|
|
|
497
533
|
```tsx
|
|
498
534
|
"use client";
|
|
499
|
-
import {
|
|
500
|
-
import {
|
|
535
|
+
import {useRef,useState} from 'react';
|
|
536
|
+
import {Button,DataTable,EmptyState,Field,FileItem,FileList,FloodRoot,Heading,Select,Stack,StatusIndicator,Surface,Tabs,ToastRegion,type ToastItem} from '@flood-md/react';
|
|
501
537
|
import {SiteHeader} from './site-header';
|
|
502
538
|
import {SiteFooter} from './site-footer';
|
|
503
539
|
import {CollectionViewControls} from './recipes/collection-views';
|
|
@@ -532,10 +568,8 @@ export function ProjectCollection({storage,onUpdateStatus}: {
|
|
|
532
568
|
const [page, setPage] = useState(1);
|
|
533
569
|
const [selection, setSelection] = useState<readonly string[]>([]);
|
|
534
570
|
const [pending, setPending] = useState(false);
|
|
535
|
-
const [feedback, setFeedback] = useState<
|
|
536
|
-
|
|
537
|
-
{ kind: 'error'; update: ProjectStatusUpdate; message: string } | null
|
|
538
|
-
>(null);
|
|
571
|
+
const [feedback, setFeedback] = useState<ToastItem | null>(null);
|
|
572
|
+
const feedbackSequence = useRef(0);
|
|
539
573
|
const inFlight = useRef(false);
|
|
540
574
|
const collection = useRef<HTMLElement>(null);
|
|
541
575
|
const selectionPanel = useRef<HTMLDivElement>(null);
|
|
@@ -549,20 +583,6 @@ export function ProjectCollection({storage,onUpdateStatus}: {
|
|
|
549
583
|
const selectedProjects=rows.filter(project=>selection.includes(project.id));
|
|
550
584
|
const hasSelection = selectedProjects.length > 0;
|
|
551
585
|
|
|
552
|
-
useEffect(() => {
|
|
553
|
-
const panel = selectionPanel.current;
|
|
554
|
-
const host = collection.current;
|
|
555
|
-
if (!hasSelection || !panel || !host) return;
|
|
556
|
-
const measure = () => host.style.setProperty('--collection-selection-height', `${panel.getBoundingClientRect().height}px`);
|
|
557
|
-
measure();
|
|
558
|
-
const observer = new ResizeObserver(measure);
|
|
559
|
-
observer.observe(panel);
|
|
560
|
-
return () => {
|
|
561
|
-
observer.disconnect();
|
|
562
|
-
host.style.removeProperty('--collection-selection-height');
|
|
563
|
-
};
|
|
564
|
-
}, [hasSelection]);
|
|
565
|
-
|
|
566
586
|
async function updateStatus(proposal: ProjectStatusUpdate) {
|
|
567
587
|
if (inFlight.current) return;
|
|
568
588
|
const update = { ids: [...new Set(proposal.ids)], status: proposal.status };
|
|
@@ -580,33 +600,38 @@ export function ProjectCollection({storage,onUpdateStatus}: {
|
|
|
580
600
|
setSelection(current => current.filter(id => !ids.has(id)));
|
|
581
601
|
setPage(1);
|
|
582
602
|
const result = update.status === 'active' ? 'set active' : update.status;
|
|
583
|
-
setFeedback({
|
|
603
|
+
setFeedback({ id: String(++feedbackSequence.current), status: 'success', title: `${ids.size} ${ids.size === 1 ? 'project' : 'projects'} ${result}.` });
|
|
584
604
|
} catch {
|
|
585
|
-
setFeedback({
|
|
605
|
+
setFeedback({
|
|
606
|
+
id: String(++feedbackSequence.current),
|
|
607
|
+
status: 'error',
|
|
608
|
+
title: `Couldn’t update ${update.ids.length} ${update.ids.length === 1 ? 'project' : 'projects'}.`,
|
|
609
|
+
action: { label: 'Retry update', onClick: () => {
|
|
610
|
+
collection.current?.focus({ preventScroll: true });
|
|
611
|
+
void updateStatus(update);
|
|
612
|
+
}},
|
|
613
|
+
});
|
|
586
614
|
} finally {
|
|
587
615
|
inFlight.current = false;
|
|
588
616
|
setPending(false);
|
|
589
617
|
}
|
|
590
618
|
}
|
|
591
619
|
|
|
592
|
-
return <section ref={collection} tabIndex={-1} className="collection-projects"
|
|
593
|
-
<CollectionViewControls label="Projects" schema={projectSchema} presets={projectPresets} value={query} onValueChange={change} storageKey="flood-project-views-v1" storage={storage}/>
|
|
620
|
+
return <section ref={collection} tabIndex={-1} className="collection-projects" aria-label="Projects collection"><Stack gap="6">
|
|
594
621
|
{hasSelection&&<div ref={selectionPanel} className="collection-selection-panel"><ProjectSelectionActions projects={selectedProjects} pending={pending} onUpdate={update=>{void updateStatus(update);}} onClear={()=>{
|
|
595
622
|
collection.current?.focus({preventScroll:true});setSelection([]);setFeedback(null);
|
|
596
623
|
}}/></div>}
|
|
597
|
-
|
|
598
|
-
{
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
<
|
|
603
|
-
|
|
604
|
-
}}>Dismiss</Button>
|
|
605
|
-
</Inline></Alert>}
|
|
606
|
-
<DataTable caption="Projects" hideCaption minWidth="36rem" selectable pageSize={5} page={page} onPageChange={setPage} selectedIds={selection} onSelectionChange={ids=>{setSelection(ids);setFeedback(null);}} sort={query.sort} onSortChange={sort=>change({...query,sort})}
|
|
624
|
+
<ToastRegion label="Project updates" items={feedback ? [feedback] : []}
|
|
625
|
+
onDismiss={id=>setFeedback(current=>current?.id===id?null:current)}/>
|
|
626
|
+
<Surface padding="0" className="collection-table-frame">
|
|
627
|
+
<DataTable caption="Projects" hideCaption minWidth="36rem" selectable showSelection={false} pageSize={5} page={page} onPageChange={setPage} selectedIds={selection} onSelectionChange={ids=>{setSelection(ids);setFeedback(null);}} sort={query.sort} onSortChange={sort=>change({...query,sort})}
|
|
628
|
+
inset="roomy" showColumnVisibility
|
|
629
|
+
toolbar={({columns}) => <CollectionViewControls label="Projects" schema={projectSchema} presets={projectPresets}
|
|
630
|
+
value={query} onValueChange={change} storageKey="flood-project-views-v1" storage={storage} toolbarActions={columns}/>}
|
|
607
631
|
emptyContent={<EmptyState title="No matching projects" actions={<Button variant="subtle" onClick={()=>{change({...query,search:'',conditions:[]});collection.current?.querySelector<HTMLInputElement>('.flood-collection-toolbar input')?.focus({preventScroll:true});}}>Reset filters</Button>}/>}
|
|
608
|
-
columns={[{id:'name',label:'Project',sortable:true},{id:'owner',label:'Owner',sortable:true},{id:'status',label:'Status',sortable:true},{id:'priority',label:'Priority',sortable:true}]}
|
|
632
|
+
columns={[{id:'name',label:'Project',sortable:true,hideable:false},{id:'owner',label:'Owner',sortable:true},{id:'status',label:'Status',sortable:true},{id:'priority',label:'Priority',sortable:true}]}
|
|
609
633
|
rows={rows.map(project=>({id:project.id,label:project.name,cells:{name:project.name,owner:owners.find(owner=>owner.value===project.owner)!.label,status:<StatusIndicator label={projectSchema.properties[0].options.find(option=>option.value===project.status)!.label} tone={project.status==='active'?'success':project.status==='paused'?'warning':'neutral'}/>,priority:project.priority==='urgent'?'Urgent':'Normal'},sortValues:{name:project.name,owner:project.owner,status:project.status,priority:project.priority==='urgent'?1:0}}))}/>
|
|
634
|
+
</Surface>
|
|
610
635
|
</Stack></section>;
|
|
611
636
|
}
|
|
612
637
|
|
|
@@ -62,6 +62,9 @@ embedded shell an explicit block size. Choose `scroll="page"` for natural
|
|
|
62
62
|
document scrolling. Place a persistent top bar and footer in the shell slots,
|
|
63
63
|
and PageHeader inside main. The shell has a skip target and owns the scroll
|
|
64
64
|
boundary; the host owns routes and persisted choices.
|
|
65
|
+
For a full-window workspace with a padded content edge, use
|
|
66
|
+
`scrollContentInset="none"` when that padding already clears the overlay rail.
|
|
67
|
+
Verify that the document width stays fixed as its collection grows and shrinks.
|
|
65
68
|
|
|
66
69
|
For a single Sidebar, opt into `navigationDrawer` with a width threshold in CSS
|
|
67
70
|
pixels and a `renderContent(close)` function. AppShell observes its own width.
|
|
@@ -2,22 +2,22 @@
|
|
|
2
2
|
|
|
3
3
|
[Open the working composition](/playground/records).
|
|
4
4
|
|
|
5
|
-
Search requests, narrow the
|
|
6
|
-
Drawer. Saving
|
|
5
|
+
Search requests, narrow the table by status, open one request, and edit it in a
|
|
6
|
+
Drawer. Saving updates the request and returns to the same table.
|
|
7
7
|
The example uses in-memory fixtures that reset on reload.
|
|
8
8
|
|
|
9
9
|
## Choose where details open
|
|
10
10
|
|
|
11
11
|
Use a Drawer for a bounded inspection or edit that starts and ends in the same
|
|
12
12
|
list. Use a destination page for details that need a stable URL, substantial
|
|
13
|
-
room, or several independent tasks.
|
|
14
|
-
because opening its Drawer is an action on the current screen.
|
|
13
|
+
room, or several independent tasks.
|
|
15
14
|
|
|
16
15
|
When users compare several attributes, DataTable can use a `primaryAction`
|
|
17
16
|
in the identity column. Pass the Drawer trigger handlers and ref to that
|
|
18
17
|
action. Check the row background, title and description, checkbox and any
|
|
19
18
|
secondary menu independently. Use `toolbar` and `inset="roomy"` for a heading
|
|
20
|
-
and filters that share the table surface.
|
|
19
|
+
and filters that share the table surface. Use the request title as the visible
|
|
20
|
+
action and keep its ID beside it for disambiguation.
|
|
21
21
|
|
|
22
22
|
```tsx
|
|
23
23
|
<Drawer title="Review workspace access" size="lg"
|
|
@@ -36,11 +36,11 @@ and filters that share the table surface.
|
|
|
36
36
|
},
|
|
37
37
|
}]} />
|
|
38
38
|
)}>
|
|
39
|
-
<Stack gap="
|
|
40
|
-
<Section title="Customer message">
|
|
39
|
+
<Stack gap="8">
|
|
40
|
+
<Section title="Customer message" headingLevel={3} headingSize="group">
|
|
41
41
|
<Text as="p">Confirm who should manage member permissions.</Text>
|
|
42
42
|
</Section>
|
|
43
|
-
<Section title="Assignment">
|
|
43
|
+
<Section title="Assignment" headingLevel={3} headingSize="group">
|
|
44
44
|
<Field label="Owner">{field => <Input {...field} />}</Field>
|
|
45
45
|
</Section>
|
|
46
46
|
</Stack>
|
|
@@ -51,17 +51,36 @@ Use `size="lg"` when long messages and several field groups need the wider
|
|
|
51
51
|
reading area. Compose the body from Section for separately named content,
|
|
52
52
|
DescriptionList for facts and Fieldset for related controls. Check wrapping and
|
|
53
53
|
scrolling with the longest title and a real footer action before selecting a size.
|
|
54
|
+
Use the group heading size inside a detail Drawer to keep the record name prominent.
|
|
55
|
+
Keep the request facts in one DescriptionList and use a compact, captioned Table
|
|
56
|
+
for related records that have comparable columns, such as activity dates and events.
|
|
57
|
+
Give that table its own heading when it identifies a distinct section. The Drawer
|
|
58
|
+
body owns vertical scrolling and the footer keeps the next action within reach.
|
|
59
|
+
|
|
60
|
+
When the detail contains line items and a total, use the Table footer to mark
|
|
61
|
+
the summary and keep its amount aligned with the line-item amounts. A row header
|
|
62
|
+
labels the total, and the Table footer supplies the separator and emphasis.
|
|
63
|
+
|
|
64
|
+
```tsx
|
|
65
|
+
<tfoot>
|
|
66
|
+
<tr>
|
|
67
|
+
<th scope="row" colSpan={2}>Total</th>
|
|
68
|
+
<td data-numeric>{formattedTotal}</td>
|
|
69
|
+
</tr>
|
|
70
|
+
</tfoot>
|
|
71
|
+
```
|
|
54
72
|
|
|
55
73
|
## Keep browsing state and edit state separate
|
|
56
74
|
|
|
57
75
|
Keep search, filter, accepted records and the open record ID in the host.
|
|
58
|
-
Keep the
|
|
76
|
+
Keep the DataTable and its ScrollArea mounted while the Drawer is open. Reset the
|
|
59
77
|
scroll position only when the user changes the query or filter.
|
|
60
78
|
|
|
61
79
|
Entering edit mode copies the selected record. Cancel edit returns to details
|
|
62
80
|
without changing accepted data. Saving replaces the record by stable ID.
|
|
63
|
-
Recompute matches using the existing search and filter.
|
|
64
|
-
|
|
81
|
+
Recompute matches using the existing search and filter. Use a floating ToastRegion
|
|
82
|
+
for the save result so it cannot move the table. If the updated record leaves
|
|
83
|
+
the results, explain why and preserve the query. For a remote save, close
|
|
65
84
|
on accepted data and retain the draft with actionable feedback on failure.
|
|
66
85
|
|
|
67
86
|
## Restore focus to a useful place
|
|
@@ -69,8 +88,9 @@ on accepted data and retain the draft with actionable feedback on failure.
|
|
|
69
88
|
Forward the Drawer trigger props from the single overlay to the row action
|
|
70
89
|
that opens it. Focus Edit request when details open, Subject when editing starts,
|
|
71
90
|
and Edit request after Cancel edit. On close, return focus to the same row
|
|
72
|
-
action without scrolling.
|
|
73
|
-
|
|
91
|
+
action without scrolling. Drawer resolves `getFallbackFocus` after closing
|
|
92
|
+
if the row left the results. The recipe returns Search requests and keeps
|
|
93
|
+
the current query.
|
|
74
94
|
|
|
75
95
|
Keep subject and ID together in the list. Status earns a stable trailing slot
|
|
76
96
|
because users filter and compare by it. Put owner and the longer summary in the
|
|
@@ -91,9 +111,10 @@ Drawer's conditional detail content.
|
|
|
91
111
|
|
|
92
112
|
import {useEffect, useId, useLayoutEffect, useRef, useState, type FormEvent} from "react";
|
|
93
113
|
import {
|
|
94
|
-
|
|
95
|
-
Field, Grid, Heading, Inline, Input,
|
|
96
|
-
Stack, StatusIndicator, Surface,
|
|
114
|
+
Button, DataTable, DescriptionList, Drawer, EmptyState,
|
|
115
|
+
Field, Grid, Heading, Inline, Input, SearchInput, Section, Select,
|
|
116
|
+
Stack, StatusIndicator, Surface, Table, ToastRegion,
|
|
117
|
+
type DataTableColumn, type ToastItem,
|
|
97
118
|
} from "@flood-md/react";
|
|
98
119
|
|
|
99
120
|
const statuses = {
|
|
@@ -111,6 +132,24 @@ type RequestRecord = {
|
|
|
111
132
|
summary: string;
|
|
112
133
|
};
|
|
113
134
|
|
|
135
|
+
const requestColumns: DataTableColumn[] = [
|
|
136
|
+
{id: "subject", label: "Subject", sortable: true},
|
|
137
|
+
{id: "status", label: "Status", sortable: true},
|
|
138
|
+
];
|
|
139
|
+
|
|
140
|
+
const activityByRequest: Record<string, readonly {date: string; activity: string}[]> = {
|
|
141
|
+
"REQ-105": [
|
|
142
|
+
{date: "2026-11-10", activity: "Access issue reported"},
|
|
143
|
+
{date: "2026-11-11", activity: "Identity verified"},
|
|
144
|
+
{date: "2026-11-12", activity: "Recovery link sent"},
|
|
145
|
+
],
|
|
146
|
+
"REQ-117": [
|
|
147
|
+
{date: "2026-11-11", activity: "Permission review requested"},
|
|
148
|
+
{date: "2026-11-12", activity: "Regional roles exported"},
|
|
149
|
+
],
|
|
150
|
+
};
|
|
151
|
+
const activityDate = new Intl.DateTimeFormat("en", {month: "short", day: "numeric", timeZone: "UTC"});
|
|
152
|
+
|
|
114
153
|
const fixtures: RequestRecord[] = [
|
|
115
154
|
{id: "REQ-104", title: "Update billing address", owner: "Mina Chen", status: "new", summary: "Use the new address on future invoices."},
|
|
116
155
|
{id: "REQ-105", title: "Restore workspace access", owner: "Alex Morgan", status: "active", summary: "A teammate cannot sign in after changing email."},
|
|
@@ -143,25 +182,20 @@ export function RecordManagement() {
|
|
|
143
182
|
const [status, setStatus] = useState<Status | "all">("all");
|
|
144
183
|
const [recordId, setRecordId] = useState<string | null>(null);
|
|
145
184
|
const [draft, setDraft] = useState<RequestRecord | null>(null);
|
|
146
|
-
const [notice, setNotice] = useState(
|
|
185
|
+
const [notice, setNotice] = useState<ToastItem | null>(null);
|
|
147
186
|
const search = useRef<HTMLInputElement>(null);
|
|
148
|
-
const
|
|
187
|
+
const table = useRef<HTMLDivElement>(null);
|
|
149
188
|
const subject = useRef<HTMLInputElement>(null);
|
|
150
189
|
const edit = useRef<HTMLButtonElement>(null);
|
|
151
|
-
const opener = useRef<HTMLButtonElement>(null);
|
|
152
|
-
const restore = useRef(false);
|
|
153
190
|
const returnToEdit = useRef(false);
|
|
154
191
|
const selected = records.find(record => record.id === recordId);
|
|
192
|
+
const activity = selected ? activityByRequest[selected.id] : undefined;
|
|
155
193
|
const results = records.filter(record => matchesRequest(record, query, status));
|
|
156
194
|
const editing = draft !== null;
|
|
157
195
|
const open = selected !== undefined;
|
|
158
196
|
|
|
159
197
|
useBrowserLayoutEffect(() => {
|
|
160
|
-
if (
|
|
161
|
-
restore.current = false;
|
|
162
|
-
const target = opener.current?.isConnected ? opener.current : search.current;
|
|
163
|
-
target?.focus({preventScroll: true});
|
|
164
|
-
} else if (editing) {
|
|
198
|
+
if (editing) {
|
|
165
199
|
subject.current?.focus({preventScroll: true});
|
|
166
200
|
} else if (open && returnToEdit.current) {
|
|
167
201
|
returnToEdit.current = false;
|
|
@@ -172,12 +206,11 @@ export function RecordManagement() {
|
|
|
172
206
|
function changeQuery(nextQuery: string, nextStatus: Status | "all") {
|
|
173
207
|
setQuery(nextQuery);
|
|
174
208
|
setStatus(nextStatus);
|
|
175
|
-
setNotice(
|
|
176
|
-
|
|
209
|
+
setNotice(null);
|
|
210
|
+
table.current?.querySelector('[role="region"][aria-label="Requests"]')?.scrollTo({top: 0});
|
|
177
211
|
}
|
|
178
212
|
|
|
179
213
|
function closeRecord() {
|
|
180
|
-
restore.current = true;
|
|
181
214
|
returnToEdit.current = false;
|
|
182
215
|
setDraft(null);
|
|
183
216
|
setRecordId(null);
|
|
@@ -194,9 +227,12 @@ export function RecordManagement() {
|
|
|
194
227
|
}
|
|
195
228
|
const updated = {...draft, title, owner: draft.owner.trim()};
|
|
196
229
|
setRecords(current => current.map(record => record.id === updated.id ? updated : record));
|
|
197
|
-
setNotice(
|
|
198
|
-
|
|
199
|
-
: `${updated.id} updated
|
|
230
|
+
setNotice({
|
|
231
|
+
id: updated.id,
|
|
232
|
+
title: `${updated.id} updated`,
|
|
233
|
+
description: matchesRequest(updated, query, status) ? undefined : "It no longer matches your search or filter.",
|
|
234
|
+
status: "success",
|
|
235
|
+
});
|
|
200
236
|
closeRecord();
|
|
201
237
|
}
|
|
202
238
|
|
|
@@ -231,35 +267,37 @@ export function RecordManagement() {
|
|
|
231
267
|
open={open}
|
|
232
268
|
onOpenChange={open => { if (!open) closeRecord(); }}
|
|
233
269
|
initialFocusRef={edit}
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
270
|
+
getFallbackFocus={() => search.current}
|
|
271
|
+
trigger={({ref, onClick, ...trigger}) =>
|
|
272
|
+
<Surface elevation="raised" padding="0" className="request-table-frame">
|
|
273
|
+
<DataTable ref={table} caption="Requests" hideCaption inset="roomy" minWidth="min-content"
|
|
274
|
+
maxViewportHeight="26rem" stickyHeader pageSize={fixtures.length}
|
|
275
|
+
columns={requestColumns} rows={results.map(record => ({
|
|
276
|
+
id: record.id,
|
|
277
|
+
cells: {status: <StatusIndicator {...statuses[record.status]} size="sm" />},
|
|
278
|
+
sortValues: {status: statuses[record.status].label},
|
|
279
|
+
primaryAction: {
|
|
280
|
+
column: "subject", title: record.title, description: record.id,
|
|
281
|
+
buttonProps: {
|
|
282
|
+
...trigger,
|
|
283
|
+
"aria-expanded": open && record.id === recordId,
|
|
284
|
+
"aria-label": `${record.title}, ${record.id}`,
|
|
285
|
+
onClick: event => {
|
|
286
|
+
ref(event.currentTarget);
|
|
287
|
+
setRecordId(record.id);
|
|
288
|
+
onClick?.(event);
|
|
289
|
+
},
|
|
290
|
+
},
|
|
291
|
+
},
|
|
292
|
+
}))}
|
|
293
|
+
emptyContent={<EmptyState title="No matching requests" actions={<Button variant="subtle" onClick={() => {
|
|
294
|
+
search.current?.focus({preventScroll: true});
|
|
295
|
+
changeQuery("", "all");
|
|
296
|
+
}}>Clear filters</Button>} />} />
|
|
259
297
|
</Surface>}
|
|
260
298
|
footer={draft
|
|
261
|
-
? <Inline gap="2"><Button
|
|
262
|
-
<Button
|
|
299
|
+
? <Inline gap="2"><Button variant="subtle" onClick={cancelEdit}>Cancel edit</Button>
|
|
300
|
+
<Button type="submit" form={formId}>Save changes</Button></Inline>
|
|
263
301
|
: <Button ref={edit} onClick={() => { if (selected) setDraft({...selected}); }}>Edit request</Button>}
|
|
264
302
|
>
|
|
265
303
|
{draft
|
|
@@ -283,13 +321,23 @@ export function RecordManagement() {
|
|
|
283
321
|
</Field>
|
|
284
322
|
</Stack>
|
|
285
323
|
</form>
|
|
286
|
-
: selected && <DescriptionList items={[
|
|
324
|
+
: selected && <Stack gap="8"><DescriptionList items={[
|
|
287
325
|
{id: "request-id", term: "Request ID", description: selected.id},
|
|
288
326
|
{id: "owner", term: "Owner", description: selected.owner || "Unassigned"},
|
|
289
327
|
{id: "status", term: "Status", description: <StatusIndicator {...statuses[selected.status]} />},
|
|
290
|
-
]} />
|
|
328
|
+
]} />
|
|
329
|
+
{activity && <Section title="Activity" headingLevel={3} headingSize="group">
|
|
330
|
+
<Table caption="Request activity" hideCaption density="compact">
|
|
331
|
+
<thead><tr><th scope="col">Date</th><th scope="col">Event</th></tr></thead>
|
|
332
|
+
<tbody>{activity.map(item => <tr key={item.date}>
|
|
333
|
+
<td><time dateTime={item.date}>{activityDate.format(new Date(item.date))}</time></td>
|
|
334
|
+
<td>{item.activity}</td>
|
|
335
|
+
</tr>)}</tbody>
|
|
336
|
+
</Table>
|
|
337
|
+
</Section>}
|
|
338
|
+
</Stack>}
|
|
291
339
|
</Drawer>
|
|
292
|
-
{notice
|
|
340
|
+
<ToastRegion items={notice ? [notice] : []} onDismiss={() => setNotice(null)} />
|
|
293
341
|
</Stack>
|
|
294
342
|
</section>;
|
|
295
343
|
}
|
package/knowledge.json
CHANGED
|
@@ -21,6 +21,11 @@
|
|
|
21
21
|
"name": "Data & feedback",
|
|
22
22
|
"description": "Structured content, files and honest operation states."
|
|
23
23
|
},
|
|
24
|
+
{
|
|
25
|
+
"id": "charts",
|
|
26
|
+
"name": "Charts",
|
|
27
|
+
"description": "Visualize trends, comparisons, distributions and change."
|
|
28
|
+
},
|
|
24
29
|
{
|
|
25
30
|
"id": "quality",
|
|
26
31
|
"name": "Quality",
|
|
@@ -65,10 +70,16 @@
|
|
|
65
70
|
"verification": [
|
|
66
71
|
"quality"
|
|
67
72
|
],
|
|
73
|
+
"visual-review": [
|
|
74
|
+
"quality"
|
|
75
|
+
],
|
|
68
76
|
"interface-content": [
|
|
69
77
|
"composition",
|
|
70
78
|
"quality"
|
|
71
79
|
],
|
|
80
|
+
"screen-composition": [
|
|
81
|
+
"composition"
|
|
82
|
+
],
|
|
72
83
|
"hierarchy": [
|
|
73
84
|
"foundations",
|
|
74
85
|
"composition"
|
|
@@ -247,6 +258,10 @@
|
|
|
247
258
|
"content": [
|
|
248
259
|
"data-feedback"
|
|
249
260
|
],
|
|
261
|
+
"charts": [
|
|
262
|
+
"charts",
|
|
263
|
+
"data-feedback"
|
|
264
|
+
],
|
|
250
265
|
"feedback": [
|
|
251
266
|
"data-feedback"
|
|
252
267
|
]
|
|
@@ -328,6 +343,7 @@
|
|
|
328
343
|
}
|
|
329
344
|
],
|
|
330
345
|
"documentGroups": {
|
|
346
|
+
"charts.md": ["charts", "data-feedback", "composition"],
|
|
331
347
|
"recipes/record-management.md": ["data-feedback", "composition"],
|
|
332
348
|
"recipes/settings-workspace.md": ["interaction", "composition"],
|
|
333
349
|
"README.md": [
|
package/lib/inspect.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export interface InspectionTarget {tag:string;id:string|null;className:string;label:string;}
|
|
2
|
-
export interface InspectionIssue {id:'icons.text-glyph'|'icons.unslotted-svg'|'table.mixed-row-centre'|'table.word-fragment'|'copy.action-echo'|'copy.unowned-support'|'forms.track-drift'|'forms.content-track-drift'|'layout.page-overflow';target:InspectionTarget;related?:InspectionTarget;message:string;offset?:number;word?:string;}
|
|
2
|
+
export interface InspectionIssue {id:'icons.text-glyph'|'icons.unslotted-svg'|'table.mixed-row-centre'|'table.word-fragment'|'copy.action-echo'|'copy.unowned-support'|'forms.track-drift'|'forms.content-track-drift'|'layout.page-overflow'|'layout.record-scroll-owner';target:InspectionTarget;related?:InspectionTarget;message:string;offset?:number;word?:string;}
|
|
3
3
|
export interface RenderedUIInspection {
|
|
4
4
|
schemaVersion:1;issues:InspectionIssue[];copy:InspectionTarget[];
|
|
5
5
|
fields:(InspectionTarget&{x:number;y:number;width:number;height:number})[];
|
|
@@ -7,6 +7,18 @@ export interface RenderedUIInspection {
|
|
|
7
7
|
}
|
|
8
8
|
/** Invoke inside a browser document, e.g. page.evaluate(inspectRenderedUI). No mutation or network calls. */
|
|
9
9
|
export declare function inspectRenderedUI(options?:{rootSelector?:string;maxElements?:number}):RenderedUIInspection;
|
|
10
|
-
export interface
|
|
10
|
+
export interface WorkspaceScrollOptions {
|
|
11
|
+
/** Selector for one full-window AppShell. Defaults to .flood-app-shell. */
|
|
12
|
+
rootSelector?: string;
|
|
13
|
+
/** Set true for overflowing fixtures or false for fixtures whose content must fit. */
|
|
14
|
+
expectOverflow?: boolean;
|
|
15
|
+
}
|
|
16
|
+
/** Run with page.evaluate after the workspace settles. Throws on a broken scroll contract. */
|
|
17
|
+
export declare function assertWorkspaceScroll(options?: WorkspaceScrollOptions): {
|
|
18
|
+
documentGutter: number;
|
|
19
|
+
documentOverflow: number;
|
|
20
|
+
workspaceOverflow: number;
|
|
21
|
+
};
|
|
22
|
+
export interface SourceUIIssue {id:'composition.native-dialog'|'composition.native-control'|'composition.page-scroll'|'composition.layout-owner'|'composition.scaled-minimum';line:number;column:number;excerpt:string;owner:string;message:string;}
|
|
11
23
|
/** Text observations, not an AST or conformance check. No file access, execution or network. */
|
|
12
24
|
export declare function inspectSourceUI(source:string,options?:{format?:'jsx'|'css';maxFindings?:number}):{schemaVersion:1;issues:SourceUIIssue[];truncated:boolean;notChecked:string[]};
|