@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,123 @@
|
|
|
1
|
+
# Error state (failed to load)
|
|
2
|
+
|
|
3
|
+
Shown in place of a region that failed to load: what failed, that nothing was lost, and Try again.
|
|
4
|
+
|
|
5
|
+
Kind: component · Group: Feedback and overlays · Status: built, import from @twoux/ui
|
|
6
|
+
|
|
7
|
+
## When to use
|
|
8
|
+
|
|
9
|
+
- A table, list, chart or panel could not fetch its data.
|
|
10
|
+
- Only one region fails while the rest of the page works; the error stays inside that region.
|
|
11
|
+
- The person lacks permission to see a region's data.
|
|
12
|
+
|
|
13
|
+
## When not to use
|
|
14
|
+
|
|
15
|
+
- An action failed (save, extend, close). Show a Banner near the action and keep the person's input.
|
|
16
|
+
- Field validation. Use the inline field error and the form error strip (Banner).
|
|
17
|
+
- Nothing matched the filters. That is an Empty state, not an error.
|
|
18
|
+
|
|
19
|
+
## Structure
|
|
20
|
+
|
|
21
|
+
1. Region header kept ("Listings")
|
|
22
|
+
2. Icon tile in the error colour
|
|
23
|
+
3. Title: what failed ("Couldn't load listings")
|
|
24
|
+
4. Description: why if known, and what is kept
|
|
25
|
+
5. Try again button (secondary) with its shortcut R
|
|
26
|
+
6. Optional: expandable technical details
|
|
27
|
+
|
|
28
|
+
## Guidelines
|
|
29
|
+
|
|
30
|
+
- Replace only the failed region, at the same size, so the header, filters and other regions stay usable.
|
|
31
|
+
- Keep the person's filters, sort and selection; say so in the message.
|
|
32
|
+
- Offer one action, Try again, which re-runs the same request.
|
|
33
|
+
- Retrying swaps back to the Loading skeleton, then to data or the error again.
|
|
34
|
+
- Put raw error codes in an expandable details section, never in the main message.
|
|
35
|
+
|
|
36
|
+
## Content
|
|
37
|
+
|
|
38
|
+
- Title says what failed in plain words: "Couldn't load listings".
|
|
39
|
+
- Description says why if known and what is safe: "The server didn't respond. Your filters are kept."
|
|
40
|
+
- Take ownership; never imply the person caused it.
|
|
41
|
+
- No apologies, jokes or exclamation marks. Do not promise that a team has been told unless that is true.
|
|
42
|
+
|
|
43
|
+
## Accessibility
|
|
44
|
+
|
|
45
|
+
- The error is identified in text, not only by the red icon (3.3.1, 1.4.1).
|
|
46
|
+
- Announce the failure once through a live region; do not move focus unless the person triggered the load (4.1.3).
|
|
47
|
+
- After Try again succeeds, keep focus on a stable element such as the region header, not on the removed button (2.4.3).
|
|
48
|
+
|
|
49
|
+
## Keyboard
|
|
50
|
+
|
|
51
|
+
| Keys | Action |
|
|
52
|
+
|---|---|
|
|
53
|
+
| R | Try again (single-key shortcut, can be turned off) |
|
|
54
|
+
| Tab / Enter | Reach and activate Try again |
|
|
55
|
+
|
|
56
|
+
## Examples
|
|
57
|
+
|
|
58
|
+
### table
|
|
59
|
+
|
|
60
|
+
```tsx
|
|
61
|
+
import { Button, Empty, EmptyContent, EmptyDescription, EmptyHeader, EmptyMedia, EmptyTitle, Skeleton } from "@twoux/ui";
|
|
62
|
+
import { CircleAlert } from "lucide-react";
|
|
63
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
64
|
+
|
|
65
|
+
export const meta = { title: "Table failed to load", description: "Only the table is replaced; the header stays. Try again (or R) swaps back to the skeleton, then to the data or the error. Technical details stay folded away.", order: 1 };
|
|
66
|
+
|
|
67
|
+
export default function Example() {
|
|
68
|
+
const [loading, setLoading] = useState(false);
|
|
69
|
+
const ref = useRef<HTMLDivElement>(null);
|
|
70
|
+
const retry = useCallback(() => {
|
|
71
|
+
setLoading(true);
|
|
72
|
+
setTimeout(() => setLoading(false), 1200);
|
|
73
|
+
}, []);
|
|
74
|
+
|
|
75
|
+
// R retries while focus is inside this region and not in a text field
|
|
76
|
+
useEffect(() => {
|
|
77
|
+
const el = ref.current;
|
|
78
|
+
const onKey = (e: KeyboardEvent) => {
|
|
79
|
+
if (e.key.toLowerCase() === "r" && !e.metaKey && !e.ctrlKey && !loading && !(e.target as HTMLElement).closest("input, textarea")) retry();
|
|
80
|
+
};
|
|
81
|
+
el?.addEventListener("keydown", onKey);
|
|
82
|
+
return () => el?.removeEventListener("keydown", onKey);
|
|
83
|
+
}, [loading, retry]);
|
|
84
|
+
|
|
85
|
+
return (
|
|
86
|
+
<div ref={ref} className="grid w-full max-w-xl gap-2">
|
|
87
|
+
<span className="text-base font-medium text-ink">Listings</span>
|
|
88
|
+
{loading ? (
|
|
89
|
+
<div className="grid min-h-56 content-start gap-0 overflow-hidden rounded-sm border border-line" aria-busy="true" aria-label="Loading listings">
|
|
90
|
+
{["w-40", "w-52", "w-36", "w-48", "w-44", "w-32"].map((w) => (
|
|
91
|
+
<div key={w} className="flex h-9 items-center gap-3 border-b border-line px-3 last:border-b-0">
|
|
92
|
+
<Skeleton className={`h-2.5 ${w}`} />
|
|
93
|
+
<Skeleton className="ml-auto h-2.5 w-6" />
|
|
94
|
+
</div>
|
|
95
|
+
))}
|
|
96
|
+
</div>
|
|
97
|
+
) : (
|
|
98
|
+
<Empty className="min-h-56 border-solid border-line">
|
|
99
|
+
<EmptyHeader>
|
|
100
|
+
<EmptyMedia variant="error"><CircleAlert /></EmptyMedia>
|
|
101
|
+
<EmptyTitle>Couldn't load listings</EmptyTitle>
|
|
102
|
+
<EmptyDescription>The server took too long to reply. Your filters are kept.</EmptyDescription>
|
|
103
|
+
</EmptyHeader>
|
|
104
|
+
<EmptyContent>
|
|
105
|
+
<Button variant="outline" size="sm" shortcut="R" onClick={retry}>Try again</Button>
|
|
106
|
+
<details className="text-sm text-ink-3">
|
|
107
|
+
<summary className="cursor-pointer rounded-xs hover:text-ink-2">Technical details</summary>
|
|
108
|
+
<code className="mt-1 block font-mono">504 Gateway Timeout · GET /listings · req 7f3a91</code>
|
|
109
|
+
</details>
|
|
110
|
+
</EmptyContent>
|
|
111
|
+
</Empty>
|
|
112
|
+
)}
|
|
113
|
+
</div>
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
## Related
|
|
119
|
+
|
|
120
|
+
- Loading skeleton
|
|
121
|
+
- Empty state
|
|
122
|
+
- Banner (inline alert)
|
|
123
|
+
- Toast
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# Export menu
|
|
2
|
+
|
|
3
|
+
One Export menu per table: options first (only visible columns, include AI columns), then file formats, then copy to clipboard.
|
|
4
|
+
|
|
5
|
+
Kind: component · Group: Data · Status: built, import from @twoux/ui
|
|
6
|
+
|
|
7
|
+
## When to use
|
|
8
|
+
|
|
9
|
+
- People take table data to another tool. Export is a table-level toolbar action.
|
|
10
|
+
- Several related export actions start one action each; a menu suits 5 to 15 such items.
|
|
11
|
+
|
|
12
|
+
## When not to use
|
|
13
|
+
|
|
14
|
+
- For a single format. Use a plain button, "Export CSV", as on chart cards.
|
|
15
|
+
- To choose a value that changes the view. Use a select.
|
|
16
|
+
|
|
17
|
+
## Structure
|
|
18
|
+
|
|
19
|
+
1. Export button with a download icon
|
|
20
|
+
2. Menu heading with the shortcut (⇧E)
|
|
21
|
+
3. Options: Only visible columns, Include AI columns (checkable, the menu stays open)
|
|
22
|
+
4. Download CSV, Download JSON
|
|
23
|
+
5. Copy as CSV, Copy as JSON
|
|
24
|
+
|
|
25
|
+
## Guidelines
|
|
26
|
+
|
|
27
|
+
- Group items with labelled sections: options, download, copy.
|
|
28
|
+
- Options are checkbox items that toggle without closing the menu.
|
|
29
|
+
- Order by frequency: the most used format first.
|
|
30
|
+
- Confirm a copy or a finished download with a short status message.
|
|
31
|
+
|
|
32
|
+
## Content
|
|
33
|
+
|
|
34
|
+
- Use verb plus format, "Download CSV", "Copy as JSON".
|
|
35
|
+
- Checkbox labels state the choice so its opposite is clear, "Only visible columns".
|
|
36
|
+
- Write labels in sentence case on a single line.
|
|
37
|
+
|
|
38
|
+
## Accessibility
|
|
39
|
+
|
|
40
|
+
- APG menu button pattern: Enter, Space or Down Arrow opens the menu and focuses the first item.
|
|
41
|
+
- APG menu pattern: options use role="menuitemcheckbox" with aria-checked; Escape closes and returns focus to Export.
|
|
42
|
+
- 4.1.3 Status Messages: "Copied 24 rows as CSV" is announced without moving focus.
|
|
43
|
+
|
|
44
|
+
## Keyboard
|
|
45
|
+
|
|
46
|
+
| Keys | Action |
|
|
47
|
+
|---|---|
|
|
48
|
+
| ⇧E | Open the Export menu |
|
|
49
|
+
| ↑ ↓ | Move between items |
|
|
50
|
+
| Space | Toggle an option without closing |
|
|
51
|
+
| Enter | Run the export and close |
|
|
52
|
+
| Esc | Close and return focus to Export |
|
|
53
|
+
|
|
54
|
+
## Examples
|
|
55
|
+
|
|
56
|
+
### menu
|
|
57
|
+
|
|
58
|
+
```tsx
|
|
59
|
+
import { ExportMenu, toast } from "@twoux/ui";
|
|
60
|
+
import { useState } from "react";
|
|
61
|
+
|
|
62
|
+
export const meta = { title: "Export a table", description: "Options toggle without closing the menu. Downloads and copies run and close, then confirm with a toast. Opens with ⇧E.", order: 1 };
|
|
63
|
+
|
|
64
|
+
export default function Example() {
|
|
65
|
+
const [onlyVisible, setOnlyVisible] = useState(true);
|
|
66
|
+
const [includeAi, setIncludeAi] = useState(false);
|
|
67
|
+
const scope = `${onlyVisible ? "visible" : "all"} columns${includeAi ? ", with AI columns" : ""}`;
|
|
68
|
+
return (
|
|
69
|
+
<ExportMenu
|
|
70
|
+
onlyVisible={onlyVisible}
|
|
71
|
+
onOnlyVisibleChange={setOnlyVisible}
|
|
72
|
+
includeAi={includeAi}
|
|
73
|
+
onIncludeAiChange={setIncludeAi}
|
|
74
|
+
onDownload={(f) => toast.success(`listings.${f.toLowerCase()} downloaded`, { description: `24 rows, ${scope}` })}
|
|
75
|
+
onCopy={(f) => toast.success(`Copied 24 rows as ${f}`)}
|
|
76
|
+
/>
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Related
|
|
82
|
+
|
|
83
|
+
- Data grid
|
|
84
|
+
- List table
|
|
85
|
+
- Breakdown table
|
|
86
|
+
- Line chart card
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# Feedback prompt
|
|
2
|
+
|
|
3
|
+
A small "Was this helpful?" block with Yes and No buttons, a thank you once voted, and an optional short follow-up form.
|
|
4
|
+
|
|
5
|
+
Kind: component · Group: Feedback and overlays · Status: built, import from @twoux/ui
|
|
6
|
+
|
|
7
|
+
## When to use
|
|
8
|
+
|
|
9
|
+
- At the end of help topics, help centre articles and beta feature pages.
|
|
10
|
+
- At the end of a report people have just read, to learn if it was useful.
|
|
11
|
+
- On a beta feature's page, to learn whether it is worth keeping.
|
|
12
|
+
|
|
13
|
+
## When not to use
|
|
14
|
+
|
|
15
|
+
- Under assistant answers; they use the good and bad controls by the origin label.
|
|
16
|
+
- As a modal or toast, or before people have read the content.
|
|
17
|
+
- For surveys of more than three questions; link to a survey page instead.
|
|
18
|
+
|
|
19
|
+
## Structure
|
|
20
|
+
|
|
21
|
+
1. Question as a small heading: "Was this helpful?"
|
|
22
|
+
2. Optional one-line description
|
|
23
|
+
3. Yes and No buttons (text, with an optional thumb icon)
|
|
24
|
+
4. States: default, sending (button spinner), sent (chosen answer selected and thank you text), failed (message and Try again)
|
|
25
|
+
5. Optional follow-up revealed after the vote: one to three fields, a privacy line and Send
|
|
26
|
+
|
|
27
|
+
## Guidelines
|
|
28
|
+
|
|
29
|
+
- Send the vote on click; send the follow-up separately with its own Send button.
|
|
30
|
+
- Once voted, the buttons are replaced by the chosen answer and the thank you; the vote can't be sent twice.
|
|
31
|
+
- One feedback prompt per page or panel.
|
|
32
|
+
- Free-text fields carry the privacy line and a 500 character limit with a count.
|
|
33
|
+
- On failure, show the message inline with Try again; keep any typed text.
|
|
34
|
+
|
|
35
|
+
## Content
|
|
36
|
+
|
|
37
|
+
- Question names the thing: "Was this help topic helpful?" or simply "Was this helpful?" when the context is clear.
|
|
38
|
+
- Sent: "Thanks, your feedback is recorded."
|
|
39
|
+
- Follow-up label: "What was missing? (optional)". Privacy line: "Don't include candidate names or other personal details."
|
|
40
|
+
|
|
41
|
+
## Accessibility
|
|
42
|
+
|
|
43
|
+
- Yes and No are real buttons with visible text; an icon alone is not enough.
|
|
44
|
+
- After a successful vote, focus moves to the thank you text.
|
|
45
|
+
- The failed state is announced in a live region.
|
|
46
|
+
- The follow-up fields have visible labels and follow the form validation rules.
|
|
47
|
+
|
|
48
|
+
## Keyboard
|
|
49
|
+
|
|
50
|
+
| Keys | Action |
|
|
51
|
+
|---|---|
|
|
52
|
+
| Tab | Move to Yes, then No |
|
|
53
|
+
| Enter / Space | Send the vote |
|
|
54
|
+
| Tab (after voting) | Reach the optional follow-up fields and Send |
|
|
55
|
+
|
|
56
|
+
## Examples
|
|
57
|
+
|
|
58
|
+
### helpful
|
|
59
|
+
|
|
60
|
+
```tsx
|
|
61
|
+
import { CheckboxField, FeedbackPrompt } from "@twoux/ui";
|
|
62
|
+
import { useState } from "react";
|
|
63
|
+
|
|
64
|
+
export const meta = { title: "Was this helpful?", description: "One vote, sent on click and never twice. After voting, an optional comment with the privacy line and a 500 character limit. Failures keep what was typed and offer Try again.", order: 1 };
|
|
65
|
+
|
|
66
|
+
export default function Example() {
|
|
67
|
+
const [fail, setFail] = useState(false);
|
|
68
|
+
return (
|
|
69
|
+
<div className="grid w-full max-w-md gap-4">
|
|
70
|
+
<CheckboxField id="fb-fail" label="Simulate a failed send" checked={fail} onCheckedChange={(v) => setFail(v === true)} />
|
|
71
|
+
<div className="rounded-sm border border-line bg-surface p-4">
|
|
72
|
+
<FeedbackPrompt
|
|
73
|
+
description="About the screening questions guide"
|
|
74
|
+
onVote={() => new Promise((res, rej) => setTimeout(() => (fail ? rej(new Error("offline")) : res()), 600))}
|
|
75
|
+
onFollowUp={() => new Promise((res) => setTimeout(res, 600))}
|
|
76
|
+
/>
|
|
77
|
+
</div>
|
|
78
|
+
</div>
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## Related
|
|
84
|
+
|
|
85
|
+
- Collecting product feedback
|
|
86
|
+
- Help panel
|
|
87
|
+
- Button
|
|
88
|
+
- Text input
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# Field mapping
|
|
2
|
+
|
|
3
|
+
A list of target fields, each with the source column that fills it: required fields marked, type mismatches warned in words, and other source columns skipped or created as new columns. Used by imports and, later, automations.
|
|
4
|
+
|
|
5
|
+
Kind: component · Group: Data · Status: built, import from @twoux/ui
|
|
6
|
+
|
|
7
|
+
## When to use
|
|
8
|
+
|
|
9
|
+
- When data from one place fills fields in another: a CSV import, a connected service, an automation step.
|
|
10
|
+
|
|
11
|
+
## When not to use
|
|
12
|
+
|
|
13
|
+
- For choosing which columns show. Use the Columns menu.
|
|
14
|
+
|
|
15
|
+
## Structure
|
|
16
|
+
|
|
17
|
+
1. Headings: Field in the table, Column in the file
|
|
18
|
+
2. Row: type icon, field name, Required, arrow, source select with a sample value
|
|
19
|
+
3. Warning under a row: 3 of 20 values aren't an email address
|
|
20
|
+
4. Other columns in the file: Don't import or Create column
|
|
21
|
+
|
|
22
|
+
## Guidelines
|
|
23
|
+
|
|
24
|
+
- Map by name automatically, ignoring case and punctuation, and let people change every pair.
|
|
25
|
+
- Mark required fields in words, and check them when people submit, not by disabling the button.
|
|
26
|
+
- Warn about values that don't fit the field type under that row, with how many.
|
|
27
|
+
- Offer Create column for source columns no field uses, defaulting to Don't import.
|
|
28
|
+
|
|
29
|
+
## Content
|
|
30
|
+
|
|
31
|
+
- Warnings name the column and the type: "3 of 20 values in E-mail aren't an email address."
|
|
32
|
+
|
|
33
|
+
## Accessibility
|
|
34
|
+
|
|
35
|
+
- Each select is labelled by its field name; warnings are linked with aria-describedby.
|
|
36
|
+
- Warnings carry an icon and words, not colour alone.
|
|
37
|
+
|
|
38
|
+
## Keyboard
|
|
39
|
+
|
|
40
|
+
| Keys | Action |
|
|
41
|
+
|---|---|
|
|
42
|
+
| Tab | Move between selects |
|
|
43
|
+
| Space / Enter | Open a select |
|
|
44
|
+
|
|
45
|
+
## Examples
|
|
46
|
+
|
|
47
|
+
### automation
|
|
48
|
+
|
|
49
|
+
```tsx
|
|
50
|
+
import { type ExtraColumns, type FieldMap, FieldMapping, type MappingSource, type MappingTarget, autoMapFields } from "@twoux/ui";
|
|
51
|
+
import { useState } from "react";
|
|
52
|
+
|
|
53
|
+
export const meta = { title: "Map a form's answers to table fields", description: "Fields with matching names are paired automatically. Required fields are marked, values that don't fit the field type are warned about, and unused columns can become new columns.", order: 1 };
|
|
54
|
+
|
|
55
|
+
const TARGETS: MappingTarget[] = [
|
|
56
|
+
{ id: "name", label: "Name", kind: "text", required: true },
|
|
57
|
+
{ id: "email", label: "Work email", kind: "email", required: true },
|
|
58
|
+
{ id: "size", label: "Company size", kind: "number" },
|
|
59
|
+
{ id: "start", label: "Start date", kind: "date" },
|
|
60
|
+
];
|
|
61
|
+
const SOURCES: MappingSource[] = [
|
|
62
|
+
{ id: "q1", label: "Your name", samples: ["Ana Silva", "Ben Hughes", "Chloe Patel"] },
|
|
63
|
+
{ id: "q2", label: "Email", samples: ["ana@harbourvale.co.uk", "ben at brightwater", "chloe@mosaiclending.co.uk"] },
|
|
64
|
+
{ id: "q3", label: "Company size", samples: ["240", "about 150", "410"] },
|
|
65
|
+
{ id: "q4", label: "How did you hear about us?", samples: ["Event", "Search"] },
|
|
66
|
+
];
|
|
67
|
+
|
|
68
|
+
export default function Example() {
|
|
69
|
+
const [map, setMap] = useState<FieldMap>(() => ({ ...autoMapFields(TARGETS, SOURCES), name: "q1" }));
|
|
70
|
+
const [extra, setExtra] = useState<ExtraColumns>({});
|
|
71
|
+
return <FieldMapping className="w-full max-w-2xl" targets={TARGETS} sources={SOURCES} value={map} onChange={setMap} extra={extra} onExtraChange={setExtra} sourceHeading="Answer in the form" />;
|
|
72
|
+
}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Related
|
|
76
|
+
|
|
77
|
+
- CSV import
|
|
78
|
+
- Sheet grid
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# File drop
|
|
2
|
+
|
|
3
|
+
Drop a file or choose one. Accepted types and the size limit show before choosing, and a preview replaces the icon once a file is picked.
|
|
4
|
+
|
|
5
|
+
Kind: component · Group: Actions and inputs · Status: built, import from @twoux/ui
|
|
6
|
+
|
|
7
|
+
## When to use
|
|
8
|
+
|
|
9
|
+
- A single file the product needs, such as a logo, an avatar or a CSV.
|
|
10
|
+
- Where dragging a file in is a shortcut, not the only way.
|
|
11
|
+
|
|
12
|
+
## When not to use
|
|
13
|
+
|
|
14
|
+
- Attaching files to a message. Use the Message composer (message-composer).
|
|
15
|
+
- Importing many files with progress. Use a list with a status per file.
|
|
16
|
+
|
|
17
|
+
## Structure
|
|
18
|
+
|
|
19
|
+
1. Label
|
|
20
|
+
2. Drop area (icon or preview, "Drop a file here", accepted types and limit)
|
|
21
|
+
3. Remove and Replace or Upload buttons
|
|
22
|
+
4. Hidden native file input
|
|
23
|
+
5. Error under the area
|
|
24
|
+
|
|
25
|
+
## Guidelines
|
|
26
|
+
|
|
27
|
+
- Show accepted types and the size limit before anyone chooses a file.
|
|
28
|
+
- Let people choose with a button as well as drop. Drag and drop is never the only way.
|
|
29
|
+
- Errors name the file's size or type and say what to choose, for example "That file is 14.2 MB. Choose one up to 10 MB."
|
|
30
|
+
- The preview replaces the icon, with Remove and Replace beside it.
|
|
31
|
+
- Use a visible label, and a native file input that is hidden visually but still labelled and focusable by the Upload button.
|
|
32
|
+
|
|
33
|
+
## Content
|
|
34
|
+
|
|
35
|
+
- Hint: "PNG or JPEG, up to 10 MB".
|
|
36
|
+
- Wrong type: "That file type isn't supported. Choose PNG or JPEG."
|
|
37
|
+
|
|
38
|
+
## Accessibility
|
|
39
|
+
|
|
40
|
+
- The visible label is attached to the real input, which keeps its accessible name and value (SC 3.3.2, 4.1.2).
|
|
41
|
+
- The error is linked to the field with aria-invalid and announced when it appears (SC 3.3.1, 4.1.3).
|
|
42
|
+
- The drag state is not conveyed by colour alone: the text changes to "Drop to upload" (SC 1.4.1).
|
|
43
|
+
|
|
44
|
+
## Keyboard
|
|
45
|
+
|
|
46
|
+
| Keys | Action |
|
|
47
|
+
|---|---|
|
|
48
|
+
| Tab | Move to the Upload button |
|
|
49
|
+
| Enter or Space | Open the system file chooser |
|
|
50
|
+
| Tab, then Enter | Remove or replace the chosen file |
|
|
51
|
+
|
|
52
|
+
## Examples
|
|
53
|
+
|
|
54
|
+
### logo
|
|
55
|
+
|
|
56
|
+
```tsx
|
|
57
|
+
import { FileDrop } from "@twoux/ui";
|
|
58
|
+
import { useEffect, useState } from "react";
|
|
59
|
+
|
|
60
|
+
export const meta = { title: "Upload a logo", description: "Types and limit show first. Drop a PNG or JPEG, or press Upload. Try a file over 10 MB or a PDF to see the error.", order: 1 };
|
|
61
|
+
|
|
62
|
+
export default function Example() {
|
|
63
|
+
const [url, setUrl] = useState<string>();
|
|
64
|
+
useEffect(() => () => { if (url) URL.revokeObjectURL(url); }, [url]);
|
|
65
|
+
return (
|
|
66
|
+
<div className="w-full max-w-md">
|
|
67
|
+
<FileDrop
|
|
68
|
+
id="company-logo"
|
|
69
|
+
label="Company logo"
|
|
70
|
+
accept="image/png,image/jpeg"
|
|
71
|
+
hint="PNG or JPEG, up to 10 MB"
|
|
72
|
+
maxBytes={10e6}
|
|
73
|
+
preview={url}
|
|
74
|
+
onFile={(f) => setUrl(URL.createObjectURL(f))}
|
|
75
|
+
onRemove={() => setUrl(undefined)}
|
|
76
|
+
/>
|
|
77
|
+
</div>
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### server-error
|
|
83
|
+
|
|
84
|
+
```tsx
|
|
85
|
+
import { FileDrop } from "@twoux/ui";
|
|
86
|
+
|
|
87
|
+
export const meta = { title: "An error from the server", description: "The message names what is wrong with the file and what to choose instead.", order: 2 };
|
|
88
|
+
|
|
89
|
+
export default function Example() {
|
|
90
|
+
return (
|
|
91
|
+
<div className="w-full max-w-md">
|
|
92
|
+
<FileDrop
|
|
93
|
+
id="hiring-csv"
|
|
94
|
+
label="Candidates file"
|
|
95
|
+
accept="text/csv"
|
|
96
|
+
hint="CSV, up to 5 MB"
|
|
97
|
+
error="Row 14 has no email address. Add one and choose the file again."
|
|
98
|
+
onFile={() => {}}
|
|
99
|
+
/>
|
|
100
|
+
</div>
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## Related
|
|
106
|
+
|
|
107
|
+
- Form validation
|
|
108
|
+
- Text input
|
|
109
|
+
- Message composer
|
|
110
|
+
- Provider logo
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
# Filter pills and filter bar
|
|
2
|
+
|
|
3
|
+
Filters applied from one button in the toolbar and shown as segmented chips on their own row: each reads as a sentence, values apply as soon as they are ticked, and the row appears only while something is filtered.
|
|
4
|
+
|
|
5
|
+
Kind: component · Group: Data · Status: built, import from @twoux/ui
|
|
6
|
+
|
|
7
|
+
## When to use
|
|
8
|
+
|
|
9
|
+
- People filter a table by a few properties and need to see every active filter without opening anything.
|
|
10
|
+
- People combine several properties, values and operators on a large collection.
|
|
11
|
+
- Results load quickly enough for each change to apply live.
|
|
12
|
+
|
|
13
|
+
## When not to use
|
|
14
|
+
|
|
15
|
+
- When there are many fields to filter by, as in candidate search. Use the TwoUX Filter panel.
|
|
16
|
+
- For collections under about five items. Do not filter at all.
|
|
17
|
+
- For settings that always have a value, such as a chart's date range or Daily and Weekly. Use a rectangle control or the TwoUX segmented control.
|
|
18
|
+
|
|
19
|
+
## Structure
|
|
20
|
+
|
|
21
|
+
1. Filter button in the toolbar (F): fields, then the chosen field’s values beside them
|
|
22
|
+
2. Filter row, shown only once a filter is applied
|
|
23
|
+
3. Applied filter chip: field, operator, value, remove
|
|
24
|
+
4. Add another filter (+)
|
|
25
|
+
5. Result count
|
|
26
|
+
6. Clear
|
|
27
|
+
|
|
28
|
+
## Guidelines
|
|
29
|
+
|
|
30
|
+
- Shape says what a control does. A filter is a segmented chip (field, operator, value, remove) on the filter row; actions and view settings are buttons in the toolbar.
|
|
31
|
+
- Each chip reads field, operator, value, for example "Status is any of Live, Expiring". Three or more values are counted: "3 statuses".
|
|
32
|
+
- Put the filter button in the toolbar. Picking a field opens its values beside the field list, and the chip appears on the filter row only once a value is ticked, never as an empty suggestion.
|
|
33
|
+
- List values apply as soon as they are ticked and show how many rows each matches. Typed values (numbers, dates) apply on Enter.
|
|
34
|
+
- Put the result count and Clear at the end of the filter row, and filter the full collection, not only the loaded rows.
|
|
35
|
+
- Each chip can be removed on its own with the remove button at its end; + after the chips adds another field.
|
|
36
|
+
|
|
37
|
+
## Content
|
|
38
|
+
|
|
39
|
+
- Use "is", "is not" and "is any of" as operators, matching the field, operator, value token model.
|
|
40
|
+
- Dates offer quick picks first, then "in the last N days", then a calendar.
|
|
41
|
+
- Write the result count as a full phrase, "24 listings", not a bare number.
|
|
42
|
+
- Show the field name before the value so values like "2" or "London" are never ambiguous.
|
|
43
|
+
|
|
44
|
+
## Accessibility
|
|
45
|
+
|
|
46
|
+
- 4.1.3 Status Messages: announce the updated result count without moving focus.
|
|
47
|
+
- 4.1.2 Name, Role, Value: each remove button is named "Remove Status filter"; ticked values expose their checked state.
|
|
48
|
+
- 1.4.11 Non-text Contrast: the chip border must reach 3:1 against the filter row, since it is the chip’s only boundary.
|
|
49
|
+
- 2.1.4 Character Key Shortcuts: F can be turned off with the other single-key shortcuts.
|
|
50
|
+
- Moving focus or scroll back to the top after each change is avoided so people keep their place.
|
|
51
|
+
|
|
52
|
+
## Keyboard
|
|
53
|
+
|
|
54
|
+
| Keys | Action |
|
|
55
|
+
|---|---|
|
|
56
|
+
| F | Open the field list |
|
|
57
|
+
| ↑ ↓, Enter | Move through and choose fields or values |
|
|
58
|
+
| Space | Tick or untick a list value (applies at once) |
|
|
59
|
+
| Enter | Apply a typed number or date |
|
|
60
|
+
| Esc | Close the values, then the field list, and return focus to the filter button |
|
|
61
|
+
|
|
62
|
+
## Examples
|
|
63
|
+
|
|
64
|
+
### listings
|
|
65
|
+
|
|
66
|
+
```tsx
|
|
67
|
+
import { Badge, FilterButton, type FilterFieldDef, FilterRow, type FilterValue, Table, TableBody, TableCell, TableRow, Timestamp, dateFilterFromValues, resolveDateRange } from "@twoux/ui";
|
|
68
|
+
import { useState } from "react";
|
|
69
|
+
import { LISTINGS, type Listing, STATUS_VARIANT } from "../list-table/data";
|
|
70
|
+
|
|
71
|
+
export const meta = {
|
|
72
|
+
title: "Filter listings",
|
|
73
|
+
description: "The filter button sits in the toolbar. Pick a field, then tick its values beside the list: each tick applies at once, and the filter shows as a chip on its own row only once it has a value. A date field (Closing date) opens typed dates and a calendar instead. + adds another field; Clear removes them all.",
|
|
74
|
+
order: 1,
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const FIELDS = [
|
|
78
|
+
{ id: "status", label: "Status", noun: { one: "status", many: "statuses" } },
|
|
79
|
+
{ id: "location", label: "Location", noun: { one: "location", many: "locations" } },
|
|
80
|
+
{ id: "role", label: "Role", noun: { one: "role", many: "roles" } },
|
|
81
|
+
] as const;
|
|
82
|
+
|
|
83
|
+
export default function Example() {
|
|
84
|
+
const [filters, setFilters] = useState<FilterValue>({ status: ["Open", "Closing soon"] });
|
|
85
|
+
const matches = (l: Listing, skip?: string) =>
|
|
86
|
+
Object.entries(filters).every(([f, vals]) => {
|
|
87
|
+
if (f === skip) return true;
|
|
88
|
+
if (f !== "closes") return vals.includes(String(l[f as keyof Listing]));
|
|
89
|
+
// A date field holds dateFilterToValues; resolveDateRange turns it into the dates to compare.
|
|
90
|
+
const { from, to } = resolveDateRange(dateFilterFromValues(vals, []));
|
|
91
|
+
return (!from || l.closes >= from) && (!to || l.closes <= to);
|
|
92
|
+
});
|
|
93
|
+
const rows = LISTINGS.filter((l) => matches(l));
|
|
94
|
+
// Counts reflect the other filters, so people see what a tick will give them.
|
|
95
|
+
const fields: FilterFieldDef[] = [
|
|
96
|
+
...FIELDS.map((f) => ({
|
|
97
|
+
id: f.id,
|
|
98
|
+
label: f.label,
|
|
99
|
+
valueNoun: f.noun,
|
|
100
|
+
options: [...new Set(LISTINGS.map((l) => String(l[f.id])))].sort().map((v) => ({ value: v, label: v, count: LISTINGS.filter((l) => matches(l, f.id) && String(l[f.id]) === v).length })),
|
|
101
|
+
})),
|
|
102
|
+
// Closing dates are in the future, so "last 30 days" presets mean nothing here: typed dates and the calendar only.
|
|
103
|
+
{ id: "closes", label: "Closing date", kind: "date", presets: [], group: "All fields" },
|
|
104
|
+
];
|
|
105
|
+
|
|
106
|
+
return (
|
|
107
|
+
<div className="grid w-full max-w-2xl overflow-hidden rounded-sm border border-line bg-surface">
|
|
108
|
+
<div className="flex items-center gap-2 border-b border-line px-3 py-2">
|
|
109
|
+
<span className="text-base font-medium text-ink">Listings</span>
|
|
110
|
+
<span className="flex-1" />
|
|
111
|
+
<FilterButton fields={fields} value={filters} onChange={setFilters} />
|
|
112
|
+
</div>
|
|
113
|
+
<FilterRow fields={fields} value={filters} onChange={setFilters} shown={rows.length} total={LISTINGS.length} noun="listings" className="border-b border-line bg-surface-2 px-3 py-2" />
|
|
114
|
+
<div className="max-h-56 overflow-y-auto">
|
|
115
|
+
<Table aria-label="Filtered listings">
|
|
116
|
+
<TableBody>
|
|
117
|
+
{rows.map((l) => (
|
|
118
|
+
<TableRow key={l.id}>
|
|
119
|
+
<TableCell className="font-medium text-ink">{l.role}</TableCell>
|
|
120
|
+
<TableCell className="text-ink-2">{l.location}</TableCell>
|
|
121
|
+
<TableCell className="text-ink-2"><Timestamp value={l.closes} format="date" short /></TableCell>
|
|
122
|
+
<TableCell><Badge variant={STATUS_VARIANT[l.status]} dot>{l.status}</Badge></TableCell>
|
|
123
|
+
</TableRow>
|
|
124
|
+
))}
|
|
125
|
+
</TableBody>
|
|
126
|
+
</Table>
|
|
127
|
+
</div>
|
|
128
|
+
</div>
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
## Related
|
|
134
|
+
|
|
135
|
+
- Filter panel
|
|
136
|
+
- Saved views
|
|
137
|
+
- List table
|
|
138
|
+
- Data grid
|