@twoux/ui 0.0.0-stage → 0.1.1

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.
Files changed (325) hide show
  1. package/LICENSE.md +61 -0
  2. package/LICENSES/Apache-2.0.txt +202 -0
  3. package/NOTICE.md +64 -0
  4. package/README.md +211 -2
  5. package/SHADCN-DIFF.md +228 -0
  6. package/agent/AGENTS.md +358 -0
  7. package/agent/docs-md/accessibility.md +49 -0
  8. package/agent/docs-md/action-catalogue.md +81 -0
  9. package/agent/docs-md/actions.md +74 -0
  10. package/agent/docs-md/agent-activity-log.md +70 -0
  11. package/agent/docs-md/agent-permissions.md +82 -0
  12. package/agent/docs-md/agent-run.md +90 -0
  13. package/agent/docs-md/agents-on-behalf.md +74 -0
  14. package/agent/docs-md/ai-column.md +85 -0
  15. package/agent/docs-md/ai-origin-label.md +70 -0
  16. package/agent/docs-md/announcing-features.md +73 -0
  17. package/agent/docs-md/answer-actions.md +81 -0
  18. package/agent/docs-md/app-shell.md +114 -0
  19. package/agent/docs-md/artifact-card.md +89 -0
  20. package/agent/docs-md/ask-on-screen.md +72 -0
  21. package/agent/docs-md/assistant-page.md +100 -0
  22. package/agent/docs-md/assistant-panel.md +142 -0
  23. package/agent/docs-md/avatar.md +82 -0
  24. package/agent/docs-md/banner.md +106 -0
  25. package/agent/docs-md/board.md +163 -0
  26. package/agent/docs-md/breadcrumb.md +89 -0
  27. package/agent/docs-md/breakdown-table.md +97 -0
  28. package/agent/docs-md/bulk-action-bar.md +112 -0
  29. package/agent/docs-md/button.md +293 -0
  30. package/agent/docs-md/card.md +115 -0
  31. package/agent/docs-md/change-list.md +99 -0
  32. package/agent/docs-md/change-request.md +166 -0
  33. package/agent/docs-md/chart-area.md +87 -0
  34. package/agent/docs-md/chart-bar.md +100 -0
  35. package/agent/docs-md/chart-box-plot.md +88 -0
  36. package/agent/docs-md/chart-bullet.md +89 -0
  37. package/agent/docs-md/chart-candlestick.md +112 -0
  38. package/agent/docs-md/chart-card-line.md +120 -0
  39. package/agent/docs-md/chart-donut.md +90 -0
  40. package/agent/docs-md/chart-drill-down.md +72 -0
  41. package/agent/docs-md/chart-funnel.md +71 -0
  42. package/agent/docs-md/chart-heatmap.md +76 -0
  43. package/agent/docs-md/chart-histogram.md +81 -0
  44. package/agent/docs-md/chart-map.md +165 -0
  45. package/agent/docs-md/chart-mixed.md +88 -0
  46. package/agent/docs-md/chart-price.md +92 -0
  47. package/agent/docs-md/chart-radial-bars.md +109 -0
  48. package/agent/docs-md/chart-radial-lines.md +111 -0
  49. package/agent/docs-md/chart-sankey.md +104 -0
  50. package/agent/docs-md/chart-scatter.md +87 -0
  51. package/agent/docs-md/chart-share-bar.md +89 -0
  52. package/agent/docs-md/chart-treemap.md +98 -0
  53. package/agent/docs-md/chart-waterfall.md +93 -0
  54. package/agent/docs-md/checkbox.md +131 -0
  55. package/agent/docs-md/choosing-selection-control.md +71 -0
  56. package/agent/docs-md/citations-sources.md +79 -0
  57. package/agent/docs-md/citing-sources.md +63 -0
  58. package/agent/docs-md/clarify-before-acting.md +69 -0
  59. package/agent/docs-md/clarifying-question.md +92 -0
  60. package/agent/docs-md/code-block.md +174 -0
  61. package/agent/docs-md/colours.md +87 -0
  62. package/agent/docs-md/columns-menu.md +140 -0
  63. package/agent/docs-md/combobox.md +161 -0
  64. package/agent/docs-md/command-palette.md +126 -0
  65. package/agent/docs-md/confidence-note.md +87 -0
  66. package/agent/docs-md/configurable-dashboard.md +76 -0
  67. package/agent/docs-md/confirmation-dialog.md +93 -0
  68. package/agent/docs-md/connected-tools.md +80 -0
  69. package/agent/docs-md/connector-picker.md +127 -0
  70. package/agent/docs-md/consistency.md +53 -0
  71. package/agent/docs-md/content-density.md +61 -0
  72. package/agent/docs-md/conversation-history.md +78 -0
  73. package/agent/docs-md/create-record.md +80 -0
  74. package/agent/docs-md/csv-import.md +88 -0
  75. package/agent/docs-md/dashboard-items.md +78 -0
  76. package/agent/docs-md/data-export.md +61 -0
  77. package/agent/docs-md/data-grid.md +137 -0
  78. package/agent/docs-md/data-vis-colours.md +76 -0
  79. package/agent/docs-md/data-visualisation.md +76 -0
  80. package/agent/docs-md/date-picker.md +178 -0
  81. package/agent/docs-md/delete-record.md +75 -0
  82. package/agent/docs-md/dense-table.md +495 -0
  83. package/agent/docs-md/design-tokens.md +53 -0
  84. package/agent/docs-md/disabled-read-only.md +75 -0
  85. package/agent/docs-md/drag-and-drop.md +74 -0
  86. package/agent/docs-md/dropdown-menu.md +137 -0
  87. package/agent/docs-md/edit-record.md +73 -0
  88. package/agent/docs-md/empty-loading-error.md +78 -0
  89. package/agent/docs-md/empty-state.md +128 -0
  90. package/agent/docs-md/error-messages.md +75 -0
  91. package/agent/docs-md/error-state.md +123 -0
  92. package/agent/docs-md/export-menu.md +86 -0
  93. package/agent/docs-md/feedback-prompt.md +88 -0
  94. package/agent/docs-md/field-mapping.md +78 -0
  95. package/agent/docs-md/file-drop.md +110 -0
  96. package/agent/docs-md/filter-bar.md +138 -0
  97. package/agent/docs-md/filter-panel.md +103 -0
  98. package/agent/docs-md/filtering-search.md +79 -0
  99. package/agent/docs-md/follow-ups-starters.md +79 -0
  100. package/agent/docs-md/form-validation.md +95 -0
  101. package/agent/docs-md/generated-columns.md +65 -0
  102. package/agent/docs-md/generated-drafts-files.md +71 -0
  103. package/agent/docs-md/grouped-list.md +89 -0
  104. package/agent/docs-md/guide-cue.md +170 -0
  105. package/agent/docs-md/help-panel.md +103 -0
  106. package/agent/docs-md/help-system.md +74 -0
  107. package/agent/docs-md/iconography.md +49 -0
  108. package/agent/docs-md/inbox.md +108 -0
  109. package/agent/docs-md/inline-editing-validation.md +80 -0
  110. package/agent/docs-md/inline-input-in-answer.md +70 -0
  111. package/agent/docs-md/items-palette.md +116 -0
  112. package/agent/docs-md/keyboard-model.md +47 -0
  113. package/agent/docs-md/keyboard-navigation.md +68 -0
  114. package/agent/docs-md/keyboard-shortcut-hint.md +87 -0
  115. package/agent/docs-md/keyboard-shortcut-sheet.md +90 -0
  116. package/agent/docs-md/labelling-ai-output.md +66 -0
  117. package/agent/docs-md/layout.md +47 -0
  118. package/agent/docs-md/list-table.md +254 -0
  119. package/agent/docs-md/loading-skeleton.md +91 -0
  120. package/agent/docs-md/managing-agents.md +71 -0
  121. package/agent/docs-md/message-composer.md +105 -0
  122. package/agent/docs-md/migrating.md +49 -0
  123. package/agent/docs-md/motion.md +93 -0
  124. package/agent/docs-md/navigation.md +76 -0
  125. package/agent/docs-md/notification-channels.md +65 -0
  126. package/agent/docs-md/notifications-decision.md +100 -0
  127. package/agent/docs-md/onboarding-step.md +114 -0
  128. package/agent/docs-md/order-book.md +108 -0
  129. package/agent/docs-md/page-header.md +96 -0
  130. package/agent/docs-md/pagination.md +88 -0
  131. package/agent/docs-md/pane-layout.md +105 -0
  132. package/agent/docs-md/period-comparison.md +66 -0
  133. package/agent/docs-md/plan-card.md +120 -0
  134. package/agent/docs-md/popover-rules.md +88 -0
  135. package/agent/docs-md/prompt-editor.md +90 -0
  136. package/agent/docs-md/propose-apply.md +65 -0
  137. package/agent/docs-md/provider-logo.md +97 -0
  138. package/agent/docs-md/radio.md +83 -0
  139. package/agent/docs-md/ranked-list.md +102 -0
  140. package/agent/docs-md/reasoning-progress.md +72 -0
  141. package/agent/docs-md/record-detail.md +71 -0
  142. package/agent/docs-md/regenerating-answers.md +70 -0
  143. package/agent/docs-md/run-status-bar.md +94 -0
  144. package/agent/docs-md/saved-tasks-schedules.md +63 -0
  145. package/agent/docs-md/saved-views.md +149 -0
  146. package/agent/docs-md/saving-views.md +68 -0
  147. package/agent/docs-md/scheduler.md +258 -0
  148. package/agent/docs-md/secondary-panels.md +72 -0
  149. package/agent/docs-md/segmented-control.md +88 -0
  150. package/agent/docs-md/selection-bulk-actions.md +70 -0
  151. package/agent/docs-md/separator.md +75 -0
  152. package/agent/docs-md/setup-checklist.md +92 -0
  153. package/agent/docs-md/shape-elevation.md +55 -0
  154. package/agent/docs-md/sheet-grid.md +211 -0
  155. package/agent/docs-md/side-panel.md +134 -0
  156. package/agent/docs-md/sign-in.md +114 -0
  157. package/agent/docs-md/spacing.md +55 -0
  158. package/agent/docs-md/split-button.md +100 -0
  159. package/agent/docs-md/stat-compare.md +82 -0
  160. package/agent/docs-md/static-dashboard.md +75 -0
  161. package/agent/docs-md/status-badge.md +140 -0
  162. package/agent/docs-md/step-card.md +75 -0
  163. package/agent/docs-md/stepper.md +92 -0
  164. package/agent/docs-md/suggested-changes.md +91 -0
  165. package/agent/docs-md/switch.md +78 -0
  166. package/agent/docs-md/table-settings.md +81 -0
  167. package/agent/docs-md/tabs.md +85 -0
  168. package/agent/docs-md/tag-input.md +108 -0
  169. package/agent/docs-md/tag.md +79 -0
  170. package/agent/docs-md/task-builder.md +87 -0
  171. package/agent/docs-md/team-instructions.md +72 -0
  172. package/agent/docs-md/templates-gallery.md +85 -0
  173. package/agent/docs-md/text-input.md +160 -0
  174. package/agent/docs-md/thinking-steps.md +89 -0
  175. package/agent/docs-md/timeline.md +189 -0
  176. package/agent/docs-md/timestamp.md +84 -0
  177. package/agent/docs-md/timestamps.md +70 -0
  178. package/agent/docs-md/toast.md +117 -0
  179. package/agent/docs-md/tooltip.md +90 -0
  180. package/agent/docs-md/typography.md +62 -0
  181. package/agent/docs-md/undo-destructive.md +66 -0
  182. package/agent/docs-md/unsaved-changes.md +71 -0
  183. package/agent/docs-md/usage-meter.md +80 -0
  184. package/agent/docs-md/user-feedback.md +71 -0
  185. package/agent/docs-md/voice-and-tone.md +60 -0
  186. package/agent/docs-md/why-choice.md +93 -0
  187. package/agent/docs-md/workspace-onboarding.md +74 -0
  188. package/agent/docs-md/writing.md +48 -0
  189. package/agent/llms-full.txt +18369 -0
  190. package/agent/llms.txt +198 -0
  191. package/agent/skills/twoux-audit/SKILL.md +49 -0
  192. package/agent/skills/twoux-build/SKILL.md +39 -0
  193. package/agent/skills/twoux-design/SKILL.md +54 -0
  194. package/agent/skills/twoux-write/SKILL.md +32 -0
  195. package/bin/check.mjs +123 -0
  196. package/bin/docs.mjs +24 -0
  197. package/bin/guard.mjs +27 -0
  198. package/bin/init.mjs +113 -0
  199. package/bin/knowledge.mjs +87 -0
  200. package/bin/mcp.mjs +116 -0
  201. package/bin/theme.mjs +168 -0
  202. package/bin/twoux.mjs +25 -0
  203. package/package.json +80 -3
  204. package/registry/jobs.json +1028 -0
  205. package/registry/palette.json +770 -0
  206. package/registry/rules.json +913 -0
  207. package/src/components/agent-review.tsx +364 -0
  208. package/src/components/ai-basics.tsx +212 -0
  209. package/src/components/ai-work.tsx +304 -0
  210. package/src/components/ai.tsx +227 -0
  211. package/src/components/alert-dialog.tsx +203 -0
  212. package/src/components/alert.tsx +86 -0
  213. package/src/components/avatar.tsx +114 -0
  214. package/src/components/badge.tsx +59 -0
  215. package/src/components/board.tsx +397 -0
  216. package/src/components/breadcrumb.tsx +114 -0
  217. package/src/components/button-group.tsx +90 -0
  218. package/src/components/button.tsx +109 -0
  219. package/src/components/calendar.tsx +247 -0
  220. package/src/components/card.tsx +102 -0
  221. package/src/components/chart-map.tsx +336 -0
  222. package/src/components/charts-more.tsx +1331 -0
  223. package/src/components/charts-radial.tsx +328 -0
  224. package/src/components/charts.tsx +390 -0
  225. package/src/components/checkbox.tsx +40 -0
  226. package/src/components/code.tsx +139 -0
  227. package/src/components/collapsible.tsx +37 -0
  228. package/src/components/combobox.tsx +339 -0
  229. package/src/components/command-extras.tsx +11 -0
  230. package/src/components/command.tsx +196 -0
  231. package/src/components/data-grid.tsx +213 -0
  232. package/src/components/data-table.tsx +334 -0
  233. package/src/components/data-views.tsx +740 -0
  234. package/src/components/date-picker.tsx +686 -0
  235. package/src/components/dense-table.tsx +207 -0
  236. package/src/components/dialog.tsx +162 -0
  237. package/src/components/dropdown-menu.tsx +264 -0
  238. package/src/components/empty.tsx +114 -0
  239. package/src/components/feedback-prompt.tsx +80 -0
  240. package/src/components/field.tsx +254 -0
  241. package/src/components/fields.tsx +112 -0
  242. package/src/components/flow.tsx +840 -0
  243. package/src/components/guide-cue.tsx +273 -0
  244. package/src/components/inbox.tsx +113 -0
  245. package/src/components/input-group.tsx +176 -0
  246. package/src/components/input.tsx +26 -0
  247. package/src/components/kbd.tsx +34 -0
  248. package/src/components/label.tsx +27 -0
  249. package/src/components/labels.tsx +176 -0
  250. package/src/components/motion.tsx +136 -0
  251. package/src/components/onboarding.tsx +454 -0
  252. package/src/components/order-book.tsx +185 -0
  253. package/src/components/pane-layout.tsx +417 -0
  254. package/src/components/popover.tsx +96 -0
  255. package/src/components/provider-logos.data.ts +49 -0
  256. package/src/components/radio-group.tsx +52 -0
  257. package/src/components/scheduler.tsx +997 -0
  258. package/src/components/segmented-control.tsx +28 -0
  259. package/src/components/select.tsx +195 -0
  260. package/src/components/separator.tsx +31 -0
  261. package/src/components/sheet.tsx +148 -0
  262. package/src/components/shell.tsx +426 -0
  263. package/src/components/shortcut-sheet.tsx +83 -0
  264. package/src/components/skeleton.tsx +18 -0
  265. package/src/components/sonner.tsx +107 -0
  266. package/src/components/spinner.tsx +13 -0
  267. package/src/components/split-button.tsx +58 -0
  268. package/src/components/stepper.tsx +78 -0
  269. package/src/components/switch.tsx +43 -0
  270. package/src/components/table.tsx +126 -0
  271. package/src/components/tabs.tsx +118 -0
  272. package/src/components/tag-input.tsx +253 -0
  273. package/src/components/textarea.tsx +21 -0
  274. package/src/components/theme.tsx +80 -0
  275. package/src/components/timeline.tsx +408 -0
  276. package/src/components/toggle-group.tsx +87 -0
  277. package/src/components/toggle.tsx +54 -0
  278. package/src/components/tooltip.tsx +65 -0
  279. package/src/components/workspace-actions.tsx +294 -0
  280. package/src/components/workspace-import.tsx +465 -0
  281. package/src/components/workspace.tsx +1884 -0
  282. package/src/index.ts +80 -0
  283. package/src/lib/active-indicator.tsx +53 -0
  284. package/src/lib/chart-kit.tsx +72 -0
  285. package/src/lib/cn.ts +6 -0
  286. package/src/lib/shortcuts.tsx +61 -0
  287. package/src/lib/workspace.tsx +10 -0
  288. package/src/shadcn/alert-dialog.tsx +196 -0
  289. package/src/shadcn/alert.tsx +65 -0
  290. package/src/shadcn/avatar.tsx +108 -0
  291. package/src/shadcn/badge.tsx +47 -0
  292. package/src/shadcn/breadcrumb.tsx +108 -0
  293. package/src/shadcn/button-group.tsx +83 -0
  294. package/src/shadcn/button.tsx +63 -0
  295. package/src/shadcn/calendar.tsx +220 -0
  296. package/src/shadcn/card.tsx +91 -0
  297. package/src/shadcn/checkbox.tsx +31 -0
  298. package/src/shadcn/collapsible.tsx +33 -0
  299. package/src/shadcn/command.tsx +184 -0
  300. package/src/shadcn/dialog.tsx +158 -0
  301. package/src/shadcn/dropdown-menu.tsx +256 -0
  302. package/src/shadcn/empty.tsx +103 -0
  303. package/src/shadcn/field.tsx +248 -0
  304. package/src/shadcn/input-group.tsx +170 -0
  305. package/src/shadcn/input.tsx +20 -0
  306. package/src/shadcn/kbd.tsx +28 -0
  307. package/src/shadcn/label.tsx +23 -0
  308. package/src/shadcn/popover.tsx +88 -0
  309. package/src/shadcn/radio-group.tsx +44 -0
  310. package/src/shadcn/select.tsx +189 -0
  311. package/src/shadcn/separator.tsx +27 -0
  312. package/src/shadcn/sheet.tsx +142 -0
  313. package/src/shadcn/skeleton.tsx +13 -0
  314. package/src/shadcn/sonner.tsx +40 -0
  315. package/src/shadcn/switch.tsx +34 -0
  316. package/src/shadcn/table.tsx +115 -0
  317. package/src/shadcn/tabs.tsx +90 -0
  318. package/src/shadcn/textarea.tsx +17 -0
  319. package/src/shadcn/toggle-group.tsx +83 -0
  320. package/src/shadcn/toggle.tsx +46 -0
  321. package/src/shadcn/tooltip.tsx +56 -0
  322. package/src/styles/palette.css +127 -0
  323. package/src/styles/theme.css +35 -0
  324. package/src/styles/twoux.css +236 -0
  325. package/theme.json +14 -0
@@ -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
+ }