@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,417 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* PaneLayout: a workspace that fills its container with columns of panes, for tool pages such as trading screens
|
|
5
|
+
* and editors (see proposals/resizable-panels.md). Panes resize with splitters and move by dragging their grip.
|
|
6
|
+
* Every drag has a keyboard path (splitter arrows, Home, End; grip Space, arrows, Space, Esc) and a menu path
|
|
7
|
+
* (Move up, down, left, right; Wider, Narrower, Taller, Shorter), as WCAG 2.5.7 asks, and every change is announced.
|
|
8
|
+
* Splitters follow the WAI-ARIA window splitter pattern.
|
|
9
|
+
* Any pane collapses to its header from its menu; a column of collapsed panes becomes a 36px strip that expands on click.
|
|
10
|
+
* Below `stackBelow` pixels the panes stack in one column and the page scrolls instead.
|
|
11
|
+
*/
|
|
12
|
+
import { ChevronDown, ChevronRight, Ellipsis, GripVertical } from "lucide-react"
|
|
13
|
+
import * as React from "react"
|
|
14
|
+
import { cn } from "../lib/cn"
|
|
15
|
+
import { Button } from "./button"
|
|
16
|
+
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger } from "./dropdown-menu"
|
|
17
|
+
|
|
18
|
+
/** collapsed keeps a pane's header only; a column whose panes are all collapsed becomes a 36px strip. */
|
|
19
|
+
export type PaneLayoutValue = { id: string; size: number; panes: { id: string; size: number; collapsed?: boolean }[] }[]
|
|
20
|
+
export type PaneDef = {
|
|
21
|
+
title: string
|
|
22
|
+
/** Replaces the title in the header, such as a segmented control switching the pane's view. The title still names the pane. */
|
|
23
|
+
header?: React.ReactNode
|
|
24
|
+
/** One control at most, such as a period switch; everything else goes in the content. */
|
|
25
|
+
actions?: React.ReactNode
|
|
26
|
+
/** DropdownMenuItems for this pane, shown first in its ⋯ menu, such as Show as table. */
|
|
27
|
+
menu?: React.ReactNode
|
|
28
|
+
children: React.ReactNode
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const MIN_W = 200
|
|
32
|
+
const MIN_H = 96
|
|
33
|
+
const STEP = 16
|
|
34
|
+
const BIG = 64
|
|
35
|
+
|
|
36
|
+
type Size = { width: number; height: number; stacked: boolean }
|
|
37
|
+
const SizeCtx = React.createContext<Size>({ width: 0, height: 0, stacked: false })
|
|
38
|
+
/** The content box of the pane this is rendered in, so a chart can draw at the height it has. */
|
|
39
|
+
export const usePaneSize = () => React.useContext(SizeCtx)
|
|
40
|
+
|
|
41
|
+
function useBox<T extends HTMLElement>() {
|
|
42
|
+
const ref = React.useRef<T>(null)
|
|
43
|
+
const [box, setBox] = React.useState({ width: 0, height: 0 })
|
|
44
|
+
React.useLayoutEffect(() => {
|
|
45
|
+
const el = ref.current
|
|
46
|
+
if (!el) return
|
|
47
|
+
const ro = new ResizeObserver(([e]) => setBox({ width: Math.round(e.contentRect.width), height: Math.round(e.contentRect.height) }))
|
|
48
|
+
ro.observe(el)
|
|
49
|
+
return () => ro.disconnect()
|
|
50
|
+
}, [])
|
|
51
|
+
return [ref, box] as const
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const clone = (l: PaneLayoutValue): PaneLayoutValue => l.map((c) => ({ ...c, panes: c.panes.map((p) => ({ ...p })) }))
|
|
55
|
+
const locate = (l: PaneLayoutValue, id: string) => {
|
|
56
|
+
for (let c = 0; c < l.length; c++) {
|
|
57
|
+
const i = l[c].panes.findIndex((p) => p.id === id)
|
|
58
|
+
if (i >= 0) return { c, i }
|
|
59
|
+
}
|
|
60
|
+
return null
|
|
61
|
+
}
|
|
62
|
+
function moveTo(l: PaneLayoutValue, id: string, c: number, i: number): PaneLayoutValue {
|
|
63
|
+
const at = locate(l, id)
|
|
64
|
+
if (!at) return l
|
|
65
|
+
const next = clone(l)
|
|
66
|
+
const [pane] = next[at.c].panes.splice(at.i, 1)
|
|
67
|
+
const target = next[c]
|
|
68
|
+
// A pane's size is a share of its own column, so in a new column it takes an equal share.
|
|
69
|
+
if (at.c !== c) pane.size = target.panes.length ? target.panes.reduce((t, p) => t + p.size, 0) / target.panes.length : 1
|
|
70
|
+
target.panes.splice(Math.max(0, Math.min(i, target.panes.length)), 0, pane)
|
|
71
|
+
return next.filter((col) => col.panes.length > 0)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function PaneLayout({
|
|
75
|
+
value,
|
|
76
|
+
onValueChange,
|
|
77
|
+
panes,
|
|
78
|
+
onReset,
|
|
79
|
+
stackBelow = 900,
|
|
80
|
+
className,
|
|
81
|
+
"aria-label": label,
|
|
82
|
+
}: {
|
|
83
|
+
value: PaneLayoutValue
|
|
84
|
+
onValueChange: (next: PaneLayoutValue) => void
|
|
85
|
+
panes: Record<string, PaneDef>
|
|
86
|
+
/** Adds Reset layout to every pane's menu. Apply it at once and offer Undo in a toast. */
|
|
87
|
+
onReset?: () => void
|
|
88
|
+
/** Container width below which panes stack in one column. */
|
|
89
|
+
stackBelow?: number
|
|
90
|
+
className?: string
|
|
91
|
+
"aria-label": string
|
|
92
|
+
}) {
|
|
93
|
+
const [rootRef, root] = useBox<HTMLDivElement>()
|
|
94
|
+
const stacked = root.width > 0 && root.width < stackBelow
|
|
95
|
+
const [message, setMessage] = React.useState("")
|
|
96
|
+
const announce = React.useCallback((t: string) => setMessage((m) => (m === t ? `${t} ` : t)), [])
|
|
97
|
+
const title = (id: string) => panes[id]?.title ?? id
|
|
98
|
+
const where = (l: PaneLayoutValue, id: string) => {
|
|
99
|
+
const at = locate(l, id)
|
|
100
|
+
return at ? `column ${at.c + 1} of ${l.length}, position ${at.i + 1} of ${l[at.c].panes.length}` : ""
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/* ---------- reorder ---------- */
|
|
104
|
+
const [drag, setDrag] = React.useState<{ id: string; over: string | null; after: boolean } | null>(null)
|
|
105
|
+
const startDrag = (id: string, e: React.PointerEvent) => {
|
|
106
|
+
if (e.button !== 0) return
|
|
107
|
+
e.preventDefault()
|
|
108
|
+
let state = { id, over: null as string | null, after: false }
|
|
109
|
+
setDrag(state)
|
|
110
|
+
announce(`Dragging ${title(id)}.`)
|
|
111
|
+
const onMove = (ev: PointerEvent) => {
|
|
112
|
+
const hit = document.elementsFromPoint(ev.clientX, ev.clientY).map((el) => (el as HTMLElement).closest<HTMLElement>("[data-pane-id]")).find((el) => el && el.dataset.paneId !== id)
|
|
113
|
+
if (!hit) return
|
|
114
|
+
const r = hit.getBoundingClientRect()
|
|
115
|
+
state = { id, over: hit.dataset.paneId ?? null, after: ev.clientY > r.top + r.height / 2 }
|
|
116
|
+
setDrag(state)
|
|
117
|
+
}
|
|
118
|
+
const onUp = () => {
|
|
119
|
+
window.removeEventListener("pointermove", onMove)
|
|
120
|
+
window.removeEventListener("pointerup", onUp)
|
|
121
|
+
setDrag(null)
|
|
122
|
+
if (!state.over) return announce(`${title(id)} not moved.`)
|
|
123
|
+
const to = locate(value, state.over)
|
|
124
|
+
const from = locate(value, id)
|
|
125
|
+
if (!to || !from) return
|
|
126
|
+
const i = to.i + (state.after ? 1 : 0) - (from.c === to.c && from.i < to.i ? 1 : 0)
|
|
127
|
+
const next = moveTo(value, id, to.c, i)
|
|
128
|
+
onValueChange(next)
|
|
129
|
+
announce(`${title(id)} moved to ${where(next, id)}.`)
|
|
130
|
+
}
|
|
131
|
+
window.addEventListener("pointermove", onMove)
|
|
132
|
+
window.addEventListener("pointerup", onUp)
|
|
133
|
+
}
|
|
134
|
+
const step = (id: string, dir: "up" | "down" | "left" | "right", l = value) => {
|
|
135
|
+
const at = locate(l, id)
|
|
136
|
+
if (!at) return l
|
|
137
|
+
if (dir === "up") return at.i > 0 ? moveTo(l, id, at.c, at.i - 1) : l
|
|
138
|
+
if (dir === "down") return at.i < l[at.c].panes.length - 1 ? moveTo(l, id, at.c, at.i + 1) : l
|
|
139
|
+
const c = at.c + (dir === "left" ? -1 : 1)
|
|
140
|
+
if (c < 0 || c >= l.length) return l
|
|
141
|
+
return moveTo(l, id, c, at.i)
|
|
142
|
+
}
|
|
143
|
+
const can = (id: string, dir: "up" | "down" | "left" | "right") => step(id, dir) !== value
|
|
144
|
+
const menuMove = (id: string, dir: "up" | "down" | "left" | "right") => {
|
|
145
|
+
const next = step(id, dir)
|
|
146
|
+
onValueChange(next)
|
|
147
|
+
announce(`${title(id)} moved to ${where(next, id)}.`)
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Keyboard pick-up on the grip: Space or Enter picks up and drops, arrows move, Esc puts it back.
|
|
151
|
+
const [picked, setPicked] = React.useState<{ id: string; before: PaneLayoutValue } | null>(null)
|
|
152
|
+
const gripKey = (id: string, e: React.KeyboardEvent) => {
|
|
153
|
+
if (e.key === " " || e.key === "Enter") {
|
|
154
|
+
e.preventDefault()
|
|
155
|
+
if (picked?.id === id) { setPicked(null); announce(`${title(id)} dropped at ${where(value, id)}.`) }
|
|
156
|
+
else { setPicked({ id, before: value }); announce(`${title(id)} picked up. Use the arrow keys to move it, Space to drop it, Escape to cancel.`) }
|
|
157
|
+
return
|
|
158
|
+
}
|
|
159
|
+
if (picked?.id !== id) return
|
|
160
|
+
const dir = ({ ArrowUp: "up", ArrowDown: "down", ArrowLeft: "left", ArrowRight: "right" } as const)[e.key as "ArrowUp"]
|
|
161
|
+
if (dir) {
|
|
162
|
+
e.preventDefault()
|
|
163
|
+
const next = step(id, dir)
|
|
164
|
+
if (next !== value) { onValueChange(next); announce(`${where(next, id)}.`) }
|
|
165
|
+
} else if (e.key === "Escape") {
|
|
166
|
+
e.preventDefault()
|
|
167
|
+
onValueChange(picked.before)
|
|
168
|
+
setPicked(null)
|
|
169
|
+
announce(`${title(id)} put back.`)
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
// Moving a pane remounts its grip; keep keyboard focus on it.
|
|
173
|
+
const grips = React.useRef(new Map<string, HTMLButtonElement>())
|
|
174
|
+
React.useEffect(() => { if (picked) grips.current.get(picked.id)?.focus() }, [value, picked])
|
|
175
|
+
|
|
176
|
+
/* ---------- resize ---------- */
|
|
177
|
+
const colRefs = React.useRef<(HTMLDivElement | null)[]>([])
|
|
178
|
+
const resize = (kind: "col" | "pane", c: number, i: number, deltaPx: number) => {
|
|
179
|
+
const next = clone(value)
|
|
180
|
+
const list = kind === "col" ? next : next[c].panes
|
|
181
|
+
const els = kind === "col" ? colRefs.current : Array.from(colRefs.current[c]?.querySelectorAll<HTMLElement>(":scope > [data-pane-id]") ?? [])
|
|
182
|
+
const px = (k: number) => (kind === "col" ? els[k]?.getBoundingClientRect().width : els[k]?.getBoundingClientRect().height) ?? 0
|
|
183
|
+
const a = px(i), b = px(i + 1)
|
|
184
|
+
const min = kind === "col" ? MIN_W : MIN_H
|
|
185
|
+
const d = Math.max(min - a, Math.min(b - min, deltaPx))
|
|
186
|
+
if (!a || !b || !d) return value
|
|
187
|
+
const total = list[i].size + list[i + 1].size
|
|
188
|
+
list[i].size = (total * (a + d)) / (a + b)
|
|
189
|
+
list[i + 1].size = total - list[i].size
|
|
190
|
+
return next
|
|
191
|
+
}
|
|
192
|
+
const splitter = (kind: "col" | "pane", c: number, i: number, name: string) => {
|
|
193
|
+
const vertical = kind === "col"
|
|
194
|
+
const list = vertical ? value : value[c].panes
|
|
195
|
+
const pct = Math.round((list[i].size / (list[i].size + list[i + 1].size)) * 100)
|
|
196
|
+
const onPointerDown = (e: React.PointerEvent) => {
|
|
197
|
+
if (e.button !== 0) return
|
|
198
|
+
e.preventDefault()
|
|
199
|
+
const start = vertical ? e.clientX : e.clientY
|
|
200
|
+
const base = value
|
|
201
|
+
let last = base
|
|
202
|
+
const onMove = (ev: PointerEvent) => {
|
|
203
|
+
const d = (vertical ? ev.clientX : ev.clientY) - start
|
|
204
|
+
// Resize from the layout at pointer-down, so the splitter tracks the pointer exactly.
|
|
205
|
+
last = resizeFrom(base, kind, c, i, d)
|
|
206
|
+
onValueChange(last)
|
|
207
|
+
}
|
|
208
|
+
const onUp = () => { window.removeEventListener("pointermove", onMove); window.removeEventListener("pointerup", onUp) }
|
|
209
|
+
window.addEventListener("pointermove", onMove)
|
|
210
|
+
window.addEventListener("pointerup", onUp)
|
|
211
|
+
}
|
|
212
|
+
const onKeyDown = (e: React.KeyboardEvent) => {
|
|
213
|
+
const by = e.shiftKey ? BIG : STEP
|
|
214
|
+
const keys: Record<string, number> = vertical ? { ArrowLeft: -by, ArrowRight: by, Home: -1e5, End: 1e5 } : { ArrowUp: -by, ArrowDown: by, Home: -1e5, End: 1e5 }
|
|
215
|
+
if (!(e.key in keys)) return
|
|
216
|
+
e.preventDefault()
|
|
217
|
+
onValueChange(resize(kind, c, i, keys[e.key]))
|
|
218
|
+
}
|
|
219
|
+
return (
|
|
220
|
+
<div
|
|
221
|
+
role="separator"
|
|
222
|
+
tabIndex={0}
|
|
223
|
+
aria-orientation={vertical ? "vertical" : "horizontal"}
|
|
224
|
+
aria-valuenow={pct}
|
|
225
|
+
aria-valuemin={0}
|
|
226
|
+
aria-valuemax={100}
|
|
227
|
+
aria-label={`Resize ${name}`}
|
|
228
|
+
onPointerDown={onPointerDown}
|
|
229
|
+
onKeyDown={onKeyDown}
|
|
230
|
+
className={cn(
|
|
231
|
+
"group/split relative z-10 shrink-0 touch-none bg-line outline-none",
|
|
232
|
+
vertical ? "w-px cursor-col-resize" : "h-px cursor-row-resize",
|
|
233
|
+
"before:absolute before:content-['']",
|
|
234
|
+
vertical ? "before:inset-y-0 before:-inset-x-1" : "before:inset-x-0 before:-inset-y-1",
|
|
235
|
+
"after:absolute after:bg-primary after:opacity-0 after:transition-opacity after:duration-(--duration-fast) after:content-[''] hover:after:opacity-100 focus-visible:after:opacity-100",
|
|
236
|
+
vertical ? "after:inset-y-0 after:-left-px after:w-0.5" : "after:inset-x-0 after:-top-px after:h-0.5"
|
|
237
|
+
)}
|
|
238
|
+
/>
|
|
239
|
+
)
|
|
240
|
+
}
|
|
241
|
+
// Same as resize, but from a fixed starting layout (for pointer drags).
|
|
242
|
+
const resizeFrom = (base: PaneLayoutValue, kind: "col" | "pane", c: number, i: number, d: number) => {
|
|
243
|
+
const next = clone(base)
|
|
244
|
+
const list = kind === "col" ? next : next[c].panes
|
|
245
|
+
const els = kind === "col" ? colRefs.current : Array.from(colRefs.current[c]?.querySelectorAll<HTMLElement>(":scope > [data-pane-id]") ?? [])
|
|
246
|
+
const total = list[i].size + list[i + 1].size
|
|
247
|
+
const span = (kind === "col" ? els.slice(i, i + 2).reduce((s, el) => s + (el?.getBoundingClientRect().width ?? 0), 0) : els.slice(i, i + 2).reduce((s, el) => s + (el?.getBoundingClientRect().height ?? 0), 0))
|
|
248
|
+
if (!span) return base
|
|
249
|
+
const baseA = (span * list[i].size) / total
|
|
250
|
+
const min = kind === "col" ? MIN_W : MIN_H
|
|
251
|
+
const a = Math.max(min, Math.min(span - min, baseA + d))
|
|
252
|
+
list[i].size = (total * a) / span
|
|
253
|
+
list[i + 1].size = total - list[i].size
|
|
254
|
+
return next
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Single-pointer resizing from the menu (WCAG 2.5.7): grow or shrink by 64px against the neighbour.
|
|
258
|
+
const grow = (id: string, axis: "width" | "height", d: number) => {
|
|
259
|
+
const at = locate(value, id)
|
|
260
|
+
if (!at) return
|
|
261
|
+
const next = axis === "width"
|
|
262
|
+
? at.c < value.length - 1 ? resize("col", at.c, at.c, d) : resize("col", at.c - 1, at.c - 1, -d)
|
|
263
|
+
: at.i < value[at.c].panes.length - 1 ? resize("pane", at.c, at.i, d) : resize("pane", at.c, at.i - 1, -d)
|
|
264
|
+
onValueChange(next)
|
|
265
|
+
announce(`${title(id)} ${d > 0 ? (axis === "width" ? "wider" : "taller") : axis === "width" ? "narrower" : "shorter"}.`)
|
|
266
|
+
}
|
|
267
|
+
const toggle = (id: string) => {
|
|
268
|
+
const next = clone(value)
|
|
269
|
+
const at = locate(next, id)
|
|
270
|
+
if (!at) return
|
|
271
|
+
const p = next[at.c].panes[at.i]
|
|
272
|
+
p.collapsed = !p.collapsed
|
|
273
|
+
onValueChange(next)
|
|
274
|
+
announce(`${title(id)} ${p.collapsed ? "collapsed" : "expanded"}.`)
|
|
275
|
+
}
|
|
276
|
+
const colCollapsed = (c: number) => !stacked && value[c].panes.every((p) => p.collapsed)
|
|
277
|
+
const pane = (id: string, c: number, i: number) => {
|
|
278
|
+
const def = panes[id]
|
|
279
|
+
if (!def) return null
|
|
280
|
+
const dropLine = drag?.over === id
|
|
281
|
+
const collapsed = !!value[c].panes[i].collapsed
|
|
282
|
+
if (colCollapsed(c)) {
|
|
283
|
+
return (
|
|
284
|
+
<section key={id} data-pane-id={id} aria-label={def.title} className="flex min-h-0 flex-1 flex-col items-center gap-2 bg-surface py-1">
|
|
285
|
+
<Button variant="ghost" size="icon-xs" aria-label={`Expand ${def.title}`} aria-expanded={false} onClick={() => toggle(id)}><ChevronRight /></Button>
|
|
286
|
+
<button type="button" tabIndex={-1} aria-hidden="true" onClick={() => toggle(id)} className="min-h-0 truncate text-sm font-medium text-ink-2 [writing-mode:vertical-rl] hover:text-ink">{def.title}</button>
|
|
287
|
+
</section>
|
|
288
|
+
)
|
|
289
|
+
}
|
|
290
|
+
return (
|
|
291
|
+
<PaneFrame
|
|
292
|
+
collapsed={collapsed}
|
|
293
|
+
onToggle={() => toggle(id)}
|
|
294
|
+
key={id}
|
|
295
|
+
id={id}
|
|
296
|
+
def={def}
|
|
297
|
+
stacked={stacked}
|
|
298
|
+
size={value[c].panes[i].size}
|
|
299
|
+
dragging={drag?.id === id || picked?.id === id}
|
|
300
|
+
drop={dropLine ? (drag?.after ? "after" : "before") : null}
|
|
301
|
+
gripRef={(el) => { if (el) grips.current.set(id, el); else grips.current.delete(id) }}
|
|
302
|
+
onGripPointerDown={(e) => startDrag(id, e)}
|
|
303
|
+
onGripKeyDown={(e) => gripKey(id, e)}
|
|
304
|
+
menu={
|
|
305
|
+
<DropdownMenuContent align="end">
|
|
306
|
+
{def.menu && !collapsed && <>{def.menu}<DropdownMenuSeparator /></>}
|
|
307
|
+
<DropdownMenuItem onSelect={() => toggle(id)}>{collapsed ? "Expand" : "Collapse"}</DropdownMenuItem>
|
|
308
|
+
<DropdownMenuSeparator />
|
|
309
|
+
<DropdownMenuItem disabled={!can(id, "up")} onSelect={() => menuMove(id, "up")}>Move up</DropdownMenuItem>
|
|
310
|
+
<DropdownMenuItem disabled={!can(id, "down")} onSelect={() => menuMove(id, "down")}>Move down</DropdownMenuItem>
|
|
311
|
+
{!stacked && <DropdownMenuItem disabled={!can(id, "left")} onSelect={() => menuMove(id, "left")}>Move left</DropdownMenuItem>}
|
|
312
|
+
{!stacked && <DropdownMenuItem disabled={!can(id, "right")} onSelect={() => menuMove(id, "right")}>Move right</DropdownMenuItem>}
|
|
313
|
+
{!stacked && (
|
|
314
|
+
<>
|
|
315
|
+
<DropdownMenuSeparator />
|
|
316
|
+
{value.length > 1 && <DropdownMenuItem onSelect={(e) => { e.preventDefault(); grow(id, "width", BIG) }}>Wider</DropdownMenuItem>}
|
|
317
|
+
{value.length > 1 && <DropdownMenuItem onSelect={(e) => { e.preventDefault(); grow(id, "width", -BIG) }}>Narrower</DropdownMenuItem>}
|
|
318
|
+
{!collapsed && value[c].panes.filter((p) => !p.collapsed).length > 1 && <DropdownMenuItem onSelect={(e) => { e.preventDefault(); grow(id, "height", BIG) }}>Taller</DropdownMenuItem>}
|
|
319
|
+
{!collapsed && value[c].panes.filter((p) => !p.collapsed).length > 1 && <DropdownMenuItem onSelect={(e) => { e.preventDefault(); grow(id, "height", -BIG) }}>Shorter</DropdownMenuItem>}
|
|
320
|
+
</>
|
|
321
|
+
)}
|
|
322
|
+
{onReset && <><DropdownMenuSeparator /><DropdownMenuItem onSelect={onReset}>Reset layout</DropdownMenuItem></>}
|
|
323
|
+
</DropdownMenuContent>
|
|
324
|
+
}
|
|
325
|
+
/>
|
|
326
|
+
)
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
return (
|
|
330
|
+
<div ref={rootRef} role="region" aria-label={label} className={cn("flex min-h-0 min-w-0", stacked ? "flex-none flex-col gap-px bg-line" : "h-full flex-1 flex-row", className)}>
|
|
331
|
+
{stacked
|
|
332
|
+
? value.flatMap((col, c) => col.panes.map((p, i) => pane(p.id, c, i)))
|
|
333
|
+
: value.map((col, c) => (
|
|
334
|
+
<React.Fragment key={col.id}>
|
|
335
|
+
{c > 0 && (colCollapsed(c - 1) || colCollapsed(c) ? <span aria-hidden="true" className="w-px shrink-0 bg-line" /> : splitter("col", c - 1, c - 1, `${value[c - 1].panes.map((p) => title(p.id)).join(" and ")} column`))}
|
|
336
|
+
<div ref={(el) => { colRefs.current[c] = el }} className={cn("flex min-h-0 min-w-0 flex-col", colCollapsed(c) ? "w-9 flex-none gap-px bg-line" : "basis-0 grow-(--g)")} style={{ "--g": col.size } as React.CSSProperties}>
|
|
337
|
+
{col.panes.map((p, i) => (
|
|
338
|
+
<React.Fragment key={p.id}>
|
|
339
|
+
{i > 0 && !colCollapsed(c) && (col.panes[i - 1].collapsed || p.collapsed ? <span aria-hidden="true" className="h-px shrink-0 bg-line" /> : splitter("pane", c, i - 1, title(col.panes[i - 1].id)))}
|
|
340
|
+
{pane(p.id, c, i)}
|
|
341
|
+
</React.Fragment>
|
|
342
|
+
))}
|
|
343
|
+
</div>
|
|
344
|
+
</React.Fragment>
|
|
345
|
+
))}
|
|
346
|
+
<p aria-live="polite" className="sr-only">{message}</p>
|
|
347
|
+
</div>
|
|
348
|
+
)
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function PaneFrame({
|
|
352
|
+
collapsed,
|
|
353
|
+
onToggle,
|
|
354
|
+
id,
|
|
355
|
+
def,
|
|
356
|
+
stacked,
|
|
357
|
+
size,
|
|
358
|
+
dragging,
|
|
359
|
+
drop,
|
|
360
|
+
gripRef,
|
|
361
|
+
onGripPointerDown,
|
|
362
|
+
onGripKeyDown,
|
|
363
|
+
menu,
|
|
364
|
+
}: {
|
|
365
|
+
id: string
|
|
366
|
+
def: PaneDef
|
|
367
|
+
stacked: boolean
|
|
368
|
+
size: number
|
|
369
|
+
dragging: boolean
|
|
370
|
+
drop: "before" | "after" | null
|
|
371
|
+
gripRef: (el: HTMLButtonElement | null) => void
|
|
372
|
+
onGripPointerDown: (e: React.PointerEvent) => void
|
|
373
|
+
onGripKeyDown: (e: React.KeyboardEvent) => void
|
|
374
|
+
menu: React.ReactNode
|
|
375
|
+
collapsed: boolean
|
|
376
|
+
onToggle: () => void
|
|
377
|
+
}) {
|
|
378
|
+
const [bodyRef, box] = useBox<HTMLDivElement>()
|
|
379
|
+
const headingId = React.useId()
|
|
380
|
+
return (
|
|
381
|
+
<section
|
|
382
|
+
data-pane-id={id}
|
|
383
|
+
aria-labelledby={headingId}
|
|
384
|
+
className={cn("relative flex min-w-0 flex-col bg-surface", stacked || collapsed ? "shrink-0" : "min-h-0 basis-0 grow-(--g)", dragging && "opacity-60")}
|
|
385
|
+
style={stacked ? undefined : ({ "--g": size } as React.CSSProperties)}
|
|
386
|
+
>
|
|
387
|
+
{drop && <span aria-hidden="true" className={cn("absolute inset-x-0 z-20 h-0.5 bg-primary", drop === "before" ? "top-0" : "bottom-0")} />}
|
|
388
|
+
<header className="flex h-8 shrink-0 items-center gap-1 border-b border-line pr-1 pl-0.5">
|
|
389
|
+
<button
|
|
390
|
+
ref={gripRef}
|
|
391
|
+
type="button"
|
|
392
|
+
aria-label={`Move ${def.title}`}
|
|
393
|
+
aria-roledescription="draggable pane"
|
|
394
|
+
aria-pressed={dragging || undefined}
|
|
395
|
+
onPointerDown={onGripPointerDown}
|
|
396
|
+
onKeyDown={onGripKeyDown}
|
|
397
|
+
className="grid h-6 w-4 shrink-0 cursor-grab touch-none place-items-center rounded-xs text-ink-3 outline-none hover:bg-hover hover:text-ink-2 focus-visible:ring-2 focus-visible:ring-primary active:cursor-grabbing [&_svg]:size-3.5"
|
|
398
|
+
>
|
|
399
|
+
<GripVertical />
|
|
400
|
+
</button>
|
|
401
|
+
<h2 id={headingId} className={cn("min-w-0 truncate text-sm font-medium text-ink", def.header && "sr-only")}>{def.title}</h2>
|
|
402
|
+
{def.header && <div className="flex min-w-0 items-center">{def.header}</div>}
|
|
403
|
+
<span className="flex-1" />
|
|
404
|
+
{collapsed ? <Button variant="ghost" size="icon-xs" aria-label={`Expand ${def.title}`} aria-expanded={false} onClick={onToggle}><ChevronDown /></Button> : def.actions}
|
|
405
|
+
<DropdownMenu>
|
|
406
|
+
<DropdownMenuTrigger asChild>
|
|
407
|
+
<Button variant="ghost" size="icon-xs" aria-label={`${def.title} options`}><Ellipsis /></Button>
|
|
408
|
+
</DropdownMenuTrigger>
|
|
409
|
+
{menu}
|
|
410
|
+
</DropdownMenu>
|
|
411
|
+
</header>
|
|
412
|
+
<div ref={bodyRef} hidden={collapsed} className={cn("min-h-0 flex-1", stacked ? "" : "overflow-auto")}>
|
|
413
|
+
<SizeCtx.Provider value={{ ...box, stacked }}>{def.children}</SizeCtx.Provider>
|
|
414
|
+
</div>
|
|
415
|
+
</section>
|
|
416
|
+
)
|
|
417
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generated from shadcn/ui new-york-v4 "popover" by scripts/tailor-shadcn.mjs. Do not edit by hand.
|
|
3
|
+
* TwoUX changes:
|
|
4
|
+
* - Floats like every overlay: 8px corners, light edge, the floating shadow, 12px padding
|
|
5
|
+
* - Never cut off: keeps 8px from the viewport edge and flips when there is no room
|
|
6
|
+
*/
|
|
7
|
+
"use client"
|
|
8
|
+
|
|
9
|
+
import * as React from "react"
|
|
10
|
+
import { cn } from "../lib/cn"
|
|
11
|
+
import { Popover as PopoverPrimitive } from "radix-ui"
|
|
12
|
+
|
|
13
|
+
function Popover({
|
|
14
|
+
...props
|
|
15
|
+
}: React.ComponentProps<typeof PopoverPrimitive.Root>) {
|
|
16
|
+
return <PopoverPrimitive.Root data-slot="popover" {...props} />
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function PopoverTrigger({
|
|
20
|
+
...props
|
|
21
|
+
}: React.ComponentProps<typeof PopoverPrimitive.Trigger>) {
|
|
22
|
+
return <PopoverPrimitive.Trigger data-slot="popover-trigger" {...props} />
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function PopoverContent({
|
|
26
|
+
className,
|
|
27
|
+
align = "center",
|
|
28
|
+
sideOffset = 4,
|
|
29
|
+
collisionPadding = 8,
|
|
30
|
+
...props
|
|
31
|
+
}: React.ComponentProps<typeof PopoverPrimitive.Content>) {
|
|
32
|
+
return (
|
|
33
|
+
<PopoverPrimitive.Portal>
|
|
34
|
+
<PopoverPrimitive.Content
|
|
35
|
+
data-slot="popover-content"
|
|
36
|
+
align={align}
|
|
37
|
+
sideOffset={sideOffset}
|
|
38
|
+
collisionPadding={collisionPadding}
|
|
39
|
+
className={cn(
|
|
40
|
+
"z-50 w-72 origin-(--radix-popover-content-transform-origin) rounded-md border border-line bg-popover p-3 text-popover-foreground shadow-pop outline-hidden data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95",
|
|
41
|
+
className
|
|
42
|
+
)}
|
|
43
|
+
{...props}
|
|
44
|
+
/>
|
|
45
|
+
</PopoverPrimitive.Portal>
|
|
46
|
+
)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function PopoverAnchor({
|
|
50
|
+
...props
|
|
51
|
+
}: React.ComponentProps<typeof PopoverPrimitive.Anchor>) {
|
|
52
|
+
return <PopoverPrimitive.Anchor data-slot="popover-anchor" {...props} />
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function PopoverHeader({ className, ...props }: React.ComponentProps<"div">) {
|
|
56
|
+
return (
|
|
57
|
+
<div
|
|
58
|
+
data-slot="popover-header"
|
|
59
|
+
className={cn("flex flex-col gap-1 text-base", className)}
|
|
60
|
+
{...props}
|
|
61
|
+
/>
|
|
62
|
+
)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function PopoverTitle({ className, ...props }: React.ComponentProps<"h2">) {
|
|
66
|
+
return (
|
|
67
|
+
<div
|
|
68
|
+
data-slot="popover-title"
|
|
69
|
+
className={cn("font-medium", className)}
|
|
70
|
+
{...props}
|
|
71
|
+
/>
|
|
72
|
+
)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function PopoverDescription({
|
|
76
|
+
className,
|
|
77
|
+
...props
|
|
78
|
+
}: React.ComponentProps<"p">) {
|
|
79
|
+
return (
|
|
80
|
+
<p
|
|
81
|
+
data-slot="popover-description"
|
|
82
|
+
className={cn("text-muted-foreground", className)}
|
|
83
|
+
{...props}
|
|
84
|
+
/>
|
|
85
|
+
)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export {
|
|
89
|
+
Popover,
|
|
90
|
+
PopoverTrigger,
|
|
91
|
+
PopoverContent,
|
|
92
|
+
PopoverAnchor,
|
|
93
|
+
PopoverHeader,
|
|
94
|
+
PopoverTitle,
|
|
95
|
+
PopoverDescription,
|
|
96
|
+
}
|