@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,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generated from shadcn/ui new-york-v4 "empty" by scripts/tailor-shadcn.mjs. Do not edit by hand.
|
|
3
|
+
* TwoUX changes:
|
|
4
|
+
* - Dashed container sized to the region it replaces, compact spacing
|
|
5
|
+
* - 32px neutral icon tile, no illustration; adds the error tile for the error state
|
|
6
|
+
* - The title is a real heading for screen readers (level 2 by default, set level to fit the page)
|
|
7
|
+
* - Description at most 36 characters wide, readable grey
|
|
8
|
+
*/
|
|
9
|
+
import { cva, type VariantProps } from "class-variance-authority"
|
|
10
|
+
import { cn } from "../lib/cn"
|
|
11
|
+
|
|
12
|
+
function Empty({ className, ...props }: React.ComponentProps<"div">) {
|
|
13
|
+
return (
|
|
14
|
+
<div
|
|
15
|
+
data-slot="empty"
|
|
16
|
+
className={cn(
|
|
17
|
+
"flex min-w-0 flex-1 flex-col items-center justify-center gap-4 rounded-sm border border-dashed border-line-strong p-6 text-center text-balance md:p-10",
|
|
18
|
+
className
|
|
19
|
+
)}
|
|
20
|
+
{...props}
|
|
21
|
+
/>
|
|
22
|
+
)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function EmptyHeader({ className, ...props }: React.ComponentProps<"div">) {
|
|
26
|
+
return (
|
|
27
|
+
<div
|
|
28
|
+
data-slot="empty-header"
|
|
29
|
+
className={cn(
|
|
30
|
+
"flex max-w-sm flex-col items-center gap-1 text-center",
|
|
31
|
+
className
|
|
32
|
+
)}
|
|
33
|
+
{...props}
|
|
34
|
+
/>
|
|
35
|
+
)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const emptyMediaVariants = cva(
|
|
39
|
+
"mb-2 flex shrink-0 items-center justify-center [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
|
40
|
+
{
|
|
41
|
+
variants: {
|
|
42
|
+
variant: {
|
|
43
|
+
default: "bg-transparent",
|
|
44
|
+
icon: "flex size-8 shrink-0 items-center justify-center rounded-sm bg-muted text-ink-2 [&_svg:not([class*='size-'])]:size-4",
|
|
45
|
+
error: "flex size-8 shrink-0 items-center justify-center rounded-sm bg-bad-bg text-bad [&_svg:not([class*='size-'])]:size-4",
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
defaultVariants: {
|
|
49
|
+
variant: "default",
|
|
50
|
+
},
|
|
51
|
+
}
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
function EmptyMedia({
|
|
55
|
+
className,
|
|
56
|
+
variant = "default",
|
|
57
|
+
...props
|
|
58
|
+
}: React.ComponentProps<"div"> & VariantProps<typeof emptyMediaVariants>) {
|
|
59
|
+
return (
|
|
60
|
+
<div
|
|
61
|
+
data-slot="empty-icon"
|
|
62
|
+
data-variant={variant}
|
|
63
|
+
className={cn(emptyMediaVariants({ variant, className }))}
|
|
64
|
+
{...props}
|
|
65
|
+
/>
|
|
66
|
+
)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function EmptyTitle({ className, level = 2, ...props }: React.ComponentProps<"div"> & { level?: 1 | 2 | 3 | 4 | 5 | 6 }) {
|
|
70
|
+
return (
|
|
71
|
+
<div
|
|
72
|
+
role="heading"
|
|
73
|
+
aria-level={level}
|
|
74
|
+
data-slot="empty-title"
|
|
75
|
+
className={cn("text-md font-semibold", className)}
|
|
76
|
+
{...props}
|
|
77
|
+
/>
|
|
78
|
+
)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function EmptyDescription({ className, ...props }: React.ComponentProps<"p">) {
|
|
82
|
+
return (
|
|
83
|
+
<div
|
|
84
|
+
data-slot="empty-description"
|
|
85
|
+
className={cn(
|
|
86
|
+
"max-w-[36ch] text-base text-ink-2 [&>a]:underline [&>a]:underline-offset-4 [&>a:hover]:text-primary",
|
|
87
|
+
className
|
|
88
|
+
)}
|
|
89
|
+
{...props}
|
|
90
|
+
/>
|
|
91
|
+
)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function EmptyContent({ className, ...props }: React.ComponentProps<"div">) {
|
|
95
|
+
return (
|
|
96
|
+
<div
|
|
97
|
+
data-slot="empty-content"
|
|
98
|
+
className={cn(
|
|
99
|
+
"flex w-full max-w-sm min-w-0 flex-col items-center gap-3 text-base text-balance",
|
|
100
|
+
className
|
|
101
|
+
)}
|
|
102
|
+
{...props}
|
|
103
|
+
/>
|
|
104
|
+
)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export {
|
|
108
|
+
Empty,
|
|
109
|
+
EmptyHeader,
|
|
110
|
+
EmptyTitle,
|
|
111
|
+
EmptyDescription,
|
|
112
|
+
EmptyContent,
|
|
113
|
+
EmptyMedia,
|
|
114
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TwoUX composition: “Was this helpful?” with Yes / No, one vote, an optional follow-up, and every state
|
|
3
|
+
* (sending, sent, failed with Try again). One per page or panel.
|
|
4
|
+
*/
|
|
5
|
+
import { Check, ThumbsDown, ThumbsUp } from "lucide-react"
|
|
6
|
+
import * as React from "react"
|
|
7
|
+
import { Button } from "./button"
|
|
8
|
+
import { Textarea } from "./textarea"
|
|
9
|
+
|
|
10
|
+
type Vote = "yes" | "no"
|
|
11
|
+
|
|
12
|
+
export function FeedbackPrompt({
|
|
13
|
+
question = "Was this helpful?",
|
|
14
|
+
description,
|
|
15
|
+
onVote,
|
|
16
|
+
onFollowUp,
|
|
17
|
+
privacy = "Don't include personal information about candidates.",
|
|
18
|
+
}: {
|
|
19
|
+
question?: string
|
|
20
|
+
description?: string
|
|
21
|
+
/** Sends the vote. Reject to show the failed state. */
|
|
22
|
+
onVote: (vote: Vote) => Promise<void>
|
|
23
|
+
/** Optional: sends what they typed after voting. */
|
|
24
|
+
onFollowUp?: (text: string, vote: Vote) => Promise<void>
|
|
25
|
+
privacy?: string
|
|
26
|
+
}) {
|
|
27
|
+
const [vote, setVote] = React.useState<Vote | null>(null)
|
|
28
|
+
const [state, setState] = React.useState<"idle" | "sending" | "sent" | "failed">("idle")
|
|
29
|
+
const [text, setText] = React.useState("")
|
|
30
|
+
const [followState, setFollowState] = React.useState<"idle" | "sending" | "sent" | "failed">("idle")
|
|
31
|
+
const id = React.useId()
|
|
32
|
+
|
|
33
|
+
const send = async (v: Vote) => {
|
|
34
|
+
setVote(v)
|
|
35
|
+
setState("sending")
|
|
36
|
+
try { await onVote(v); setState("sent") } catch { setState("failed") }
|
|
37
|
+
}
|
|
38
|
+
const sendFollow = async () => {
|
|
39
|
+
if (!vote || !onFollowUp) return
|
|
40
|
+
setFollowState("sending")
|
|
41
|
+
try { await onFollowUp(text, vote); setFollowState("sent") } catch { setFollowState("failed") }
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return (
|
|
45
|
+
<section aria-labelledby={`${id}-q`} data-slot="feedback-prompt" className="grid gap-2">
|
|
46
|
+
<h3 id={`${id}-q`} className="text-base font-medium text-ink">{question}</h3>
|
|
47
|
+
{description && <p className="text-sm text-ink-2">{description}</p>}
|
|
48
|
+
{state === "sent" && vote ? (
|
|
49
|
+
<p role="status" className="flex items-center gap-control text-base text-ink-2">
|
|
50
|
+
<Check className="size-3.5 text-ok" aria-hidden="true" />
|
|
51
|
+
You answered {vote === "yes" ? "Yes" : "No"}. Thanks for telling us.
|
|
52
|
+
</p>
|
|
53
|
+
) : (
|
|
54
|
+
<div className="flex items-center gap-2">
|
|
55
|
+
<Button variant="outline" size="sm" loading={state === "sending" && vote === "yes"} disabled={state === "sending"} onClick={() => send("yes")}><ThumbsUp />Yes</Button>
|
|
56
|
+
<Button variant="outline" size="sm" loading={state === "sending" && vote === "no"} disabled={state === "sending"} onClick={() => send("no")}><ThumbsDown />No</Button>
|
|
57
|
+
</div>
|
|
58
|
+
)}
|
|
59
|
+
{state === "failed" && (
|
|
60
|
+
<p role="alert" className="flex items-center gap-2 text-sm text-bad">
|
|
61
|
+
Your answer wasn't sent.
|
|
62
|
+
<Button variant="ghost" size="xs" onClick={() => vote && send(vote)}>Try again</Button>
|
|
63
|
+
</p>
|
|
64
|
+
)}
|
|
65
|
+
{state === "sent" && onFollowUp && followState !== "sent" && (
|
|
66
|
+
<div className="grid gap-2">
|
|
67
|
+
<label htmlFor={`${id}-f`} className="text-sm font-medium text-ink">{vote === "no" ? "What was missing or wrong? (optional)" : "Anything else? (optional)"}</label>
|
|
68
|
+
<Textarea id={`${id}-f`} value={text} maxLength={500} onChange={(e) => setText(e.target.value)} aria-describedby={`${id}-p ${id}-c`} />
|
|
69
|
+
<div className="flex items-center justify-between gap-2 text-sm text-ink-3">
|
|
70
|
+
<span id={`${id}-p`}>{privacy}</span>
|
|
71
|
+
<span id={`${id}-c`} className="tabular">{text.length} / 500</span>
|
|
72
|
+
</div>
|
|
73
|
+
{followState === "failed" && <p role="alert" className="text-sm text-bad">Your comment wasn't sent. It's still here; try again.</p>}
|
|
74
|
+
<Button size="sm" variant="outline" className="justify-self-start" disabled={!text.trim()} loading={followState === "sending"} onClick={sendFollow}>Send</Button>
|
|
75
|
+
</div>
|
|
76
|
+
)}
|
|
77
|
+
{followState === "sent" && <p role="status" className="text-sm text-ink-2">Comment sent.</p>}
|
|
78
|
+
</section>
|
|
79
|
+
)
|
|
80
|
+
}
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generated from shadcn/ui new-york-v4 "field" by scripts/tailor-shadcn.mjs. Do not edit by hand.
|
|
3
|
+
* TwoUX changes:
|
|
4
|
+
* - TwoUX spacing between fields is tighter (compact density)
|
|
5
|
+
* - An invalid field keeps its label and value in normal text; only the edge and the message turn red (less alarming, still clear)
|
|
6
|
+
*/
|
|
7
|
+
"use client"
|
|
8
|
+
|
|
9
|
+
import { useMemo } from "react"
|
|
10
|
+
import { cva, type VariantProps } from "class-variance-authority"
|
|
11
|
+
import { cn } from "../lib/cn"
|
|
12
|
+
|
|
13
|
+
import { Label } from "./label"
|
|
14
|
+
import { Separator } from "./separator"
|
|
15
|
+
|
|
16
|
+
function FieldSet({ className, ...props }: React.ComponentProps<"fieldset">) {
|
|
17
|
+
return (
|
|
18
|
+
<fieldset
|
|
19
|
+
data-slot="field-set"
|
|
20
|
+
className={cn(
|
|
21
|
+
"flex flex-col gap-6",
|
|
22
|
+
"has-[>[data-slot=checkbox-group]]:gap-3 has-[>[data-slot=radio-group]]:gap-3",
|
|
23
|
+
className
|
|
24
|
+
)}
|
|
25
|
+
{...props}
|
|
26
|
+
/>
|
|
27
|
+
)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function FieldLegend({
|
|
31
|
+
className,
|
|
32
|
+
variant = "legend",
|
|
33
|
+
...props
|
|
34
|
+
}: React.ComponentProps<"legend"> & { variant?: "legend" | "label" }) {
|
|
35
|
+
return (
|
|
36
|
+
<legend
|
|
37
|
+
data-slot="field-legend"
|
|
38
|
+
data-variant={variant}
|
|
39
|
+
className={cn(
|
|
40
|
+
"mb-3 font-medium",
|
|
41
|
+
"data-[variant=legend]:text-md",
|
|
42
|
+
"data-[variant=label]:text-base",
|
|
43
|
+
className
|
|
44
|
+
)}
|
|
45
|
+
{...props}
|
|
46
|
+
/>
|
|
47
|
+
)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function FieldGroup({ className, ...props }: React.ComponentProps<"div">) {
|
|
51
|
+
return (
|
|
52
|
+
<div
|
|
53
|
+
data-slot="field-group"
|
|
54
|
+
className={cn(
|
|
55
|
+
"group/field-group @container/field-group flex w-full flex-col gap-5 data-[slot=checkbox-group]:gap-3 [&>[data-slot=field-group]]:gap-4",
|
|
56
|
+
className
|
|
57
|
+
)}
|
|
58
|
+
{...props}
|
|
59
|
+
/>
|
|
60
|
+
)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const fieldVariants = cva(
|
|
64
|
+
"group/field flex w-full gap-3",
|
|
65
|
+
{
|
|
66
|
+
variants: {
|
|
67
|
+
orientation: {
|
|
68
|
+
vertical: ["flex-col [&>*]:w-full [&>.sr-only]:w-auto"],
|
|
69
|
+
horizontal: [
|
|
70
|
+
"flex-row items-center",
|
|
71
|
+
"[&>[data-slot=field-label]]:flex-auto",
|
|
72
|
+
"has-[>[data-slot=field-content]]:items-start has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px",
|
|
73
|
+
],
|
|
74
|
+
responsive: [
|
|
75
|
+
"flex-col @md/field-group:flex-row @md/field-group:items-center [&>*]:w-full @md/field-group:[&>*]:w-auto [&>.sr-only]:w-auto",
|
|
76
|
+
"@md/field-group:[&>[data-slot=field-label]]:flex-auto",
|
|
77
|
+
"@md/field-group:has-[>[data-slot=field-content]]:items-start @md/field-group:has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px",
|
|
78
|
+
],
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
defaultVariants: {
|
|
82
|
+
orientation: "vertical",
|
|
83
|
+
},
|
|
84
|
+
}
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
function Field({
|
|
88
|
+
className,
|
|
89
|
+
orientation = "vertical",
|
|
90
|
+
...props
|
|
91
|
+
}: React.ComponentProps<"div"> & VariantProps<typeof fieldVariants>) {
|
|
92
|
+
return (
|
|
93
|
+
<div
|
|
94
|
+
role="group"
|
|
95
|
+
data-slot="field"
|
|
96
|
+
data-orientation={orientation}
|
|
97
|
+
className={cn(fieldVariants({ orientation }), className)}
|
|
98
|
+
{...props}
|
|
99
|
+
/>
|
|
100
|
+
)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function FieldContent({ className, ...props }: React.ComponentProps<"div">) {
|
|
104
|
+
return (
|
|
105
|
+
<div
|
|
106
|
+
data-slot="field-content"
|
|
107
|
+
className={cn(
|
|
108
|
+
"group/field-content flex flex-1 flex-col gap-1.5 leading-snug",
|
|
109
|
+
className
|
|
110
|
+
)}
|
|
111
|
+
{...props}
|
|
112
|
+
/>
|
|
113
|
+
)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function FieldLabel({
|
|
117
|
+
className,
|
|
118
|
+
...props
|
|
119
|
+
}: React.ComponentProps<typeof Label>) {
|
|
120
|
+
return (
|
|
121
|
+
<Label
|
|
122
|
+
data-slot="field-label"
|
|
123
|
+
className={cn(
|
|
124
|
+
"group/field-label peer/field-label flex w-fit gap-2 leading-snug group-data-[disabled=true]/field:opacity-50",
|
|
125
|
+
"has-[>[data-slot=field]]:w-full has-[>[data-slot=field]]:flex-col has-[>[data-slot=field]]:rounded-sm has-[>[data-slot=field]]:border [&>*]:data-[slot=field]:p-4",
|
|
126
|
+
"has-data-[state=checked]:border-primary has-data-[state=checked]:bg-primary/5",
|
|
127
|
+
className
|
|
128
|
+
)}
|
|
129
|
+
{...props}
|
|
130
|
+
/>
|
|
131
|
+
)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function FieldTitle({ className, ...props }: React.ComponentProps<"div">) {
|
|
135
|
+
return (
|
|
136
|
+
<div
|
|
137
|
+
data-slot="field-label"
|
|
138
|
+
className={cn(
|
|
139
|
+
"flex w-fit items-center gap-2 text-base leading-snug font-medium group-data-[disabled=true]/field:opacity-50",
|
|
140
|
+
className
|
|
141
|
+
)}
|
|
142
|
+
{...props}
|
|
143
|
+
/>
|
|
144
|
+
)
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function FieldDescription({ className, ...props }: React.ComponentProps<"p">) {
|
|
148
|
+
return (
|
|
149
|
+
<p
|
|
150
|
+
data-slot="field-description"
|
|
151
|
+
className={cn(
|
|
152
|
+
"text-base leading-normal font-normal text-muted-foreground group-has-[[data-orientation=horizontal]]/field:text-balance",
|
|
153
|
+
"last:mt-0 nth-last-2:-mt-1 [[data-variant=legend]+&]:-mt-1.5",
|
|
154
|
+
"[&>a]:underline [&>a]:underline-offset-4 [&>a:hover]:text-primary",
|
|
155
|
+
className
|
|
156
|
+
)}
|
|
157
|
+
{...props}
|
|
158
|
+
/>
|
|
159
|
+
)
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function FieldSeparator({
|
|
163
|
+
children,
|
|
164
|
+
className,
|
|
165
|
+
...props
|
|
166
|
+
}: React.ComponentProps<"div"> & {
|
|
167
|
+
children?: React.ReactNode
|
|
168
|
+
}) {
|
|
169
|
+
return (
|
|
170
|
+
<div
|
|
171
|
+
data-slot="field-separator"
|
|
172
|
+
data-content={!!children}
|
|
173
|
+
className={cn(
|
|
174
|
+
"relative -my-2 h-5 text-base group-data-[variant=outline]/field-group:-mb-2",
|
|
175
|
+
className
|
|
176
|
+
)}
|
|
177
|
+
{...props}
|
|
178
|
+
>
|
|
179
|
+
<Separator className="absolute inset-0 top-1/2" />
|
|
180
|
+
{children && (
|
|
181
|
+
<span
|
|
182
|
+
className="relative mx-auto block w-fit bg-background px-2 text-muted-foreground"
|
|
183
|
+
data-slot="field-separator-content"
|
|
184
|
+
>
|
|
185
|
+
{children}
|
|
186
|
+
</span>
|
|
187
|
+
)}
|
|
188
|
+
</div>
|
|
189
|
+
)
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function FieldError({
|
|
193
|
+
className,
|
|
194
|
+
children,
|
|
195
|
+
errors,
|
|
196
|
+
...props
|
|
197
|
+
}: React.ComponentProps<"div"> & {
|
|
198
|
+
errors?: Array<{ message?: string } | undefined>
|
|
199
|
+
}) {
|
|
200
|
+
const content = useMemo(() => {
|
|
201
|
+
if (children) {
|
|
202
|
+
return children
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (!errors?.length) {
|
|
206
|
+
return null
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const uniqueErrors = [
|
|
210
|
+
...new Map(errors.map((error) => [error?.message, error])).values(),
|
|
211
|
+
]
|
|
212
|
+
|
|
213
|
+
if (uniqueErrors?.length == 1) {
|
|
214
|
+
return uniqueErrors[0]?.message
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
return (
|
|
218
|
+
<ul className="ml-4 flex list-disc flex-col gap-1">
|
|
219
|
+
{uniqueErrors.map(
|
|
220
|
+
(error, index) =>
|
|
221
|
+
error?.message && <li key={index}>{error.message}</li>
|
|
222
|
+
)}
|
|
223
|
+
</ul>
|
|
224
|
+
)
|
|
225
|
+
}, [children, errors])
|
|
226
|
+
|
|
227
|
+
if (!content) {
|
|
228
|
+
return null
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return (
|
|
232
|
+
<div
|
|
233
|
+
role="alert"
|
|
234
|
+
data-slot="field-error"
|
|
235
|
+
className={cn("text-base font-normal text-destructive", className)}
|
|
236
|
+
{...props}
|
|
237
|
+
>
|
|
238
|
+
{content}
|
|
239
|
+
</div>
|
|
240
|
+
)
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
export {
|
|
244
|
+
Field,
|
|
245
|
+
FieldLabel,
|
|
246
|
+
FieldDescription,
|
|
247
|
+
FieldError,
|
|
248
|
+
FieldGroup,
|
|
249
|
+
FieldLegend,
|
|
250
|
+
FieldSeparator,
|
|
251
|
+
FieldSet,
|
|
252
|
+
FieldContent,
|
|
253
|
+
FieldTitle,
|
|
254
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TwoUX compositions on shadcn Field, Label, Input, Checkbox, RadioGroup and Switch.
|
|
3
|
+
* They wire label, hint and error to the control (htmlFor, aria-describedby, aria-invalid) so every field is accessible by default.
|
|
4
|
+
*/
|
|
5
|
+
import { AlertCircle, CheckCircle2 } from "lucide-react"
|
|
6
|
+
import * as React from "react"
|
|
7
|
+
import { cn } from "../lib/cn"
|
|
8
|
+
import { Checkbox } from "./checkbox"
|
|
9
|
+
import { Field, FieldDescription, FieldLabel } from "./field"
|
|
10
|
+
import { Input } from "./input"
|
|
11
|
+
import { InputGroup, InputGroupAddon, InputGroupInput, InputGroupText } from "./input-group"
|
|
12
|
+
import { RadioGroupItem } from "./radio-group"
|
|
13
|
+
import { Switch } from "./switch"
|
|
14
|
+
|
|
15
|
+
type TextFieldProps = Omit<React.ComponentProps<"input">, "prefix"> & {
|
|
16
|
+
label: string
|
|
17
|
+
hint?: string
|
|
18
|
+
error?: string
|
|
19
|
+
/** Confirms the value meets the rules, e.g. a password. Shown in place of the hint. */
|
|
20
|
+
success?: string
|
|
21
|
+
optional?: boolean
|
|
22
|
+
/** Inside the field, before the value: £, a search icon. */
|
|
23
|
+
prefix?: React.ReactNode
|
|
24
|
+
/** Inside the field, after the value: a unit or a shortcut. */
|
|
25
|
+
suffix?: React.ReactNode
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function TextField({ label, hint, error, success, optional, required, prefix, suffix, id, className, ...props }: TextFieldProps) {
|
|
29
|
+
const auto = React.useId()
|
|
30
|
+
const fid = id ?? auto
|
|
31
|
+
const describedBy = error ? `${fid}-error` : success ? `${fid}-ok` : hint ? `${fid}-hint` : undefined
|
|
32
|
+
const control = { id: fid, required, "aria-invalid": error ? true : undefined, "aria-describedby": describedBy, ...props }
|
|
33
|
+
return (
|
|
34
|
+
<Field data-invalid={!!error || undefined} className={cn("gap-2", className)}>
|
|
35
|
+
<FieldLabel htmlFor={fid}>
|
|
36
|
+
{label}
|
|
37
|
+
{required && <span aria-hidden="true" className="text-bad">*</span>}
|
|
38
|
+
{optional && <span className="font-normal text-ink-3">(optional)</span>}
|
|
39
|
+
</FieldLabel>
|
|
40
|
+
{prefix || suffix ? (
|
|
41
|
+
<InputGroup>
|
|
42
|
+
{prefix && <InputGroupAddon><InputGroupText>{prefix}</InputGroupText></InputGroupAddon>}
|
|
43
|
+
<InputGroupInput {...control} />
|
|
44
|
+
{suffix && <InputGroupAddon align="inline-end"><InputGroupText>{suffix}</InputGroupText></InputGroupAddon>}
|
|
45
|
+
</InputGroup>
|
|
46
|
+
) : (
|
|
47
|
+
<Input {...control} />
|
|
48
|
+
)}
|
|
49
|
+
{error ? (
|
|
50
|
+
<p id={`${fid}-error`} className="flex items-start gap-control text-sm text-bad">
|
|
51
|
+
<AlertCircle className="mt-0.5 size-3.5 shrink-0" aria-hidden="true" />
|
|
52
|
+
{error}
|
|
53
|
+
</p>
|
|
54
|
+
) : success ? (
|
|
55
|
+
<p id={`${fid}-ok`} className="flex items-start gap-control text-sm text-ok">
|
|
56
|
+
<CheckCircle2 className="mt-0.5 size-3.5 shrink-0" aria-hidden="true" />
|
|
57
|
+
{success}
|
|
58
|
+
</p>
|
|
59
|
+
) : hint ? (
|
|
60
|
+
<FieldDescription id={`${fid}-hint`}>{hint}</FieldDescription>
|
|
61
|
+
) : null}
|
|
62
|
+
</Field>
|
|
63
|
+
)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function Choice({ id, label, hint, control }: { id: string; label: string; hint?: string; control: React.ReactNode }) {
|
|
67
|
+
return (
|
|
68
|
+
<div className="grid grid-cols-[16px_1fr] gap-x-2">
|
|
69
|
+
<span className="flex h-6 items-center">{control}</span>
|
|
70
|
+
<label htmlFor={id} className="flex min-h-6 cursor-pointer items-center text-base text-ink">{label}</label>
|
|
71
|
+
{hint && <span id={`${id}-hint`} className="col-start-2 text-sm text-ink-3">{hint}</span>}
|
|
72
|
+
</div>
|
|
73
|
+
)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function CheckboxField({ id, label, hint, ...props }: React.ComponentProps<typeof Checkbox> & { id: string; label: string; hint?: string }) {
|
|
77
|
+
return <Choice id={id} label={label} hint={hint} control={<Checkbox id={id} aria-describedby={hint ? `${id}-hint` : undefined} {...props} />} />
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function RadioField({ id, value, label, hint, disabled }: { id: string; value: string; label: string; hint?: string; disabled?: boolean }) {
|
|
81
|
+
return <Choice id={id} label={label} hint={hint} control={<RadioGroupItem id={id} value={value} disabled={disabled} aria-describedby={hint ? `${id}-hint` : undefined} />} />
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function SwitchField({ id, label, hint, ...props }: React.ComponentProps<typeof Switch> & { id: string; label: string; hint?: string }) {
|
|
85
|
+
return (
|
|
86
|
+
<div className="flex items-start justify-between gap-6">
|
|
87
|
+
<div className="grid gap-0.5">
|
|
88
|
+
<label htmlFor={id} className="cursor-pointer text-base text-ink">{label}</label>
|
|
89
|
+
{hint && <span id={`${id}-hint`} className="text-sm text-ink-3">{hint}</span>}
|
|
90
|
+
</div>
|
|
91
|
+
<span className="flex h-5 items-center"><Switch id={id} aria-describedby={hint ? `${id}-hint` : undefined} {...props} /></span>
|
|
92
|
+
</div>
|
|
93
|
+
)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Shown above a form after a failed submit. Each link moves focus to its field. */
|
|
97
|
+
export function ErrorSummary({ errors, className }: { errors: { field: string; message: string; href: string }[]; className?: string }) {
|
|
98
|
+
if (!errors.length) return null
|
|
99
|
+
return (
|
|
100
|
+
<div role="alert" tabIndex={-1} className={cn("grid gap-2 rounded-md border border-bad-line bg-bad-bg/60 px-3 py-2", className)}>
|
|
101
|
+
<p className="flex items-center gap-control text-base font-medium text-bad">
|
|
102
|
+
<AlertCircle className="size-3.5" aria-hidden="true" />
|
|
103
|
+
{errors.length === 1 ? "1 field needs fixing" : `${errors.length} fields need fixing`}
|
|
104
|
+
</p>
|
|
105
|
+
<ul className="grid gap-0.5 pl-5.5 text-sm">
|
|
106
|
+
{errors.map((e) => (
|
|
107
|
+
<li key={e.href}><a href={e.href} className="text-ink underline decoration-bad-line underline-offset-3 transition-colors duration-(--duration-fast) hover:decoration-bad"><span className="font-medium">{e.field}</span>: {e.message}</a></li>
|
|
108
|
+
))}
|
|
109
|
+
</ul>
|
|
110
|
+
</div>
|
|
111
|
+
)
|
|
112
|
+
}
|