@twoux/ui 0.0.0-stage → 0.1.0
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/LICENSE.md +61 -0
- package/LICENSES/Apache-2.0.txt +202 -0
- package/NOTICE.md +64 -0
- package/SHADCN-DIFF.md +228 -0
- package/agent/AGENTS.md +358 -0
- package/agent/docs-md/accessibility.md +49 -0
- package/agent/docs-md/action-catalogue.md +81 -0
- package/agent/docs-md/actions.md +74 -0
- package/agent/docs-md/agent-activity-log.md +70 -0
- package/agent/docs-md/agent-permissions.md +82 -0
- package/agent/docs-md/agent-run.md +90 -0
- package/agent/docs-md/agents-on-behalf.md +74 -0
- package/agent/docs-md/ai-column.md +85 -0
- package/agent/docs-md/ai-origin-label.md +70 -0
- package/agent/docs-md/announcing-features.md +73 -0
- package/agent/docs-md/answer-actions.md +81 -0
- package/agent/docs-md/app-shell.md +114 -0
- package/agent/docs-md/artifact-card.md +89 -0
- package/agent/docs-md/ask-on-screen.md +72 -0
- package/agent/docs-md/assistant-page.md +100 -0
- package/agent/docs-md/assistant-panel.md +142 -0
- package/agent/docs-md/avatar.md +82 -0
- package/agent/docs-md/banner.md +106 -0
- package/agent/docs-md/board.md +163 -0
- package/agent/docs-md/breadcrumb.md +89 -0
- package/agent/docs-md/breakdown-table.md +97 -0
- package/agent/docs-md/bulk-action-bar.md +112 -0
- package/agent/docs-md/button.md +293 -0
- package/agent/docs-md/card.md +115 -0
- package/agent/docs-md/change-list.md +99 -0
- package/agent/docs-md/change-request.md +166 -0
- package/agent/docs-md/chart-area.md +87 -0
- package/agent/docs-md/chart-bar.md +100 -0
- package/agent/docs-md/chart-box-plot.md +88 -0
- package/agent/docs-md/chart-bullet.md +89 -0
- package/agent/docs-md/chart-candlestick.md +112 -0
- package/agent/docs-md/chart-card-line.md +120 -0
- package/agent/docs-md/chart-donut.md +90 -0
- package/agent/docs-md/chart-drill-down.md +72 -0
- package/agent/docs-md/chart-funnel.md +71 -0
- package/agent/docs-md/chart-heatmap.md +76 -0
- package/agent/docs-md/chart-histogram.md +81 -0
- package/agent/docs-md/chart-map.md +165 -0
- package/agent/docs-md/chart-mixed.md +88 -0
- package/agent/docs-md/chart-price.md +92 -0
- package/agent/docs-md/chart-radial-bars.md +109 -0
- package/agent/docs-md/chart-radial-lines.md +111 -0
- package/agent/docs-md/chart-sankey.md +104 -0
- package/agent/docs-md/chart-scatter.md +87 -0
- package/agent/docs-md/chart-share-bar.md +89 -0
- package/agent/docs-md/chart-treemap.md +98 -0
- package/agent/docs-md/chart-waterfall.md +93 -0
- package/agent/docs-md/checkbox.md +131 -0
- package/agent/docs-md/choosing-selection-control.md +71 -0
- package/agent/docs-md/citations-sources.md +79 -0
- package/agent/docs-md/citing-sources.md +63 -0
- package/agent/docs-md/clarify-before-acting.md +69 -0
- package/agent/docs-md/clarifying-question.md +92 -0
- package/agent/docs-md/code-block.md +174 -0
- package/agent/docs-md/colours.md +87 -0
- package/agent/docs-md/columns-menu.md +140 -0
- package/agent/docs-md/combobox.md +161 -0
- package/agent/docs-md/command-palette.md +126 -0
- package/agent/docs-md/confidence-note.md +87 -0
- package/agent/docs-md/configurable-dashboard.md +76 -0
- package/agent/docs-md/confirmation-dialog.md +93 -0
- package/agent/docs-md/connected-tools.md +80 -0
- package/agent/docs-md/connector-picker.md +127 -0
- package/agent/docs-md/consistency.md +53 -0
- package/agent/docs-md/content-density.md +61 -0
- package/agent/docs-md/conversation-history.md +78 -0
- package/agent/docs-md/create-record.md +80 -0
- package/agent/docs-md/csv-import.md +88 -0
- package/agent/docs-md/dashboard-items.md +78 -0
- package/agent/docs-md/data-export.md +61 -0
- package/agent/docs-md/data-grid.md +137 -0
- package/agent/docs-md/data-vis-colours.md +76 -0
- package/agent/docs-md/data-visualisation.md +76 -0
- package/agent/docs-md/date-picker.md +178 -0
- package/agent/docs-md/delete-record.md +75 -0
- package/agent/docs-md/dense-table.md +495 -0
- package/agent/docs-md/design-tokens.md +53 -0
- package/agent/docs-md/disabled-read-only.md +75 -0
- package/agent/docs-md/drag-and-drop.md +74 -0
- package/agent/docs-md/dropdown-menu.md +137 -0
- package/agent/docs-md/edit-record.md +73 -0
- package/agent/docs-md/empty-loading-error.md +78 -0
- package/agent/docs-md/empty-state.md +128 -0
- package/agent/docs-md/error-messages.md +75 -0
- package/agent/docs-md/error-state.md +123 -0
- package/agent/docs-md/export-menu.md +86 -0
- package/agent/docs-md/feedback-prompt.md +88 -0
- package/agent/docs-md/field-mapping.md +78 -0
- package/agent/docs-md/file-drop.md +110 -0
- package/agent/docs-md/filter-bar.md +138 -0
- package/agent/docs-md/filter-panel.md +103 -0
- package/agent/docs-md/filtering-search.md +79 -0
- package/agent/docs-md/follow-ups-starters.md +79 -0
- package/agent/docs-md/form-validation.md +95 -0
- package/agent/docs-md/generated-columns.md +65 -0
- package/agent/docs-md/generated-drafts-files.md +71 -0
- package/agent/docs-md/grouped-list.md +89 -0
- package/agent/docs-md/guide-cue.md +170 -0
- package/agent/docs-md/help-panel.md +103 -0
- package/agent/docs-md/help-system.md +74 -0
- package/agent/docs-md/iconography.md +49 -0
- package/agent/docs-md/inbox.md +108 -0
- package/agent/docs-md/inline-editing-validation.md +80 -0
- package/agent/docs-md/inline-input-in-answer.md +70 -0
- package/agent/docs-md/items-palette.md +116 -0
- package/agent/docs-md/keyboard-model.md +47 -0
- package/agent/docs-md/keyboard-navigation.md +68 -0
- package/agent/docs-md/keyboard-shortcut-hint.md +87 -0
- package/agent/docs-md/keyboard-shortcut-sheet.md +90 -0
- package/agent/docs-md/labelling-ai-output.md +66 -0
- package/agent/docs-md/layout.md +47 -0
- package/agent/docs-md/list-table.md +254 -0
- package/agent/docs-md/loading-skeleton.md +91 -0
- package/agent/docs-md/managing-agents.md +71 -0
- package/agent/docs-md/message-composer.md +105 -0
- package/agent/docs-md/migrating.md +49 -0
- package/agent/docs-md/motion.md +93 -0
- package/agent/docs-md/navigation.md +76 -0
- package/agent/docs-md/notification-channels.md +65 -0
- package/agent/docs-md/notifications-decision.md +100 -0
- package/agent/docs-md/onboarding-step.md +114 -0
- package/agent/docs-md/order-book.md +108 -0
- package/agent/docs-md/page-header.md +96 -0
- package/agent/docs-md/pagination.md +88 -0
- package/agent/docs-md/pane-layout.md +105 -0
- package/agent/docs-md/period-comparison.md +66 -0
- package/agent/docs-md/plan-card.md +120 -0
- package/agent/docs-md/popover-rules.md +88 -0
- package/agent/docs-md/prompt-editor.md +90 -0
- package/agent/docs-md/propose-apply.md +65 -0
- package/agent/docs-md/provider-logo.md +97 -0
- package/agent/docs-md/radio.md +83 -0
- package/agent/docs-md/ranked-list.md +102 -0
- package/agent/docs-md/reasoning-progress.md +72 -0
- package/agent/docs-md/record-detail.md +71 -0
- package/agent/docs-md/regenerating-answers.md +70 -0
- package/agent/docs-md/run-status-bar.md +94 -0
- package/agent/docs-md/saved-tasks-schedules.md +63 -0
- package/agent/docs-md/saved-views.md +149 -0
- package/agent/docs-md/saving-views.md +68 -0
- package/agent/docs-md/scheduler.md +258 -0
- package/agent/docs-md/secondary-panels.md +72 -0
- package/agent/docs-md/segmented-control.md +88 -0
- package/agent/docs-md/selection-bulk-actions.md +70 -0
- package/agent/docs-md/separator.md +75 -0
- package/agent/docs-md/setup-checklist.md +92 -0
- package/agent/docs-md/shape-elevation.md +55 -0
- package/agent/docs-md/sheet-grid.md +211 -0
- package/agent/docs-md/side-panel.md +134 -0
- package/agent/docs-md/sign-in.md +114 -0
- package/agent/docs-md/spacing.md +55 -0
- package/agent/docs-md/split-button.md +100 -0
- package/agent/docs-md/stat-compare.md +82 -0
- package/agent/docs-md/static-dashboard.md +75 -0
- package/agent/docs-md/status-badge.md +140 -0
- package/agent/docs-md/step-card.md +75 -0
- package/agent/docs-md/stepper.md +92 -0
- package/agent/docs-md/suggested-changes.md +91 -0
- package/agent/docs-md/switch.md +78 -0
- package/agent/docs-md/table-settings.md +81 -0
- package/agent/docs-md/tabs.md +85 -0
- package/agent/docs-md/tag-input.md +108 -0
- package/agent/docs-md/tag.md +79 -0
- package/agent/docs-md/task-builder.md +87 -0
- package/agent/docs-md/team-instructions.md +72 -0
- package/agent/docs-md/templates-gallery.md +85 -0
- package/agent/docs-md/text-input.md +160 -0
- package/agent/docs-md/thinking-steps.md +89 -0
- package/agent/docs-md/timeline.md +189 -0
- package/agent/docs-md/timestamp.md +84 -0
- package/agent/docs-md/timestamps.md +70 -0
- package/agent/docs-md/toast.md +117 -0
- package/agent/docs-md/tooltip.md +90 -0
- package/agent/docs-md/typography.md +62 -0
- package/agent/docs-md/undo-destructive.md +66 -0
- package/agent/docs-md/unsaved-changes.md +71 -0
- package/agent/docs-md/usage-meter.md +80 -0
- package/agent/docs-md/user-feedback.md +71 -0
- package/agent/docs-md/voice-and-tone.md +60 -0
- package/agent/docs-md/why-choice.md +93 -0
- package/agent/docs-md/workspace-onboarding.md +74 -0
- package/agent/docs-md/writing.md +48 -0
- package/agent/llms-full.txt +18369 -0
- package/agent/llms.txt +198 -0
- package/agent/skills/twoux-audit/SKILL.md +49 -0
- package/agent/skills/twoux-build/SKILL.md +39 -0
- package/agent/skills/twoux-design/SKILL.md +54 -0
- package/agent/skills/twoux-write/SKILL.md +32 -0
- package/bin/check.mjs +123 -0
- package/bin/docs.mjs +24 -0
- package/bin/guard.mjs +27 -0
- package/bin/init.mjs +113 -0
- package/bin/knowledge.mjs +87 -0
- package/bin/mcp.mjs +116 -0
- package/bin/theme.mjs +168 -0
- package/bin/twoux.mjs +25 -0
- package/package.json +80 -3
- package/registry/jobs.json +1028 -0
- package/registry/palette.json +770 -0
- package/registry/rules.json +913 -0
- package/src/components/agent-review.tsx +364 -0
- package/src/components/ai-basics.tsx +212 -0
- package/src/components/ai-work.tsx +304 -0
- package/src/components/ai.tsx +227 -0
- package/src/components/alert-dialog.tsx +203 -0
- package/src/components/alert.tsx +86 -0
- package/src/components/avatar.tsx +114 -0
- package/src/components/badge.tsx +59 -0
- package/src/components/board.tsx +397 -0
- package/src/components/breadcrumb.tsx +114 -0
- package/src/components/button-group.tsx +90 -0
- package/src/components/button.tsx +109 -0
- package/src/components/calendar.tsx +247 -0
- package/src/components/card.tsx +102 -0
- package/src/components/chart-map.tsx +336 -0
- package/src/components/charts-more.tsx +1331 -0
- package/src/components/charts-radial.tsx +328 -0
- package/src/components/charts.tsx +390 -0
- package/src/components/checkbox.tsx +40 -0
- package/src/components/code.tsx +139 -0
- package/src/components/collapsible.tsx +37 -0
- package/src/components/combobox.tsx +339 -0
- package/src/components/command-extras.tsx +11 -0
- package/src/components/command.tsx +196 -0
- package/src/components/data-grid.tsx +213 -0
- package/src/components/data-table.tsx +334 -0
- package/src/components/data-views.tsx +740 -0
- package/src/components/date-picker.tsx +686 -0
- package/src/components/dense-table.tsx +207 -0
- package/src/components/dialog.tsx +162 -0
- package/src/components/dropdown-menu.tsx +264 -0
- package/src/components/empty.tsx +114 -0
- package/src/components/feedback-prompt.tsx +80 -0
- package/src/components/field.tsx +254 -0
- package/src/components/fields.tsx +112 -0
- package/src/components/flow.tsx +840 -0
- package/src/components/guide-cue.tsx +273 -0
- package/src/components/inbox.tsx +113 -0
- package/src/components/input-group.tsx +176 -0
- package/src/components/input.tsx +26 -0
- package/src/components/kbd.tsx +34 -0
- package/src/components/label.tsx +27 -0
- package/src/components/labels.tsx +176 -0
- package/src/components/motion.tsx +136 -0
- package/src/components/onboarding.tsx +454 -0
- package/src/components/order-book.tsx +185 -0
- package/src/components/pane-layout.tsx +417 -0
- package/src/components/popover.tsx +96 -0
- package/src/components/provider-logos.data.ts +49 -0
- package/src/components/radio-group.tsx +52 -0
- package/src/components/scheduler.tsx +997 -0
- package/src/components/segmented-control.tsx +28 -0
- package/src/components/select.tsx +195 -0
- package/src/components/separator.tsx +31 -0
- package/src/components/sheet.tsx +148 -0
- package/src/components/shell.tsx +426 -0
- package/src/components/shortcut-sheet.tsx +83 -0
- package/src/components/skeleton.tsx +18 -0
- package/src/components/sonner.tsx +107 -0
- package/src/components/spinner.tsx +13 -0
- package/src/components/split-button.tsx +58 -0
- package/src/components/stepper.tsx +78 -0
- package/src/components/switch.tsx +43 -0
- package/src/components/table.tsx +126 -0
- package/src/components/tabs.tsx +118 -0
- package/src/components/tag-input.tsx +253 -0
- package/src/components/textarea.tsx +21 -0
- package/src/components/theme.tsx +80 -0
- package/src/components/timeline.tsx +408 -0
- package/src/components/toggle-group.tsx +87 -0
- package/src/components/toggle.tsx +54 -0
- package/src/components/tooltip.tsx +65 -0
- package/src/components/workspace-actions.tsx +294 -0
- package/src/components/workspace-import.tsx +465 -0
- package/src/components/workspace.tsx +1884 -0
- package/src/index.ts +80 -0
- package/src/lib/active-indicator.tsx +53 -0
- package/src/lib/chart-kit.tsx +72 -0
- package/src/lib/cn.ts +6 -0
- package/src/lib/shortcuts.tsx +61 -0
- package/src/lib/workspace.tsx +10 -0
- package/src/shadcn/alert-dialog.tsx +196 -0
- package/src/shadcn/alert.tsx +65 -0
- package/src/shadcn/avatar.tsx +108 -0
- package/src/shadcn/badge.tsx +47 -0
- package/src/shadcn/breadcrumb.tsx +108 -0
- package/src/shadcn/button-group.tsx +83 -0
- package/src/shadcn/button.tsx +63 -0
- package/src/shadcn/calendar.tsx +220 -0
- package/src/shadcn/card.tsx +91 -0
- package/src/shadcn/checkbox.tsx +31 -0
- package/src/shadcn/collapsible.tsx +33 -0
- package/src/shadcn/command.tsx +184 -0
- package/src/shadcn/dialog.tsx +158 -0
- package/src/shadcn/dropdown-menu.tsx +256 -0
- package/src/shadcn/empty.tsx +103 -0
- package/src/shadcn/field.tsx +248 -0
- package/src/shadcn/input-group.tsx +170 -0
- package/src/shadcn/input.tsx +20 -0
- package/src/shadcn/kbd.tsx +28 -0
- package/src/shadcn/label.tsx +23 -0
- package/src/shadcn/popover.tsx +88 -0
- package/src/shadcn/radio-group.tsx +44 -0
- package/src/shadcn/select.tsx +189 -0
- package/src/shadcn/separator.tsx +27 -0
- package/src/shadcn/sheet.tsx +142 -0
- package/src/shadcn/skeleton.tsx +13 -0
- package/src/shadcn/sonner.tsx +40 -0
- package/src/shadcn/switch.tsx +34 -0
- package/src/shadcn/table.tsx +115 -0
- package/src/shadcn/tabs.tsx +90 -0
- package/src/shadcn/textarea.tsx +17 -0
- package/src/shadcn/toggle-group.tsx +83 -0
- package/src/shadcn/toggle.tsx +46 -0
- package/src/shadcn/tooltip.tsx +56 -0
- package/src/styles/palette.css +127 -0
- package/src/styles/theme.css +35 -0
- package/src/styles/twoux.css +236 -0
- package/theme.json +14 -0
- package/README.md +0 -3
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# Selection and bulk actions
|
|
2
|
+
|
|
3
|
+
Pick rows with X or checkboxes, then act on all of them from one bar above the table that says how many rows it will touch.
|
|
4
|
+
|
|
5
|
+
Kind: pattern · Group: Patterns: general
|
|
6
|
+
|
|
7
|
+
**Problem:** Extending, closing or exporting listings one by one is slow, and bulk actions placed in different spots on each page are missed or misfired.
|
|
8
|
+
|
|
9
|
+
**Solution:** Selecting rows shows the bulk action bar directly above the table, with the actions, the count and Esc to clear. Filters and column headers stay where they were.
|
|
10
|
+
|
|
11
|
+
**Built from:** List table, Data grid, Bulk action bar, Checkbox, Toast, Confirmation dialog, Keyboard shortcut hint
|
|
12
|
+
|
|
13
|
+
## How it works
|
|
14
|
+
|
|
15
|
+
1. Select a row with its checkbox or X; ⇧J extends the selection downward. The header checkbox selects only the rows on the current page.
|
|
16
|
+
2. The bulk action bar appears directly above the table: the actions with their shortcuts on the left, the count ("2 listings selected") and Clear selection (Esc) on the right.
|
|
17
|
+
3. If more rows match than are on screen, the bar offers "Select all 412", and the count updates to say so.
|
|
18
|
+
4. Choosing an action runs it on the selected rows. Reversible actions show a toast with Undo; irreversible ones confirm first.
|
|
19
|
+
5. If an action cannot apply to some selected rows, say how many and why before it runs.
|
|
20
|
+
|
|
21
|
+
## When to use
|
|
22
|
+
|
|
23
|
+
- When the same action is often applied to many rows: extend, close, assign, export, ask the assistant.
|
|
24
|
+
|
|
25
|
+
## When not to use
|
|
26
|
+
|
|
27
|
+
- For an action on one record. Use the row's actions or the side panel.
|
|
28
|
+
- When the action needs per-row input, such as different closing dates. Use inline editing in the data grid.
|
|
29
|
+
|
|
30
|
+
## Guidelines
|
|
31
|
+
|
|
32
|
+
- Every bulk action is also available on a single selected row. The bar only adds the multi-row case.
|
|
33
|
+
- Labels are verb plus noun and say how many rows: "Close 2 listings".
|
|
34
|
+
- Keep the filters visible while rows are selected; the bar sits between the filters and the table instead of replacing the toolbar.
|
|
35
|
+
- Changing filters or sort clears the selection, so an action never touches rows the person can no longer see.
|
|
36
|
+
- Bulk results confirm in one toast ("2 listings extended") with Undo, not one toast per row.
|
|
37
|
+
- A bulk action that can't apply to the current selection stays in the bar as unavailable, with a reason: "Available when all selected listings are Live".
|
|
38
|
+
- The bar holds every action that can run on more than one record; row menus never offer "Close all" or similar.
|
|
39
|
+
- Order bar actions the same way as the page header: status actions first, destructive near the end, the main action last.
|
|
40
|
+
- Beyond four actions, the rest go in a More menu in the bar.
|
|
41
|
+
|
|
42
|
+
## Content
|
|
43
|
+
|
|
44
|
+
- Count format: "2 listings selected" in the bar, "2 of 24 selected" in the table footer.
|
|
45
|
+
- Destructive bulk confirmations name the count and the consequence: "Close 2 listings? They stop taking applications now."
|
|
46
|
+
|
|
47
|
+
## Accessibility
|
|
48
|
+
|
|
49
|
+
- Selected rows set aria-selected="true"; the selection is also shown with a tick, not colour alone.
|
|
50
|
+
- Announce the selection count when it changes.
|
|
51
|
+
- The header checkbox shows an indeterminate state when some rows are selected.
|
|
52
|
+
- Unavailable bulk actions use aria-disabled so their reason can be reached by keyboard.
|
|
53
|
+
|
|
54
|
+
## Do
|
|
55
|
+
|
|
56
|
+
- Do keep the count in the bar beside Clear selection, and name it in the toolbar's label so screen readers hear it before the actions.
|
|
57
|
+
- Do offer Undo after a reversible bulk action.
|
|
58
|
+
|
|
59
|
+
## Don't
|
|
60
|
+
|
|
61
|
+
- Don't replace the filter bar with the bulk bar; people lose sight of what they selected from.
|
|
62
|
+
- Don't silently skip rows an action can't apply to.
|
|
63
|
+
|
|
64
|
+
## Related
|
|
65
|
+
|
|
66
|
+
- Undo and destructive actions
|
|
67
|
+
- Keyboard-first navigation
|
|
68
|
+
- Data export
|
|
69
|
+
- Ask about what's on screen
|
|
70
|
+
- Bulk action bar
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# Separator
|
|
2
|
+
|
|
3
|
+
A thin line that divides groups of content or menu items. With a label, it separates two ways of doing the same thing, such as “or”.
|
|
4
|
+
|
|
5
|
+
Kind: component · Group: Content · Status: built, import from @twoux/ui
|
|
6
|
+
|
|
7
|
+
## When to use
|
|
8
|
+
|
|
9
|
+
- To divide groups inside a card, panel or menu when spacing alone is not enough.
|
|
10
|
+
- With a label, between two routes to the same result: “or” between sign in with Google and the email form.
|
|
11
|
+
|
|
12
|
+
## When not to use
|
|
13
|
+
|
|
14
|
+
- Between every row or field. Use spacing; lines everywhere add noise.
|
|
15
|
+
- To separate sections of a page. Use headings and space.
|
|
16
|
+
|
|
17
|
+
## Structure
|
|
18
|
+
|
|
19
|
+
1. Line: 1px, light edge colour
|
|
20
|
+
2. Label (optional): short word centred on the line, on the surface colour
|
|
21
|
+
|
|
22
|
+
## Guidelines
|
|
23
|
+
|
|
24
|
+
- Prefer space first. Add a line only where groups would otherwise run together.
|
|
25
|
+
- Keep the label to one or two words in lower case: “or”, “or continue with”.
|
|
26
|
+
- Lines run the full width of their container, or align with the content inset in menus.
|
|
27
|
+
|
|
28
|
+
## Content
|
|
29
|
+
|
|
30
|
+
- Label in lower case with no full stop: “or”.
|
|
31
|
+
|
|
32
|
+
## Accessibility
|
|
33
|
+
|
|
34
|
+
- A purely visual line is decorative, so screen readers skip it. Mark it as a separator only when it divides a menu or a toolbar.
|
|
35
|
+
- The label is real text, so it is read in order between the two groups.
|
|
36
|
+
|
|
37
|
+
## Keyboard
|
|
38
|
+
|
|
39
|
+
| Keys | Action |
|
|
40
|
+
|---|---|
|
|
41
|
+
| None | Not focusable |
|
|
42
|
+
|
|
43
|
+
## Examples
|
|
44
|
+
|
|
45
|
+
### plain-and-labelled
|
|
46
|
+
|
|
47
|
+
```tsx
|
|
48
|
+
import { FieldSeparator, Separator } from "@twoux/ui";
|
|
49
|
+
|
|
50
|
+
export const meta = { title: "Plain and labelled", description: "A plain line divides groups when space alone is not enough. A labelled one sits between two routes to the same result.", order: 1 };
|
|
51
|
+
|
|
52
|
+
export default function Example() {
|
|
53
|
+
return (
|
|
54
|
+
<div className="grid w-full max-w-sm gap-6">
|
|
55
|
+
<div className="grid gap-3 rounded-sm border border-line bg-surface p-4">
|
|
56
|
+
<div className="grid gap-0.5">
|
|
57
|
+
<span className="text-base font-medium text-ink">Senior Product Designer</span>
|
|
58
|
+
<span className="text-sm text-ink-3">Leeds · Hybrid</span>
|
|
59
|
+
</div>
|
|
60
|
+
<Separator />
|
|
61
|
+
<div className="flex justify-between text-base text-ink-2">
|
|
62
|
+
<span>Applicants</span>
|
|
63
|
+
<span className="tabular text-ink">38</span>
|
|
64
|
+
</div>
|
|
65
|
+
</div>
|
|
66
|
+
<FieldSeparator>or</FieldSeparator>
|
|
67
|
+
</div>
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Related
|
|
73
|
+
|
|
74
|
+
- Card
|
|
75
|
+
- Dropdown menu
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# Setup checklist
|
|
2
|
+
|
|
3
|
+
A short list in the sidebar of the few things that make the product useful, with progress shown as "2 of 4".
|
|
4
|
+
|
|
5
|
+
Kind: component · Group: Navigation · Status: built, import from @twoux/ui
|
|
6
|
+
|
|
7
|
+
## When to use
|
|
8
|
+
|
|
9
|
+
- Showing the next few things that make an empty workspace useful.
|
|
10
|
+
- Setup that people can do in any order and return to later.
|
|
11
|
+
|
|
12
|
+
## When not to use
|
|
13
|
+
|
|
14
|
+
- A fixed sequence. Use Stepper (stepper) or Onboarding step (onboarding-step).
|
|
15
|
+
- Teaching the interface. Use contextual help, not a modal tour.
|
|
16
|
+
|
|
17
|
+
## Structure
|
|
18
|
+
|
|
19
|
+
1. Heading ("Get set up")
|
|
20
|
+
2. Progress text ("2 of 4") and bar
|
|
21
|
+
3. Tasks (status, label, Start)
|
|
22
|
+
4. Dismiss button
|
|
23
|
+
|
|
24
|
+
## Guidelines
|
|
25
|
+
|
|
26
|
+
- Keep it to three to five tasks that make the product useful.
|
|
27
|
+
- Put it in the sidebar and let people dismiss it.
|
|
28
|
+
- Show progress as text, "2 of 4", with the bar as extra.
|
|
29
|
+
- Start opens the task in place. Never a modal tour or a full-page takeover.
|
|
30
|
+
- When everything is done, collapse it or let it disappear.
|
|
31
|
+
|
|
32
|
+
## Content
|
|
33
|
+
|
|
34
|
+
- Tasks begin with a verb: "Connect Slack", "Invite a teammate".
|
|
35
|
+
- Start button text stays "Start"; the task name is read out with it.
|
|
36
|
+
|
|
37
|
+
## Accessibility
|
|
38
|
+
|
|
39
|
+
- The checklist is a labelled section with a list; done tasks say "done" for assistive technology (SC 1.3.1, 1.4.1).
|
|
40
|
+
- Start and Dismiss have names that include what they act on (SC 2.4.6).
|
|
41
|
+
- The progress bar is decorative; the count is text (SC 1.4.1).
|
|
42
|
+
|
|
43
|
+
## Keyboard
|
|
44
|
+
|
|
45
|
+
| Keys | Action |
|
|
46
|
+
|---|---|
|
|
47
|
+
| Tab | Move through Start buttons and Dismiss |
|
|
48
|
+
| Enter or Space | Start a task or hide the checklist |
|
|
49
|
+
|
|
50
|
+
## Examples
|
|
51
|
+
|
|
52
|
+
### sidebar
|
|
53
|
+
|
|
54
|
+
```tsx
|
|
55
|
+
import { Button, SetupChecklist, type SetupItem } from "@twoux/ui";
|
|
56
|
+
import { useState } from "react";
|
|
57
|
+
|
|
58
|
+
export const meta = { title: "Get set up in the sidebar", description: "Start does the task in place. Finish all four and the checklist goes away; Hide closes it sooner.", order: 1 };
|
|
59
|
+
|
|
60
|
+
const TASKS = [
|
|
61
|
+
{ id: "slack", label: "Connect Slack" },
|
|
62
|
+
{ id: "invite", label: "Invite a teammate" },
|
|
63
|
+
{ id: "import", label: "Import your candidates" },
|
|
64
|
+
{ id: "agent", label: "Turn on the daily summary" },
|
|
65
|
+
];
|
|
66
|
+
|
|
67
|
+
export default function Example() {
|
|
68
|
+
const [done, setDone] = useState<string[]>(["slack"]);
|
|
69
|
+
const [hidden, setHidden] = useState(false);
|
|
70
|
+
const items: SetupItem[] = TASKS.map((t) => ({ ...t, done: done.includes(t.id), onStart: () => setDone((d) => [...d, t.id]) }));
|
|
71
|
+
const finished = done.length === TASKS.length;
|
|
72
|
+
return (
|
|
73
|
+
<div className="grid w-full max-w-64 gap-3">
|
|
74
|
+
{!hidden && !finished && <SetupChecklist items={items} onDismiss={() => setHidden(true)} />}
|
|
75
|
+
{(hidden || finished) && (
|
|
76
|
+
<div className="grid gap-2 text-sm text-ink-2">
|
|
77
|
+
<p>{finished ? "Setup is complete." : "The setup checklist is hidden."}</p>
|
|
78
|
+
<Button variant="outline" size="sm" onClick={() => { setDone(["slack"]); setHidden(false); }}>Start again</Button>
|
|
79
|
+
</div>
|
|
80
|
+
)}
|
|
81
|
+
</div>
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## Related
|
|
87
|
+
|
|
88
|
+
- Onboarding to an empty workspace
|
|
89
|
+
- Empty state
|
|
90
|
+
- Onboarding step
|
|
91
|
+
- App shell and sidebar navigation
|
|
92
|
+
- Stepper
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# Shape and elevation
|
|
2
|
+
|
|
3
|
+
Four radii, each tied to a role, and two shadows. The page is flat; only layers that float above it (menus, popovers, toasts, dialogs) get a real shadow.
|
|
4
|
+
|
|
5
|
+
Kind: foundation · Group: Visual foundation
|
|
6
|
+
|
|
7
|
+
## Principles
|
|
8
|
+
|
|
9
|
+
- Radius follows role, and stays tight: 4 to 8px. Small parts are tightest, overlays the roundest.
|
|
10
|
+
- Flat by default. Raised and overlay elevation are used sparingly because they make a UI busy. Borders and surface changes do the grouping.
|
|
11
|
+
- A shadow means 'this floats'. If it casts a --shadow-pop, it sits above the page and can be dismissed.
|
|
12
|
+
|
|
13
|
+
## Guidelines
|
|
14
|
+
|
|
15
|
+
- rounded-xs 4px: parts that sit inside a control or a row. Badges, checkboxes, keycaps, menu and palette items, segments, skeleton bars, tags.
|
|
16
|
+
- rounded-sm 6px: controls and cards. Buttons, inputs, toggles, the segmented track, banners, cards, empty and error states.
|
|
17
|
+
- rounded-md 8px: layers that float. Menus, popovers, the command palette, toasts and dialogs.
|
|
18
|
+
- rounded-lg 12px and rounded-xl 16px stay in the scale for marketing surfaces; product UI does not use them.
|
|
19
|
+
- Fully round (99px or 50%) is only for avatars, dots and pill toggles.
|
|
20
|
+
- --shadow-pop is for overlays only: command menu, menus, popovers, toasts, tooltips and dialogs, the UI that floats over other UI.
|
|
21
|
+
- --shadow-sm is a 1px lift on primary and secondary buttons and the AI composer, a tactile cue rather than elevation.
|
|
22
|
+
- Known drift: the side panel uses a raw shadow value and several components use raw 2, 3 and 5px radii and raw z-index values. These should become tokens (for example a --shadow-panel and a --z-* scale).
|
|
23
|
+
|
|
24
|
+
## Accessibility
|
|
25
|
+
|
|
26
|
+
- A shadow is never the only boundary of a control; it also has a border or fill that meets 3:1 where the boundary matters.
|
|
27
|
+
- Overlays must not hide the element that has focus.
|
|
28
|
+
|
|
29
|
+
## Do
|
|
30
|
+
|
|
31
|
+
- Match radius to role: a button inside a card still uses rounded-sm.
|
|
32
|
+
- Pair --shadow-pop with --surface: each elevation has its own surface and shadow.
|
|
33
|
+
|
|
34
|
+
## Don't
|
|
35
|
+
|
|
36
|
+
- Do not put a shadow on a card that sits on the canvas.
|
|
37
|
+
- Do not invent radii like 5px or 10px.
|
|
38
|
+
|
|
39
|
+
## Tokens
|
|
40
|
+
|
|
41
|
+
| Token | Value | Use |
|
|
42
|
+
|---|---|---|
|
|
43
|
+
| `rounded-xs` | `4px` | Badges, checkboxes, keycaps, menu items, segments |
|
|
44
|
+
| `rounded-sm` | `6px` | Buttons, inputs, toggles, banners, cards |
|
|
45
|
+
| `rounded-md` | `8px` | Menus, popovers, palette, toasts, dialogs |
|
|
46
|
+
| `--shadow-sm` | `0 1px 2px rgba(23,23,40,.05)` | Button lift, composer |
|
|
47
|
+
| `--shadow-pop` | `0 0 0 1px var(--line), 0 10px 28px -8px rgba(23,23,40,.20)` | Floating layers only |
|
|
48
|
+
| `--ring` | `0 0 0 2px var(--surface), 0 0 0 4px color-mix(in srgb, var(--accent) 55%, transparent)` | Focus ring on :focus-visible |
|
|
49
|
+
|
|
50
|
+
## Related
|
|
51
|
+
|
|
52
|
+
- Colours
|
|
53
|
+
- Layout
|
|
54
|
+
- Motion
|
|
55
|
+
- Design tokens
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
# Sheet grid
|
|
2
|
+
|
|
3
|
+
A spreadsheet of records with typed columns, a menu per column, row numbers and a status row for computed columns, and Excel and Google Sheets keys: select ranges, copy and paste to and from either, fill, clear and undo. size="dense" gives 28px rows for spreadsheet-like data.
|
|
4
|
+
|
|
5
|
+
Kind: component · Group: Data · Status: built, import from @twoux/ui
|
|
6
|
+
|
|
7
|
+
## When to use
|
|
8
|
+
|
|
9
|
+
- People build a table of records and add columns that are typed, calculated or filled in per row, such as a work email looked up for every person.
|
|
10
|
+
- The table is wider than the screen and people compare values across columns, so the name column must stay in view.
|
|
11
|
+
- People work as they would in Excel or Google Sheets: they paste blocks of values in, fill down, copy ranges out and undo mistakes.
|
|
12
|
+
- Spreadsheet-like data, such as inventories, ledgers and imports, where people need many rows in view: use size="dense".
|
|
13
|
+
|
|
14
|
+
## When not to use
|
|
15
|
+
|
|
16
|
+
- For scanning and triage of records. Use the List table; a static table is simpler to read.
|
|
17
|
+
- For editing a few columns of known data without computed columns. Use the Data grid.
|
|
18
|
+
- For deep edits of one record. Use the side panel.
|
|
19
|
+
|
|
20
|
+
## Structure
|
|
21
|
+
|
|
22
|
+
1. Header row: type icon (or the service's logo), column name, sort arrow, menu
|
|
23
|
+
2. Filter row (optional): a search field under each header
|
|
24
|
+
3. Status row for computed columns: Up to date, 62%, 3 errors
|
|
25
|
+
4. Pinned row numbers (with checkbox and ⋮ row menu when set) and frozen first columns
|
|
26
|
+
5. Group rows (optional): label, count and each total column's sum, collapsible
|
|
27
|
+
6. Cells: values, Tag for select, Switch with its word, n/a, or Waiting, Writing, Looking up, Failed with the reason, Not found; the selected range in indigo
|
|
28
|
+
7. Totals row (optional), then Add row, Add 10 rows, the status line and the sum and count of the selection
|
|
29
|
+
|
|
30
|
+
## Guidelines
|
|
31
|
+
|
|
32
|
+
- Every column shows its type in the header: an icon for text, number, date and select, the formula mark, the AI origin mark, or the logo of the service an enrichment uses.
|
|
33
|
+
- The grid has one tab stop. Arrow keys move between cells; Up from the first row reaches the column headers, where Enter opens the column menu.
|
|
34
|
+
- The column menu holds rename, change type, sort, filter, wrap text, pin or unpin, column width, hide, insert left or right and delete. Delete happens at once with Undo.
|
|
35
|
+
- Columns resize from the header: drag its right edge (60 to 640px) or double-click the edge to fit the widest value. From the keyboard, ⌥⇧ with an arrow on a header, or Column width in the column menu (Wider, Narrower, Fit to content).
|
|
36
|
+
- Pin any column from its menu to keep it in view when scrolling sideways; pinned columns move to the left edge and show a pin in the header. Pinned columns never take more than 60% of the grid's width.
|
|
37
|
+
- You always see where you are and which record changes: the row being edited is tinted (--color-active-row), and its row number and column header turn primary, as in Google Sheets. The active cell has a 2px primary outline on the surface. Under the pointer, the row takes --color-hover and the cell --color-selected. A select's list opens below the cell, never over it.
|
|
38
|
+
- Select columns read as selectors: the value with a chevron, shown on row hover and focus. A second click on the active cell, a click on the chevron, Enter or any letter opens the list. Don't show select values as badges in an editable grid; badges read as status, not as something to change.
|
|
39
|
+
- The row header is one compact slot: the row number, replaced by a checkbox on hover, on focus and while any row is checked, then the ⋮ menu, shown on hover. On touch screens both stay visible.
|
|
40
|
+
- Computed columns show their progress in the status row and their state in each cell, in words. A run never overwrites a typed value.
|
|
41
|
+
- Pressing Enter on a failed cell runs it again; the status line states the cost first.
|
|
42
|
+
- Keys follow Excel and Google Sheets: Shift with an arrow extends the selection, ⌘ or Ctrl with an arrow jumps to the edge of the data, ⌘C and ⌘V copy and paste, ⌘D and ⌘R fill, ⌘Z undoes. Ctrl replaces ⌘ on Windows and Linux.
|
|
43
|
+
- Copy puts tab-separated values on the clipboard, so a range pastes straight into Excel or Google Sheets, and numbers stay numbers.
|
|
44
|
+
- Paste reads tab-separated values from Excel or Sheets into the range starting at the active cell, and checks each value against its column's type. Cells that don't fit keep their value, are outlined in red with the reason, and the status line says how many were left and why. Nothing is dropped without saying so.
|
|
45
|
+
- Paste, fill, clear and edits can all be undone (⌘Z) and redone (⌘⇧Z or Ctrl+Y), and each one is announced: “Pasted 12 cells”, “Undid paste of 12 cells”.
|
|
46
|
+
- Use size="dense" (28px rows) for spreadsheet-like data. It is a variant for that data, never a row height setting; offer Wrap text per column for long values.
|
|
47
|
+
- Tab moves out of the grid unless a cell is being edited, where it saves and moves right, so the grid stays one tab stop.
|
|
48
|
+
- With onOpenRow, the name cell shows Open on hover and focus and the row menu has Open too; ⇧F10 opens the row menu from the keyboard.
|
|
49
|
+
- Pair the grid with a ShortcutSheet group from sheetGridShortcuts(), which writes ⌘ on a Mac and Ctrl elsewhere, and list the same groups on the page with ShortcutList, so the keys are visible without pressing ? or ⌘/.
|
|
50
|
+
- Add row and Add 10 rows put the new rows at the bottom, scroll there and make the first new cell active, so people can type straight away.
|
|
51
|
+
|
|
52
|
+
## Content
|
|
53
|
+
|
|
54
|
+
- Keep the three empty-looking states apart: a dash when there is nothing, Not found when the service looked and found nothing, and Failed with the reason.
|
|
55
|
+
- Status row words: Up to date, 62%, 3 errors, Out of date. Never a bare spinner.
|
|
56
|
+
- Right-align numbers; show dates as 14 Mar 2026.
|
|
57
|
+
- Status line messages say what happened and to how many cells: Copied 6 cells, Pasted 12 cells. 2 cells left as they were, Filled down 4 cells, Cleared 3 cells.
|
|
58
|
+
- Rejection reasons say what to enter: “Enter a number, such as 1200.”, “Choose one of Lead, Qualified, Proposal.”
|
|
59
|
+
|
|
60
|
+
## Accessibility
|
|
61
|
+
|
|
62
|
+
- APG grid pattern: role grid, roving focus on cells, aria-sort on the sorted header, row numbers as row headers.
|
|
63
|
+
- 2.1.1 Keyboard: every column action is in a menu reached from the header by keyboard.
|
|
64
|
+
- 1.4.1 Use of colour: failed cells carry an icon and the reason, not only red.
|
|
65
|
+
- 4.1.3 Status messages: saves, clears and retries are announced from the status line.
|
|
66
|
+
- 2.4.11 Focus not obscured: cells scroll clear of the sticky header and frozen columns.
|
|
67
|
+
- APG grid: aria-multiselectable on the grid and aria-selected on cells in the range; selections are announced as “6 cells selected: Job title to Seniority, rows 1 to 3.”
|
|
68
|
+
- 4.1.3 Status messages: copy, paste, fill, clear, undo and redo are announced from the status line, including the reason any cell was rejected.
|
|
69
|
+
- 3.3.1 Error identification: a rejected paste cell has aria-invalid, a red dashed outline and its reason in text, not colour alone.
|
|
70
|
+
|
|
71
|
+
## Keyboard
|
|
72
|
+
|
|
73
|
+
| Keys | Action |
|
|
74
|
+
|---|---|
|
|
75
|
+
| ← → ↑ ↓ | Move one cell; up from row 1 to the filter row and the headers |
|
|
76
|
+
| ⌘ / Ctrl + arrow | Jump to the edge of the data in that direction |
|
|
77
|
+
| Home / End | First or last cell in the row |
|
|
78
|
+
| ⌘ Home / ⌘ End (Ctrl on Windows) | First or last cell in the sheet |
|
|
79
|
+
| Page Up / Page Down | Move 10 rows |
|
|
80
|
+
| ⇧ + arrow, ⇧ Home / End | Extend the selection |
|
|
81
|
+
| ⌘ ⇧ + arrow | Extend the selection to the edge of the data |
|
|
82
|
+
| ⇧ click, click and drag | Select a range with the mouse |
|
|
83
|
+
| ⌘ A | Select all cells |
|
|
84
|
+
| Ctrl Space | Select the column |
|
|
85
|
+
| ⇧ Space | Select or deselect the rows of the selected cells (with row selection) |
|
|
86
|
+
| ⌘ C | Copy the range as tab-separated values, for here, Excel or Google Sheets |
|
|
87
|
+
| ⌘ X | Copy the range and clear it |
|
|
88
|
+
| ⌘ V | Paste from here, Excel or Google Sheets into the range from the active cell; each value is checked |
|
|
89
|
+
| Delete / Backspace | Clear the editable cells in the range |
|
|
90
|
+
| ⌘ D | Fill down: the top row of the range into the rows below (one row: from the row above) |
|
|
91
|
+
| ⌘ R | Fill right: the left column of the range into the columns to its right |
|
|
92
|
+
| ⌘ Z | Undo the last edit, paste, clear or fill |
|
|
93
|
+
| ⌘ ⇧ Z / Ctrl Y | Redo |
|
|
94
|
+
| Enter / F2 | Edit; on a header, open the column menu; on a failed cell, run it again; on a group row, collapse or expand |
|
|
95
|
+
| Any character | Start editing and replace the value; on a select cell, open the list |
|
|
96
|
+
| Space (yes or no cell) | Turn it on or off |
|
|
97
|
+
| Enter (editing) | Save and move down |
|
|
98
|
+
| ⇧ Enter (editing) | Save and move up |
|
|
99
|
+
| Tab (editing) | Save and move right |
|
|
100
|
+
| ⇧ Tab (editing) | Save and move left |
|
|
101
|
+
| ⌘ Enter (editing) | Save the value into every selected cell |
|
|
102
|
+
| Esc (editing) | Cancel the edit |
|
|
103
|
+
| Esc | Clear the selection back to the active cell |
|
|
104
|
+
| ⇧ F10 / Menu key, right click | Open the row's actions |
|
|
105
|
+
| Alt ↓ | Open the column menu from a header |
|
|
106
|
+
| ⌥ ⇧ ← / → (Alt Shift on Windows) | On a header: make the column narrower or wider by 16px |
|
|
107
|
+
| ⌘ ; (Ctrl ; on Windows) | Put today's date in a date cell |
|
|
108
|
+
| ⌘ / (Ctrl / on Windows), ? | Show every shortcut |
|
|
109
|
+
| Tab | Leave the grid (one tab stop) |
|
|
110
|
+
|
|
111
|
+
## Examples
|
|
112
|
+
|
|
113
|
+
### dense
|
|
114
|
+
|
|
115
|
+
```tsx
|
|
116
|
+
import { Button, type SheetCellChange, SheetGrid, type SheetRow, ShortcutSheet, sheetGridShortcuts } from "@twoux/ui";
|
|
117
|
+
import { Keyboard } from "lucide-react";
|
|
118
|
+
import { useState } from "react";
|
|
119
|
+
import { PROSPECT_COLUMNS, prospectRows } from "../dense-table/prospects-data";
|
|
120
|
+
|
|
121
|
+
export const meta = {
|
|
122
|
+
title: "Dense, with spreadsheet keys",
|
|
123
|
+
description: "The TwoUX prospects table at size=\"dense\" for 28px rows. Shift+arrows or drag to select, ⌘C to copy as tab-separated values, ⌘V to paste from here, Excel or Google Sheets (each value is checked against its column), ⌘D and ⌘R to fill, Delete to clear, ⌘Z to undo. Press ? for every key.",
|
|
124
|
+
order: 2,
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
export default function Example() {
|
|
128
|
+
const [columns, setColumns] = useState(PROSPECT_COLUMNS);
|
|
129
|
+
const [single, setSingle] = useState(true);
|
|
130
|
+
const [rows, setRows] = useState<SheetRow[]>(() => prospectRows(30));
|
|
131
|
+
const onCellsChange = (changes: SheetCellChange[]) =>
|
|
132
|
+
setRows((rs) => rs.map((r) => {
|
|
133
|
+
const mine = changes.filter((c) => c.rowId === r.id);
|
|
134
|
+
return mine.length ? { ...r, values: { ...r.values, ...Object.fromEntries(mine.map((c) => [c.columnId, c.value])) } } : r;
|
|
135
|
+
}));
|
|
136
|
+
return (
|
|
137
|
+
<div className="grid w-full gap-2">
|
|
138
|
+
<div className="flex items-center gap-2">
|
|
139
|
+
<span className="text-sm text-ink-2">TwoUX prospects · {rows.length} people</span>
|
|
140
|
+
<span className="flex-1" />
|
|
141
|
+
<Button variant="ghost" size="sm" onClick={() => document.dispatchEvent(new KeyboardEvent("keydown", { key: "?" }))}><Keyboard />Keys (?)</Button>
|
|
142
|
+
</div>
|
|
143
|
+
<SheetGrid
|
|
144
|
+
size="dense"
|
|
145
|
+
aria-label="TwoUX prospects"
|
|
146
|
+
className="h-96 w-full"
|
|
147
|
+
columns={columns}
|
|
148
|
+
onColumnsChange={setColumns}
|
|
149
|
+
rows={rows}
|
|
150
|
+
onCellChange={(id, col, v) => setRows((rs) => rs.map((r) => (r.id === id ? { ...r, values: { ...r.values, [col]: v } } : r)))}
|
|
151
|
+
onCellsChange={onCellsChange}
|
|
152
|
+
onAddRows={(n) => setRows((rs) => [...rs, ...Array.from({ length: n }, (_, i) => ({ id: `new-${rs.length + i}`, values: { subscribed: false } }))])}
|
|
153
|
+
/>
|
|
154
|
+
<ShortcutSheet singleKeys={single} onSingleKeysChange={setSingle} groups={sheetGridShortcuts()} />
|
|
155
|
+
</div>
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
### prospects
|
|
161
|
+
|
|
162
|
+
```tsx
|
|
163
|
+
import { SheetGrid, type SheetColumn, type SheetRow, type SheetSort } from "@twoux/ui";
|
|
164
|
+
import { useState } from "react";
|
|
165
|
+
|
|
166
|
+
export const meta = { title: "Prospects with computed columns", description: "Focus a cell and use the arrow keys. Enter edits, Esc cancels, Tab saves and moves on. Press Up from the first row to reach the headers, then Enter for the column menu. Enter on a failed cell runs it again.", order: 1 };
|
|
167
|
+
|
|
168
|
+
const COLUMNS: SheetColumn[] = [
|
|
169
|
+
{ id: "name", label: "Name", type: "text", width: 160, pinned: true, locked: true },
|
|
170
|
+
{ id: "title", label: "Job title", type: "text", width: 200 },
|
|
171
|
+
{ id: "seniority", label: "Seniority", type: "select", width: 120, options: ["Mid", "Senior", "Director", "VP"] },
|
|
172
|
+
{ id: "size", label: "Company size", type: "number", width: 130 },
|
|
173
|
+
{ id: "summary", label: "Summarise role", type: "ai", width: 260, costPerRow: 1, status: { done: 3, total: 5, running: true } },
|
|
174
|
+
{ id: "email", label: "Work email", type: "enrichment", provider: "hubspot", width: 230, costPerRow: 0.5, status: { done: 5, total: 5, errors: 1 } },
|
|
175
|
+
];
|
|
176
|
+
|
|
177
|
+
export default function Example() {
|
|
178
|
+
const [columns, setColumns] = useState(COLUMNS);
|
|
179
|
+
const [sort, setSort] = useState<SheetSort>(null);
|
|
180
|
+
const [rows, setRows] = useState<SheetRow[]>([
|
|
181
|
+
{ id: "1", values: { name: "Ana Silva", title: "Head of Revenue Operations", seniority: "Senior", size: 240 }, computed: { summary: { state: "done", value: "Leads a function; shortlists tools." }, email: { state: "done", value: "ana.silva@harbourvale.co.uk" } } },
|
|
182
|
+
{ id: "2", values: { name: "Ben Hughes", title: "Chief Operating Officer", seniority: "VP", size: 160 }, computed: { summary: { state: "done", value: "Runs the business; owns the budget." }, email: { state: "not-found" } } },
|
|
183
|
+
{ id: "3", values: { name: "Chloe Patel", title: "", seniority: "Mid", size: 410 }, computed: { summary: { state: "failed", reason: "Job title is empty." }, email: { state: "done", value: "chloe.patel@mosaiclending.co.uk" } } },
|
|
184
|
+
{ id: "4", values: { name: "Daniel Murray", title: "IT Director", seniority: "Director", size: 1200 }, computed: { summary: { state: "writing" }, email: { state: "failed", reason: "No company domain." } } },
|
|
185
|
+
{ id: "5", values: { name: "Ella Brennan", title: "Operations Manager", seniority: "Mid", size: 130 }, computed: { summary: { state: "queued" }, email: { state: "done", value: "ella.brennan@orrin.dev" } } },
|
|
186
|
+
]);
|
|
187
|
+
return (
|
|
188
|
+
<SheetGrid
|
|
189
|
+
aria-label="Prospects"
|
|
190
|
+
className="h-96 w-full"
|
|
191
|
+
columns={columns}
|
|
192
|
+
onColumnsChange={setColumns}
|
|
193
|
+
rows={rows}
|
|
194
|
+
onCellChange={(id, col, v) => setRows((rs) => rs.map((r) => (r.id === id ? { ...r, values: { ...r.values, [col]: v } } : r)))}
|
|
195
|
+
sort={sort}
|
|
196
|
+
onSortChange={setSort}
|
|
197
|
+
onRetryCell={(id, col) => setRows((rs) => rs.map((r) => (r.id === id ? { ...r, computed: { ...r.computed, [col]: { state: "queued" } } } : r)))}
|
|
198
|
+
onAddRows={(n) => setRows((rs) => [...rs, ...Array.from({ length: n }, (_, i) => ({ id: `n${rs.length + i}`, values: {} }))])}
|
|
199
|
+
/>
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
## Related
|
|
205
|
+
|
|
206
|
+
- Data grid
|
|
207
|
+
- Columns menu
|
|
208
|
+
- Run status bar
|
|
209
|
+
- Action catalogue
|
|
210
|
+
- AI column
|
|
211
|
+
- Dense table
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
# Side panel (drawer)
|
|
2
|
+
|
|
3
|
+
A panel on the right that shows the selected row's detail beside the table, in push (default) or overlay mode; 440px by default and resizable from 360px to half the canvas in push mode.
|
|
4
|
+
|
|
5
|
+
Kind: component · Group: Feedback and overlays · Status: built, import from @twoux/ui
|
|
6
|
+
|
|
7
|
+
## When to use
|
|
8
|
+
|
|
9
|
+
- Viewing or editing one record while the list stays in view, such as a listing's figures beside the listings table.
|
|
10
|
+
- Working down a list one record at a time: J and K move the selection and the panel follows.
|
|
11
|
+
- Secondary tasks that should not take the person off the page, such as the Assistant's quick questions.
|
|
12
|
+
|
|
13
|
+
## When not to use
|
|
14
|
+
|
|
15
|
+
- The full picture of one record, with history and long sections. Open the full page (⇧↵).
|
|
16
|
+
- A decision that must interrupt the task. Use a Confirmation dialog.
|
|
17
|
+
- Confirming an action. Use a Toast.
|
|
18
|
+
- Comparing many records side by side. Use the table with the chosen columns.
|
|
19
|
+
|
|
20
|
+
## Structure
|
|
21
|
+
|
|
22
|
+
1. Control bar without a title: previous, next, position ("3 of 9"), open full page, close
|
|
23
|
+
2. Record ID, title and status line
|
|
24
|
+
3. Tabs (Overview, Candidates, Activity)
|
|
25
|
+
4. Sections split by full-width rules, each with a 13px heading and one action on the right
|
|
26
|
+
5. Empty fields show "Add"
|
|
27
|
+
6. Pinned footer: destructive action left, primary right
|
|
28
|
+
|
|
29
|
+
## Guidelines
|
|
30
|
+
|
|
31
|
+
- Push mode (default) on list pages: the table narrows so filters, page actions and the first columns stay usable and nothing is hidden behind the panel.
|
|
32
|
+
- Overlay mode when the page is narrower than about 1100px, or for wide content (dashboards, wide tables) that pushing would crush. The first columns stay visible.
|
|
33
|
+
- One component with mode="push" | "overlay"; the person can switch with ⇧P and the choice is remembered.
|
|
34
|
+
- Opens only from a person's action (Enter or a click on a row), never on its own.
|
|
35
|
+
- Esc closes it and focus returns to the row it came from.
|
|
36
|
+
- Do not repeat table header actions inside the panel; the footer holds actions for this record only.
|
|
37
|
+
- Below 860px the panel takes the full width of the canvas.
|
|
38
|
+
- A panel for one thing, such as settings or a form, has its title in the 44px header bar, with the object it belongs to beside it in muted text (Table settings · People) and Close on the right. A record panel that steps through a list keeps previous, next and the position in the bar, and shows the record's name and status under it.
|
|
39
|
+
|
|
40
|
+
## Content
|
|
41
|
+
|
|
42
|
+
- The control bar has no title; the record's own title sits below it, matching the row the person picked.
|
|
43
|
+
- Section headings are short nouns ("Performance", "Funnel", "Details") with one action each.
|
|
44
|
+
- Empty fields say "Add" and the field name rather than a dash, so the gap is an invitation, not an error.
|
|
45
|
+
- Figures sit next to a comparison ("vs board median") so a number can be judged without leaving.
|
|
46
|
+
|
|
47
|
+
## Accessibility
|
|
48
|
+
|
|
49
|
+
- The panel is a non-modal complementary region with a label; it does not trap focus, so J/K keep driving the table (2.1.2).
|
|
50
|
+
- In overlay mode the panel must not cover the focused row; the table scrolls the active row into the visible part (2.4.11).
|
|
51
|
+
- Tabs inside the panel follow the APG tabs pattern with arrow keys between tabs.
|
|
52
|
+
- Push and overlay both reflow to a single column at 320px wide without horizontal scrolling of text (1.4.10).
|
|
53
|
+
- Focus order inside the panel follows reading order: control bar, title, tabs, sections, footer (2.4.3).
|
|
54
|
+
|
|
55
|
+
## Keyboard
|
|
56
|
+
|
|
57
|
+
| Keys | Action |
|
|
58
|
+
|---|---|
|
|
59
|
+
| Enter | Open the panel for the focused row |
|
|
60
|
+
| J / K | Next or previous row; the panel follows |
|
|
61
|
+
| ⇧↵ | Open the record as a full page |
|
|
62
|
+
| ⇧P | Switch between push and overlay |
|
|
63
|
+
| Esc | Close the panel and return focus to the row |
|
|
64
|
+
|
|
65
|
+
## Examples
|
|
66
|
+
|
|
67
|
+
### record
|
|
68
|
+
|
|
69
|
+
```tsx
|
|
70
|
+
import { Badge, Button, PanelMode, SegmentedControl, Segment, SidePanel, Table, TableBody, TableCell, TableRow, Timestamp } from "@twoux/ui";
|
|
71
|
+
import { useState } from "react";
|
|
72
|
+
import { LISTINGS, STATUS_VARIANT } from "../list-table/data";
|
|
73
|
+
|
|
74
|
+
export const meta = { title: "Record beside the list", description: "Push mode narrows the table so the first columns stay usable; overlay floats over it. J and K move through records with the panel following; Esc closes and focus returns to the row.", order: 1 };
|
|
75
|
+
|
|
76
|
+
const ROWS = LISTINGS.slice(0, 6);
|
|
77
|
+
|
|
78
|
+
export default function Example() {
|
|
79
|
+
const [open, setOpen] = useState<number | null>(1);
|
|
80
|
+
const [mode, setMode] = useState<PanelMode>("push");
|
|
81
|
+
const rec = open === null ? null : ROWS[open];
|
|
82
|
+
return (
|
|
83
|
+
<div className="grid w-full gap-2">
|
|
84
|
+
<SegmentedControl value={mode} onValueChange={(v) => setMode(v as PanelMode)} size="sm" aria-label="Panel mode" className="justify-self-start">
|
|
85
|
+
<Segment value="push">Push</Segment><Segment value="overlay">Overlay</Segment>
|
|
86
|
+
</SegmentedControl>
|
|
87
|
+
<div className="flex h-96 overflow-hidden rounded-sm border border-line bg-surface">
|
|
88
|
+
<div className="min-w-0 flex-1 overflow-auto">
|
|
89
|
+
<Table aria-label="Listings">
|
|
90
|
+
<TableBody>
|
|
91
|
+
{ROWS.map((l, i) => (
|
|
92
|
+
<TableRow key={l.id} data-state={open === i ? "selected" : undefined} tabIndex={0} onClick={() => setOpen(i)} onKeyDown={(e) => { if (e.key === "Enter") setOpen(i); }} className="cursor-default">
|
|
93
|
+
<TableCell className="font-medium text-ink">{l.role}</TableCell>
|
|
94
|
+
<TableCell className="text-ink-2">{l.location}</TableCell>
|
|
95
|
+
<TableCell><Badge variant={STATUS_VARIANT[l.status]} dot>{l.status}</Badge></TableCell>
|
|
96
|
+
</TableRow>
|
|
97
|
+
))}
|
|
98
|
+
</TableBody>
|
|
99
|
+
</Table>
|
|
100
|
+
</div>
|
|
101
|
+
{rec && (
|
|
102
|
+
<SidePanel
|
|
103
|
+
open={open !== null}
|
|
104
|
+
onOpenChange={(o) => !o && setOpen(null)}
|
|
105
|
+
mode={mode}
|
|
106
|
+
label="Listing details"
|
|
107
|
+
title={rec.role}
|
|
108
|
+
meta={<span className="flex items-center gap-2"><span className="font-mono">{rec.id}</span><Badge variant={STATUS_VARIANT[rec.status]} dot>{rec.status}</Badge></span>}
|
|
109
|
+
position={{ index: open ?? 0, total: ROWS.length }}
|
|
110
|
+
onPrevious={() => setOpen((o) => Math.max(0, (o ?? 0) - 1))}
|
|
111
|
+
onNext={() => setOpen((o) => Math.min(ROWS.length - 1, (o ?? 0) + 1))}
|
|
112
|
+
onOpenFull={() => {}}
|
|
113
|
+
footer={<Button size="sm">Edit listing</Button>}
|
|
114
|
+
>
|
|
115
|
+
<dl className="grid grid-cols-[120px_1fr] gap-x-3 gap-y-2 text-base">
|
|
116
|
+
<dt className="text-ink-2">Location</dt><dd className="text-ink">{rec.location}</dd>
|
|
117
|
+
<dt className="text-ink-2">Applicants</dt><dd className="tabular text-ink">{rec.applicants}</dd>
|
|
118
|
+
<dt className="text-ink-2">Closes</dt><dd><Timestamp value={`${rec.closes}T17:00:00Z`} format="absolute" /></dd>
|
|
119
|
+
<dt className="text-ink-2">Hiring manager</dt><dd><Button variant="link" size="xs">Add</Button></dd>
|
|
120
|
+
</dl>
|
|
121
|
+
</SidePanel>
|
|
122
|
+
)}
|
|
123
|
+
</div>
|
|
124
|
+
</div>
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
## Related
|
|
130
|
+
|
|
131
|
+
- App shell and sidebar navigation
|
|
132
|
+
- Command palette (⌘K)
|
|
133
|
+
- Confirmation dialog
|
|
134
|
+
- Loading skeleton
|