@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,397 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TwoUX composition after an open-source board layout (Apache 2.0, see NOTICE.md): a grid of
|
|
3
|
+
* self-contained items people can reorder and resize. Built on TwoUX's Card, Button and Dropdown menu.
|
|
4
|
+
* Every drag has a keyboard and a menu alternative (WCAG 2.1.1, 2.5.7), and every change is announced.
|
|
5
|
+
*/
|
|
6
|
+
import { Ellipsis, GripVertical, MoveHorizontal, Plus, Search } from "lucide-react"
|
|
7
|
+
import * as React from "react"
|
|
8
|
+
import { createPortal } from "react-dom"
|
|
9
|
+
import { cn } from "../lib/cn"
|
|
10
|
+
import { Button } from "./button"
|
|
11
|
+
import { Input } from "./input"
|
|
12
|
+
import { Tooltip, TooltipContent, TooltipTrigger } from "./tooltip"
|
|
13
|
+
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger } from "./dropdown-menu"
|
|
14
|
+
|
|
15
|
+
export type BoardItemData = { id: string; title: string; columnSpan?: number }
|
|
16
|
+
const PALETTE_MIME = "application/x-twoux-board-item"
|
|
17
|
+
const COLUMNS = 4
|
|
18
|
+
const SPAN: Record<number, string> = { 1: "", 2: "@2xl:col-span-2", 3: "@2xl:col-span-2 @5xl:col-span-3", 4: "@2xl:col-span-2 @5xl:col-span-4" }
|
|
19
|
+
const reducedMotion = () => typeof window !== "undefined" && window.matchMedia?.("(prefers-reduced-motion: reduce)").matches
|
|
20
|
+
|
|
21
|
+
type Drag = { id: string; from: number; over: number; x: number; y: number; w: number; h: number; dx: number; dy: number }
|
|
22
|
+
type BoardCtx = {
|
|
23
|
+
items: BoardItemData[]
|
|
24
|
+
move: (id: string, to: number) => void
|
|
25
|
+
resize: (id: string, span: number) => void
|
|
26
|
+
remove: (id: string) => void
|
|
27
|
+
announce: (text: string) => void
|
|
28
|
+
startDrag: (id: string, e: React.PointerEvent, card: HTMLElement) => void
|
|
29
|
+
draggingId: string | null
|
|
30
|
+
settledId: string | null
|
|
31
|
+
}
|
|
32
|
+
const Ctx = React.createContext<BoardCtx | null>(null)
|
|
33
|
+
const useBoard = () => {
|
|
34
|
+
const c = React.useContext(Ctx)
|
|
35
|
+
if (!c) throw new Error("BoardItem must be inside Board")
|
|
36
|
+
return c
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function Board({
|
|
40
|
+
items,
|
|
41
|
+
onItemsChange,
|
|
42
|
+
renderItem,
|
|
43
|
+
empty,
|
|
44
|
+
onAddFromPalette,
|
|
45
|
+
className,
|
|
46
|
+
"aria-label": label = "Dashboard",
|
|
47
|
+
}: {
|
|
48
|
+
items: BoardItemData[]
|
|
49
|
+
onItemsChange: (items: BoardItemData[]) => void
|
|
50
|
+
renderItem: (item: BoardItemData) => React.ReactNode
|
|
51
|
+
/** Shown when there are no items, e.g. an Empty state with "Add items". */
|
|
52
|
+
empty: React.ReactNode
|
|
53
|
+
/** An item dragged in from the ItemsPalette was dropped at this position. */
|
|
54
|
+
onAddFromPalette?: (paletteId: string, index: number) => void
|
|
55
|
+
className?: string
|
|
56
|
+
"aria-label"?: string
|
|
57
|
+
}) {
|
|
58
|
+
const [message, setMessage] = React.useState("")
|
|
59
|
+
const [drag, setDragState] = React.useState<Drag | null>(null)
|
|
60
|
+
const dragRef = React.useRef<Drag | null>(null)
|
|
61
|
+
const setDrag = (d: Drag | null) => { dragRef.current = d; setDragState(d) }
|
|
62
|
+
const [paletteOver, setPaletteOver] = React.useState<number | null>(null)
|
|
63
|
+
const [settledId, setSettledId] = React.useState<string | null>(null)
|
|
64
|
+
const listRef = React.useRef<HTMLUListElement>(null)
|
|
65
|
+
const announce = React.useCallback((t: string) => setMessage((m) => (m === t ? `${t} ` : t)), [])
|
|
66
|
+
|
|
67
|
+
const settle = (id: string) => { setSettledId(id); setTimeout(() => setSettledId((s) => (s === id ? null : s)), 900) }
|
|
68
|
+
const move = (id: string, to: number) => {
|
|
69
|
+
const from = items.findIndex((i) => i.id === id)
|
|
70
|
+
const clamped = Math.max(0, Math.min(items.length - 1, to))
|
|
71
|
+
if (from < 0 || from === clamped) return
|
|
72
|
+
const next = [...items]
|
|
73
|
+
const [it] = next.splice(from, 1)
|
|
74
|
+
next.splice(clamped, 0, it)
|
|
75
|
+
onItemsChange(next)
|
|
76
|
+
announce(`${it.title} moved to position ${clamped + 1} of ${items.length}.`)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Which slot is under the pointer, against the layout measured when the drag started (so the moving slot can't chase itself).
|
|
80
|
+
const snapshot = () => Array.from(listRef.current?.querySelectorAll<HTMLElement>("[data-cell-id]") ?? []).map((c) => ({ id: c.dataset.cellId!, r: c.getBoundingClientRect() }))
|
|
81
|
+
const slotFrom = (cells: { id: string; r: DOMRect }[], x: number, y: number) => {
|
|
82
|
+
let best = { i: cells.length, d: Number.POSITIVE_INFINITY }
|
|
83
|
+
cells.forEach(({ r }, i) => {
|
|
84
|
+
if (x >= r.left && x <= r.right && y >= r.top && y <= r.bottom) { best = { i, d: -1 }; return }
|
|
85
|
+
if (best.d < 0) return
|
|
86
|
+
const d = Math.hypot(x - (r.left + r.width / 2), y - (r.top + r.height / 2))
|
|
87
|
+
if (d < best.d) best = { i: i === cells.length - 1 && (y > r.bottom || x > r.right) ? cells.length : i, d }
|
|
88
|
+
})
|
|
89
|
+
return best.i
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const startDrag = (id: string, e: React.PointerEvent, card: HTMLElement) => {
|
|
93
|
+
if (e.button !== 0) return
|
|
94
|
+
const r = card.getBoundingClientRect()
|
|
95
|
+
const from = items.findIndex((i) => i.id === id)
|
|
96
|
+
const sx = e.clientX, sy = e.clientY
|
|
97
|
+
const cells = snapshot()
|
|
98
|
+
let started = false
|
|
99
|
+
const onMove = (ev: PointerEvent) => {
|
|
100
|
+
if (!started && Math.hypot(ev.clientX - sx, ev.clientY - sy) < 4) return // a click on the handle is not a drag
|
|
101
|
+
if (!started) { started = true; announce(`Dragging ${items[from].title}.`) }
|
|
102
|
+
setDrag({ id, from, over: Math.min(slotFrom(cells, ev.clientX, ev.clientY), items.length - 1), x: ev.clientX, y: ev.clientY, w: r.width, h: r.height, dx: sx - r.left, dy: sy - r.top })
|
|
103
|
+
}
|
|
104
|
+
const finish = (commit: boolean) => {
|
|
105
|
+
window.removeEventListener("pointermove", onMove)
|
|
106
|
+
window.removeEventListener("pointerup", onUp)
|
|
107
|
+
window.removeEventListener("keydown", onKey)
|
|
108
|
+
const d = dragRef.current
|
|
109
|
+
setDrag(null)
|
|
110
|
+
if (d && commit && d.over !== d.from) { move(id, d.over); settle(id) }
|
|
111
|
+
else if (d && !commit) announce(`Move cancelled. ${items[from].title} is back at position ${from + 1}.`)
|
|
112
|
+
}
|
|
113
|
+
const onUp = () => finish(true)
|
|
114
|
+
const onKey = (ev: KeyboardEvent) => { if (ev.key === "Escape") { ev.preventDefault(); finish(false) } }
|
|
115
|
+
window.addEventListener("pointermove", onMove)
|
|
116
|
+
window.addEventListener("pointerup", onUp)
|
|
117
|
+
window.addEventListener("keydown", onKey)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Items in the order they'd have if dropped now; the dragged one becomes the landing slot.
|
|
121
|
+
const preview = React.useMemo(() => {
|
|
122
|
+
if (!drag) return items
|
|
123
|
+
const rest = items.filter((i) => i.id !== drag.id)
|
|
124
|
+
return [...rest.slice(0, drag.over), items[drag.from], ...rest.slice(drag.over)]
|
|
125
|
+
}, [items, drag])
|
|
126
|
+
|
|
127
|
+
// FLIP: cells glide to their new places instead of jumping.
|
|
128
|
+
const rects = React.useRef(new Map<string, DOMRect>())
|
|
129
|
+
React.useLayoutEffect(() => {
|
|
130
|
+
const cells = Array.from(listRef.current?.querySelectorAll<HTMLElement>("[data-cell-id]") ?? [])
|
|
131
|
+
const reduce = reducedMotion()
|
|
132
|
+
for (const c of cells) {
|
|
133
|
+
const id = c.dataset.cellId!
|
|
134
|
+
const now = c.getBoundingClientRect()
|
|
135
|
+
const before = rects.current.get(id)
|
|
136
|
+
if (before && !reduce && (before.left !== now.left || before.top !== now.top)) {
|
|
137
|
+
c.animate([{ transform: `translate(${before.left - now.left}px, ${before.top - now.top}px)` }, { transform: "none" }], { duration: 180, easing: "cubic-bezier(0.2, 0, 0, 1)" })
|
|
138
|
+
}
|
|
139
|
+
rects.current.set(id, now)
|
|
140
|
+
}
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
const draggingItem = drag ? items[drag.from] : null
|
|
144
|
+
const active = !!drag || paletteOver !== null
|
|
145
|
+
const value: BoardCtx = {
|
|
146
|
+
items,
|
|
147
|
+
move,
|
|
148
|
+
resize: (id, span) => {
|
|
149
|
+
const sp = Math.max(1, Math.min(COLUMNS, span))
|
|
150
|
+
const it = items.find((i) => i.id === id)
|
|
151
|
+
if (!it || (it.columnSpan ?? 1) === sp) return
|
|
152
|
+
onItemsChange(items.map((i) => (i.id === id ? { ...i, columnSpan: sp } : i)))
|
|
153
|
+
announce(`${it.title} now ${sp} of ${COLUMNS} columns wide.`)
|
|
154
|
+
settle(id)
|
|
155
|
+
},
|
|
156
|
+
remove: (id) => {
|
|
157
|
+
const it = items.find((i) => i.id === id)
|
|
158
|
+
onItemsChange(items.filter((i) => i.id !== id))
|
|
159
|
+
if (it) announce(`${it.title} removed.`)
|
|
160
|
+
},
|
|
161
|
+
announce,
|
|
162
|
+
startDrag,
|
|
163
|
+
draggingId: drag?.id ?? null,
|
|
164
|
+
settledId,
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const paletteCells = React.useRef<{ id: string; r: DOMRect }[] | null>(null)
|
|
168
|
+
const onPaletteOver = (e: React.DragEvent) => {
|
|
169
|
+
if (!e.dataTransfer.types.includes(PALETTE_MIME)) return
|
|
170
|
+
e.preventDefault()
|
|
171
|
+
e.dataTransfer.dropEffect = "copy"
|
|
172
|
+
paletteCells.current ??= snapshot()
|
|
173
|
+
const at = slotFrom(paletteCells.current, e.clientX, e.clientY)
|
|
174
|
+
if (at !== paletteOver) setPaletteOver(at)
|
|
175
|
+
}
|
|
176
|
+
const onPaletteDrop = (e: React.DragEvent) => {
|
|
177
|
+
const pid = e.dataTransfer.getData(PALETTE_MIME)
|
|
178
|
+
if (pid && onAddFromPalette) { e.preventDefault(); onAddFromPalette(pid, paletteOver ?? items.length) }
|
|
179
|
+
paletteCells.current = null
|
|
180
|
+
setPaletteOver(null)
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const slot = (key: string, span = 1, h?: number) => (
|
|
184
|
+
<li
|
|
185
|
+
key={key}
|
|
186
|
+
aria-hidden="true"
|
|
187
|
+
className={cn("min-h-24 rounded-sm border-2 border-dashed border-primary/50 bg-primary-soft/60 h-(--slot-h)", SPAN[span])}
|
|
188
|
+
style={h ? ({ "--slot-h": `${h}px` } as React.CSSProperties) : undefined}
|
|
189
|
+
/>
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
return (
|
|
193
|
+
<Ctx.Provider value={value}>
|
|
194
|
+
<section
|
|
195
|
+
aria-label={label}
|
|
196
|
+
data-slot="board"
|
|
197
|
+
data-dragging={active || undefined}
|
|
198
|
+
className={cn("@container rounded-md transition-[background-color,box-shadow] duration-(--duration-fast)", active && "bg-primary-soft/30 ring-1 ring-primary/25 ring-offset-4 ring-offset-bg", className)}
|
|
199
|
+
onDragOver={onPaletteOver}
|
|
200
|
+
onDragLeave={(e) => { if (!e.currentTarget.contains(e.relatedTarget as Node)) { paletteCells.current = null; setPaletteOver(null) } }}
|
|
201
|
+
onDrop={onPaletteDrop}
|
|
202
|
+
>
|
|
203
|
+
{items.length === 0 && paletteOver === null ? (
|
|
204
|
+
empty
|
|
205
|
+
) : (
|
|
206
|
+
<ul ref={listRef} className="grid grid-cols-1 gap-3 @2xl:grid-cols-2 @5xl:grid-cols-4">
|
|
207
|
+
{preview.map((it, i) => {
|
|
208
|
+
if (drag && it.id === drag.id) return slot(`slot-${it.id}`, it.columnSpan ?? 1, drag.h)
|
|
209
|
+
return (
|
|
210
|
+
<React.Fragment key={it.id}>
|
|
211
|
+
{paletteOver === i && slot("palette-slot")}
|
|
212
|
+
<li data-cell-id={it.id} data-slot="board-cell" className={cn("min-w-0", SPAN[it.columnSpan ?? 1])}>
|
|
213
|
+
{renderItem(it)}
|
|
214
|
+
</li>
|
|
215
|
+
</React.Fragment>
|
|
216
|
+
)
|
|
217
|
+
})}
|
|
218
|
+
{paletteOver !== null && paletteOver >= preview.length && slot("palette-slot")}
|
|
219
|
+
</ul>
|
|
220
|
+
)}
|
|
221
|
+
{drag && draggingItem && typeof document !== "undefined" && createPortal(
|
|
222
|
+
<div
|
|
223
|
+
aria-hidden="true"
|
|
224
|
+
className="pointer-events-none fixed z-50 w-(--w) rotate-1 cursor-grabbing opacity-95 shadow-pop left-(--x) top-(--y) [&_[data-slot=board-item]]:shadow-pop"
|
|
225
|
+
style={{ "--w": `${drag.w}px`, "--x": `${drag.x - drag.dx}px`, "--y": `${drag.y - drag.dy}px` } as React.CSSProperties}
|
|
226
|
+
>
|
|
227
|
+
{renderItem(draggingItem)}
|
|
228
|
+
</div>,
|
|
229
|
+
document.body,
|
|
230
|
+
)}
|
|
231
|
+
<div aria-live="assertive" className="sr-only">{message}</div>
|
|
232
|
+
</section>
|
|
233
|
+
</Ctx.Provider>
|
|
234
|
+
)
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
export function BoardItem({
|
|
238
|
+
id,
|
|
239
|
+
header,
|
|
240
|
+
description,
|
|
241
|
+
settings,
|
|
242
|
+
actions,
|
|
243
|
+
footer,
|
|
244
|
+
disableContentPaddings = false,
|
|
245
|
+
level = 3,
|
|
246
|
+
className,
|
|
247
|
+
children,
|
|
248
|
+
}: {
|
|
249
|
+
id: string
|
|
250
|
+
header: string
|
|
251
|
+
/** Heading level of the header. Use 2 when the board sits straight under the page's h1. */
|
|
252
|
+
level?: 2 | 3 | 4
|
|
253
|
+
description?: string
|
|
254
|
+
/** Extra items for the item's menu; Move, Resize and Remove are always there. */
|
|
255
|
+
settings?: React.ReactNode
|
|
256
|
+
/** One or two quiet buttons in the header, before the menu (Refresh, View in…). */
|
|
257
|
+
actions?: React.ReactNode
|
|
258
|
+
footer?: React.ReactNode
|
|
259
|
+
disableContentPaddings?: boolean
|
|
260
|
+
className?: string
|
|
261
|
+
children: React.ReactNode
|
|
262
|
+
}) {
|
|
263
|
+
const board = useBoard()
|
|
264
|
+
const index = board.items.findIndex((i) => i.id === id)
|
|
265
|
+
const span = board.items[index]?.columnSpan ?? 1
|
|
266
|
+
const [grabbed, setGrabbed] = React.useState<{ from: number } | null>(null)
|
|
267
|
+
const handle = React.useRef<HTMLButtonElement>(null)
|
|
268
|
+
const card = React.useRef<HTMLElement>(null)
|
|
269
|
+
const headingId = React.useId()
|
|
270
|
+
const settled = board.settledId === id
|
|
271
|
+
|
|
272
|
+
// Keyboard reorder: Enter or Space picks up, arrows move, Enter drops, Esc puts it back.
|
|
273
|
+
const onHandleKey = (e: React.KeyboardEvent) => {
|
|
274
|
+
const k = e.key
|
|
275
|
+
if (!grabbed && (k === "Enter" || k === " ")) {
|
|
276
|
+
e.preventDefault()
|
|
277
|
+
setGrabbed({ from: index })
|
|
278
|
+
board.announce(`Picked up ${header}, position ${index + 1} of ${board.items.length}. Use arrow keys to move, Enter to drop, Escape to cancel.`)
|
|
279
|
+
return
|
|
280
|
+
}
|
|
281
|
+
if (!grabbed) return
|
|
282
|
+
if (["ArrowLeft", "ArrowUp", "ArrowRight", "ArrowDown"].includes(k)) {
|
|
283
|
+
e.preventDefault()
|
|
284
|
+
board.move(id, index + (k === "ArrowLeft" || k === "ArrowUp" ? -1 : 1))
|
|
285
|
+
} else if (k === "Enter" || k === " ") {
|
|
286
|
+
e.preventDefault()
|
|
287
|
+
setGrabbed(null)
|
|
288
|
+
board.announce(`${header} dropped at position ${index + 1}.`)
|
|
289
|
+
} else if (k === "Escape") {
|
|
290
|
+
e.preventDefault()
|
|
291
|
+
board.move(id, grabbed.from)
|
|
292
|
+
setGrabbed(null)
|
|
293
|
+
board.announce(`Move cancelled. ${header} is back at position ${grabbed.from + 1}.`)
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
React.useEffect(() => { if (grabbed) handle.current?.focus() }, [grabbed, index])
|
|
297
|
+
|
|
298
|
+
return (
|
|
299
|
+
<article
|
|
300
|
+
ref={card}
|
|
301
|
+
aria-labelledby={headingId}
|
|
302
|
+
data-slot="board-item"
|
|
303
|
+
data-grabbed={grabbed ? "" : undefined}
|
|
304
|
+
className={cn(
|
|
305
|
+
"group/item flex h-full flex-col rounded-sm border bg-surface shadow-card transition-[box-shadow,border-color] duration-(--duration-base) ease-(--ease-out)",
|
|
306
|
+
grabbed ? "border-primary shadow-pop ring-3 ring-primary/20" : settled ? "border-primary ring-3 ring-primary/20" : "border-line hover:border-line-strong",
|
|
307
|
+
className
|
|
308
|
+
)}
|
|
309
|
+
>
|
|
310
|
+
<header className="flex items-start gap-1 py-2 pr-2 pl-1">
|
|
311
|
+
<Tooltip>
|
|
312
|
+
<TooltipTrigger asChild>
|
|
313
|
+
<button
|
|
314
|
+
ref={handle}
|
|
315
|
+
type="button"
|
|
316
|
+
aria-label={`Move ${header}`}
|
|
317
|
+
aria-description="Drag to move, or press Enter and use the arrow keys."
|
|
318
|
+
aria-pressed={!!grabbed}
|
|
319
|
+
onKeyDown={onHandleKey}
|
|
320
|
+
onPointerDown={(e) => { if (card.current && !grabbed) board.startDrag(id, e, card.current) }}
|
|
321
|
+
className="relative mt-px grid size-6 shrink-0 cursor-grab touch-none place-items-center rounded-xs text-ink-3 transition-colors group-hover/item:bg-hover group-hover/item:text-ink-2 hover:text-ink active:cursor-grabbing aria-pressed:bg-primary-soft aria-pressed:text-primary-ink"
|
|
322
|
+
>
|
|
323
|
+
<GripVertical className="size-3.5" aria-hidden="true" />
|
|
324
|
+
</button>
|
|
325
|
+
</TooltipTrigger>
|
|
326
|
+
<TooltipContent>Drag to move, or press Enter to use the arrow keys</TooltipContent>
|
|
327
|
+
</Tooltip>
|
|
328
|
+
<div className="grid min-w-0 flex-1 gap-0.5 pt-0.5">
|
|
329
|
+
{React.createElement(`h${level}`, { id: headingId, className: "truncate text-base font-semibold text-ink" }, header)}
|
|
330
|
+
{description && <p className="truncate text-sm text-ink-3">{description}</p>}
|
|
331
|
+
</div>
|
|
332
|
+
{actions && <div className="flex shrink-0 items-center gap-1">{actions}</div>}
|
|
333
|
+
<DropdownMenu>
|
|
334
|
+
<DropdownMenuTrigger asChild>
|
|
335
|
+
<Button variant="ghost" size="icon-sm" aria-label={`Settings for ${header}`}><Ellipsis /></Button>
|
|
336
|
+
</DropdownMenuTrigger>
|
|
337
|
+
<DropdownMenuContent align="end">
|
|
338
|
+
{settings}
|
|
339
|
+
{settings && <DropdownMenuSeparator />}
|
|
340
|
+
<DropdownMenuItem disabled={index === 0} onSelect={() => board.move(id, index - 1)}>Move earlier</DropdownMenuItem>
|
|
341
|
+
<DropdownMenuItem disabled={index === board.items.length - 1} onSelect={() => board.move(id, index + 1)}>Move later</DropdownMenuItem>
|
|
342
|
+
<DropdownMenuItem disabled={span >= COLUMNS} onSelect={() => board.resize(id, span + 1)}><MoveHorizontal />Wider</DropdownMenuItem>
|
|
343
|
+
<DropdownMenuItem disabled={span <= 1} onSelect={() => board.resize(id, span - 1)}>Narrower</DropdownMenuItem>
|
|
344
|
+
<DropdownMenuSeparator />
|
|
345
|
+
<DropdownMenuItem variant="destructive" onSelect={() => board.remove(id)}>Remove from dashboard</DropdownMenuItem>
|
|
346
|
+
</DropdownMenuContent>
|
|
347
|
+
</DropdownMenu>
|
|
348
|
+
</header>
|
|
349
|
+
<div className={cn("min-h-0 flex-1", !disableContentPaddings && "px-3 pb-3")}>{children}</div>
|
|
350
|
+
{footer && <footer className="border-t border-line px-3 py-2 text-sm text-ink-2">{footer}</footer>}
|
|
351
|
+
</article>
|
|
352
|
+
)
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/* ---------------------------------------------------------------- items palette */
|
|
356
|
+
|
|
357
|
+
export type PaletteItem = { id: string; title: string; description: string; type: "Key metrics" | "Chart" | "List" | "Status" | "Links" }
|
|
358
|
+
|
|
359
|
+
/** Items not yet on the board: title, one line, type. Add with the button (keyboard and one click) or drag onto the board. */
|
|
360
|
+
export function ItemsPalette({ items, onAdd, className }: { items: PaletteItem[]; onAdd: (id: string) => void; className?: string }) {
|
|
361
|
+
const [q, setQ] = React.useState("")
|
|
362
|
+
const shown = items.filter((i) => `${i.title} ${i.description}`.toLowerCase().includes(q.toLowerCase()))
|
|
363
|
+
return (
|
|
364
|
+
<div data-slot="items-palette" className={cn("grid content-start gap-2", className)}>
|
|
365
|
+
{items.length > 10 && (
|
|
366
|
+
<div className="relative">
|
|
367
|
+
<Search className="pointer-events-none absolute top-1/2 left-2.5 size-3.5 -translate-y-1/2 text-ink-3" aria-hidden="true" />
|
|
368
|
+
<Input value={q} onChange={(e) => setQ(e.target.value)} placeholder="Find an item" aria-label="Find an item" className="pl-8" />
|
|
369
|
+
</div>
|
|
370
|
+
)}
|
|
371
|
+
{items.length === 0 ? (
|
|
372
|
+
<p className="py-6 text-center text-base text-ink-2">All items are on your dashboard.</p>
|
|
373
|
+
) : shown.length === 0 ? (
|
|
374
|
+
<p className="flex items-center justify-center gap-2 py-6 text-base text-ink-2">No items match. <Button variant="ghost" size="xs" onClick={() => setQ("")}>Clear search</Button></p>
|
|
375
|
+
) : (
|
|
376
|
+
<ul className="grid gap-2" aria-label="Items you can add">
|
|
377
|
+
{shown.map((it) => (
|
|
378
|
+
<li
|
|
379
|
+
key={it.id}
|
|
380
|
+
draggable
|
|
381
|
+
onDragStart={(e) => { e.dataTransfer.setData(PALETTE_MIME, it.id); e.dataTransfer.effectAllowed = "copy" }}
|
|
382
|
+
className="group/pal flex cursor-grab items-start gap-2 rounded-sm border border-line bg-surface p-2 shadow-xs transition-[border-color,box-shadow] duration-(--duration-fast) hover:border-line-strong hover:shadow-card active:cursor-grabbing"
|
|
383
|
+
>
|
|
384
|
+
<GripVertical className="mt-0.5 size-3.5 shrink-0 text-ink-3 transition-colors group-hover/pal:text-ink-2" aria-hidden="true" />
|
|
385
|
+
<span className="grid min-w-0 flex-1 gap-0.5">
|
|
386
|
+
<span className="text-base font-medium text-ink">{it.title}</span>
|
|
387
|
+
<span className="text-sm text-ink-2">{it.description}</span>
|
|
388
|
+
<span className="text-xs text-ink-3">{it.type}</span>
|
|
389
|
+
</span>
|
|
390
|
+
<Button variant="outline" size="xs" onClick={() => onAdd(it.id)} aria-label={`Add ${it.title} to dashboard`}><Plus />Add</Button>
|
|
391
|
+
</li>
|
|
392
|
+
))}
|
|
393
|
+
</ul>
|
|
394
|
+
)}
|
|
395
|
+
</div>
|
|
396
|
+
)
|
|
397
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generated from shadcn/ui new-york-v4 "breadcrumb" by scripts/tailor-shadcn.mjs. Do not edit by hand.
|
|
3
|
+
* TwoUX changes:
|
|
4
|
+
* - One line in the top bar: never wraps; the current page truncates last and reads darker
|
|
5
|
+
* - Separator chevrons are small and muted
|
|
6
|
+
*/
|
|
7
|
+
import * as React from "react"
|
|
8
|
+
import { cn } from "../lib/cn"
|
|
9
|
+
import { ChevronRight, MoreHorizontal } from "lucide-react"
|
|
10
|
+
import { Slot } from "radix-ui"
|
|
11
|
+
|
|
12
|
+
function Breadcrumb({ ...props }: React.ComponentProps<"nav">) {
|
|
13
|
+
return <nav aria-label="breadcrumb" data-slot="breadcrumb" {...props} />
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function BreadcrumbList({ className, ...props }: React.ComponentProps<"ol">) {
|
|
17
|
+
return (
|
|
18
|
+
<ol
|
|
19
|
+
data-slot="breadcrumb-list"
|
|
20
|
+
className={cn(
|
|
21
|
+
"flex min-w-0 flex-nowrap items-center gap-1.5 text-base text-ink-2",
|
|
22
|
+
className
|
|
23
|
+
)}
|
|
24
|
+
{...props}
|
|
25
|
+
/>
|
|
26
|
+
)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function BreadcrumbItem({ className, ...props }: React.ComponentProps<"li">) {
|
|
30
|
+
return (
|
|
31
|
+
<li
|
|
32
|
+
data-slot="breadcrumb-item"
|
|
33
|
+
className={cn("inline-flex min-w-0 items-center gap-1.5", className)}
|
|
34
|
+
{...props}
|
|
35
|
+
/>
|
|
36
|
+
)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function BreadcrumbLink({
|
|
40
|
+
asChild,
|
|
41
|
+
className,
|
|
42
|
+
...props
|
|
43
|
+
}: React.ComponentProps<"a"> & {
|
|
44
|
+
asChild?: boolean
|
|
45
|
+
}) {
|
|
46
|
+
const Comp = asChild ? Slot.Root : "a"
|
|
47
|
+
|
|
48
|
+
return (
|
|
49
|
+
<Comp
|
|
50
|
+
data-slot="breadcrumb-link"
|
|
51
|
+
className={cn("rounded-xs whitespace-nowrap transition-colors duration-(--duration-fast) hover:text-ink", className)}
|
|
52
|
+
{...props}
|
|
53
|
+
/>
|
|
54
|
+
)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function BreadcrumbPage({ className, ...props }: React.ComponentProps<"span">) {
|
|
58
|
+
return (
|
|
59
|
+
<span
|
|
60
|
+
data-slot="breadcrumb-page"
|
|
61
|
+
role="link"
|
|
62
|
+
aria-disabled="true"
|
|
63
|
+
aria-current="page"
|
|
64
|
+
className={cn("truncate font-medium text-ink", className)}
|
|
65
|
+
{...props}
|
|
66
|
+
/>
|
|
67
|
+
)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function BreadcrumbSeparator({
|
|
71
|
+
children,
|
|
72
|
+
className,
|
|
73
|
+
...props
|
|
74
|
+
}: React.ComponentProps<"li">) {
|
|
75
|
+
return (
|
|
76
|
+
<li
|
|
77
|
+
data-slot="breadcrumb-separator"
|
|
78
|
+
role="presentation"
|
|
79
|
+
aria-hidden="true"
|
|
80
|
+
className={cn("text-ink-3 [&>svg]:size-3", className)}
|
|
81
|
+
{...props}
|
|
82
|
+
>
|
|
83
|
+
{children ?? <ChevronRight />}
|
|
84
|
+
</li>
|
|
85
|
+
)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function BreadcrumbEllipsis({
|
|
89
|
+
className,
|
|
90
|
+
...props
|
|
91
|
+
}: React.ComponentProps<"span">) {
|
|
92
|
+
return (
|
|
93
|
+
<span
|
|
94
|
+
data-slot="breadcrumb-ellipsis"
|
|
95
|
+
role="presentation"
|
|
96
|
+
aria-hidden="true"
|
|
97
|
+
className={cn("flex size-6 items-center justify-center", className)}
|
|
98
|
+
{...props}
|
|
99
|
+
>
|
|
100
|
+
<MoreHorizontal className="size-4" />
|
|
101
|
+
<span className="sr-only">More</span>
|
|
102
|
+
</span>
|
|
103
|
+
)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export {
|
|
107
|
+
Breadcrumb,
|
|
108
|
+
BreadcrumbList,
|
|
109
|
+
BreadcrumbItem,
|
|
110
|
+
BreadcrumbLink,
|
|
111
|
+
BreadcrumbPage,
|
|
112
|
+
BreadcrumbSeparator,
|
|
113
|
+
BreadcrumbEllipsis,
|
|
114
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generated from shadcn/ui new-york-v4 "button-group" by scripts/tailor-shadcn.mjs. Do not edit by hand.
|
|
3
|
+
* TwoUX changes:
|
|
4
|
+
* - Text segment on the quiet surface with the light edge, 12px padding and 14px icons, in proportion with compact buttons
|
|
5
|
+
* - The divider between two filled buttons uses the primary-on-primary edge, so a split primary button reads as one control with two parts
|
|
6
|
+
*/
|
|
7
|
+
import * as React from "react"
|
|
8
|
+
import { cva, type VariantProps } from "class-variance-authority"
|
|
9
|
+
import { cn } from "../lib/cn"
|
|
10
|
+
import { Slot } from "radix-ui"
|
|
11
|
+
|
|
12
|
+
import { Separator } from "./separator"
|
|
13
|
+
|
|
14
|
+
const buttonGroupVariants = cva(
|
|
15
|
+
"flex w-fit items-stretch has-[>[data-slot=button-group]]:gap-2 [&>*]:focus-visible:relative [&>*]:focus-visible:z-10 has-[select[aria-hidden=true]:last-child]:[&>[data-slot=select-trigger]:last-of-type]:rounded-r-sm [&>[data-slot=select-trigger]:not([class*='w-'])]:w-fit [&>input]:flex-1",
|
|
16
|
+
{
|
|
17
|
+
variants: {
|
|
18
|
+
orientation: {
|
|
19
|
+
horizontal:
|
|
20
|
+
"[&>*:not(:first-child)]:rounded-l-none [&>*:not(:first-child)]:border-l-0 [&>*:not(:last-child)]:rounded-r-none",
|
|
21
|
+
vertical:
|
|
22
|
+
"flex-col [&>*:not(:first-child)]:rounded-t-none [&>*:not(:first-child)]:border-t-0 [&>*:not(:last-child)]:rounded-b-none",
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
defaultVariants: {
|
|
26
|
+
orientation: "horizontal",
|
|
27
|
+
},
|
|
28
|
+
}
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
function ButtonGroup({
|
|
32
|
+
className,
|
|
33
|
+
orientation,
|
|
34
|
+
...props
|
|
35
|
+
}: React.ComponentProps<"div"> & VariantProps<typeof buttonGroupVariants>) {
|
|
36
|
+
return (
|
|
37
|
+
<div
|
|
38
|
+
role="group"
|
|
39
|
+
data-slot="button-group"
|
|
40
|
+
data-orientation={orientation}
|
|
41
|
+
className={cn(buttonGroupVariants({ orientation }), className)}
|
|
42
|
+
{...props}
|
|
43
|
+
/>
|
|
44
|
+
)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function ButtonGroupText({
|
|
48
|
+
className,
|
|
49
|
+
asChild = false,
|
|
50
|
+
...props
|
|
51
|
+
}: React.ComponentProps<"div"> & {
|
|
52
|
+
asChild?: boolean
|
|
53
|
+
}) {
|
|
54
|
+
const Comp = asChild ? Slot.Root : "div"
|
|
55
|
+
|
|
56
|
+
return (
|
|
57
|
+
<Comp
|
|
58
|
+
className={cn(
|
|
59
|
+
"flex items-center gap-2 rounded-sm border border-line bg-surface-2 px-3 text-base font-medium shadow-xs [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-3.5",
|
|
60
|
+
className
|
|
61
|
+
)}
|
|
62
|
+
{...props}
|
|
63
|
+
/>
|
|
64
|
+
)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function ButtonGroupSeparator({
|
|
68
|
+
className,
|
|
69
|
+
orientation = "vertical",
|
|
70
|
+
...props
|
|
71
|
+
}: React.ComponentProps<typeof Separator>) {
|
|
72
|
+
return (
|
|
73
|
+
<Separator
|
|
74
|
+
data-slot="button-group-separator"
|
|
75
|
+
orientation={orientation}
|
|
76
|
+
className={cn(
|
|
77
|
+
"relative m-0! self-stretch bg-input data-[orientation=vertical]:h-auto in-data-[variant=default]:bg-primary-foreground/30",
|
|
78
|
+
className
|
|
79
|
+
)}
|
|
80
|
+
{...props}
|
|
81
|
+
/>
|
|
82
|
+
)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export {
|
|
86
|
+
ButtonGroup,
|
|
87
|
+
ButtonGroupSeparator,
|
|
88
|
+
ButtonGroupText,
|
|
89
|
+
buttonGroupVariants,
|
|
90
|
+
}
|