@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,273 @@
1
+ /**
2
+ * GuideCue: a short guide of one to three popovers, each pointing at a real control. Pulled, never pushed.
3
+ * TwoUX composition on the @twoux/ui Popover (Radix Popover.Anchor with the target element as its virtual ref).
4
+ * - Opens only when the person asks: a SetupChecklist Start, a Help menu entry, a HelpPanel link or a FeatureLabel
5
+ * popover's "Show me". Never on page load, on a timer or on hover. This is the only in-product guide TwoUX allows.
6
+ * - One to three steps, counted as text ("Step 2 of 3"). A step whose target is missing or hidden is skipped, so the
7
+ * cue never points at nothing; if no step has a target, the guide does not open.
8
+ * - The target gets the focus ring (outline in the focus colour) drawn above it. No dimmed overlay, no spotlight, no
9
+ * beacon: the page stays usable, and nothing pulses (WCAG 2.3.3).
10
+ * - Not modal: role="dialog" aria-modal="false", named by its heading, described by its body. Focus moves to the
11
+ * heading on open, step changes are announced, Esc or Close ends it and focus returns to what started it.
12
+ * - Back (outline), Next or Done (the single primary inside the cue) and Close (ghost icon button).
13
+ */
14
+ "use client"
15
+
16
+ import { X } from "lucide-react"
17
+ import * as React from "react"
18
+ import { createPortal } from "react-dom"
19
+ import { cn } from "../lib/cn"
20
+ import { Button } from "./button"
21
+ import { Kbd } from "./kbd"
22
+ import { Popover, PopoverAnchor, PopoverContent } from "./popover"
23
+ import { Tooltip, TooltipContent, TooltipTrigger } from "./tooltip"
24
+
25
+ /* ---------------------------------------------------------------- types */
26
+
27
+ /** Where a step points: a ref, a CSS selector (prefer a data attribute, e.g. '[data-guide="filters"]') or a function. */
28
+ export type GuideTarget = React.RefObject<HTMLElement | null> | string | (() => HTMLElement | null | undefined)
29
+
30
+ export type GuideStep = {
31
+ target: GuideTarget
32
+ /** Names what the person does here, sentence case: "Filter the list" */
33
+ title: string
34
+ /** One or two sentences: what the control does for them. */
35
+ body: React.ReactNode
36
+ /** Preferred side of the target. The cue flips when there is no room. */
37
+ side?: "top" | "right" | "bottom" | "left"
38
+ align?: "start" | "center" | "end"
39
+ }
40
+
41
+ export type GuideCueProps = {
42
+ /** One to three steps. More are dropped (with a warning in development): split the guide or write a help article. */
43
+ steps: GuideStep[]
44
+ open: boolean
45
+ onOpenChange: (open: boolean) => void
46
+ /** Called when the person presses Done on the last step (not on Close or Esc). */
47
+ onFinish?: () => void
48
+ /** Accessible name prefix for the step count and the live announcement, e.g. "Saved views guide". */
49
+ label?: string
50
+ /**
51
+ * Docs and design previews only: the cue is shown open without taking focus or scrolling, because nobody started it.
52
+ * In a product a guide opens only from something the person pressed; never set this there.
53
+ */
54
+ preview?: boolean
55
+ className?: string
56
+ }
57
+
58
+ export const GUIDE_MAX_STEPS = 3
59
+
60
+ /* ---------------------------------------------------------------- helpers */
61
+
62
+ function resolve(target: GuideTarget): HTMLElement | null {
63
+ if (typeof document === "undefined") return null
64
+ if (typeof target === "string") return document.querySelector<HTMLElement>(target)
65
+ if (typeof target === "function") return target() ?? null
66
+ return target.current
67
+ }
68
+
69
+ /** Present, laid out and not hidden from people: a cue never points at nothing. */
70
+ export function isGuideTargetAvailable(el: HTMLElement | null): el is HTMLElement {
71
+ if (!el || !el.isConnected) return false
72
+ if (el.getClientRects().length === 0) return false
73
+ if (el.closest("[hidden],[inert],[aria-hidden='true']")) return false
74
+ const style = window.getComputedStyle(el)
75
+ return style.visibility !== "hidden"
76
+ }
77
+
78
+ const reducedMotion = () => typeof window !== "undefined" && !!window.matchMedia?.("(prefers-reduced-motion: reduce)").matches
79
+
80
+ /** The focus ring drawn over the target, following it on scroll and resize. Never takes pointer events. */
81
+ function TargetRing({ el }: { el: HTMLElement }) {
82
+ const [rect, setRect] = React.useState<DOMRect | null>(null)
83
+ React.useLayoutEffect(() => {
84
+ let frame = 0
85
+ const update = () => {
86
+ cancelAnimationFrame(frame)
87
+ frame = requestAnimationFrame(() => setRect(el.getBoundingClientRect()))
88
+ }
89
+ setRect(el.getBoundingClientRect())
90
+ const ro = new ResizeObserver(update)
91
+ ro.observe(el)
92
+ window.addEventListener("scroll", update, true)
93
+ window.addEventListener("resize", update)
94
+ return () => {
95
+ cancelAnimationFrame(frame)
96
+ ro.disconnect()
97
+ window.removeEventListener("scroll", update, true)
98
+ window.removeEventListener("resize", update)
99
+ }
100
+ }, [el])
101
+ if (!rect) return null
102
+ const radius = window.getComputedStyle(el).borderRadius
103
+ return createPortal(
104
+ <span
105
+ aria-hidden="true"
106
+ data-slot="guide-cue-ring"
107
+ className="pointer-events-none fixed top-(--gy) left-(--gx) z-40 h-(--gh) w-(--gw) rounded-(--gr) outline-2 outline-offset-2 outline-ring"
108
+ style={{ "--gx": `${rect.left}px`, "--gy": `${rect.top}px`, "--gw": `${rect.width}px`, "--gh": `${rect.height}px`, "--gr": radius || "0px" } as React.CSSProperties}
109
+ />,
110
+ document.body,
111
+ )
112
+ }
113
+
114
+ /* ---------------------------------------------------------------- component */
115
+
116
+ export function GuideCue({ steps, open, onOpenChange, onFinish, label = "Guide", preview = false, className }: GuideCueProps) {
117
+ const id = React.useId()
118
+ const list = steps.slice(0, GUIDE_MAX_STEPS)
119
+
120
+ // Resolved once per opening: the steps whose target is there, with their elements.
121
+ const [live, setLive] = React.useState<{ step: GuideStep; el: HTMLElement }[]>([])
122
+ const [index, setIndex] = React.useState(0)
123
+ const [announce, setAnnounce] = React.useState("")
124
+ const returnTo = React.useRef<HTMLElement | null>(null)
125
+ const headingRef = React.useRef<HTMLHeadingElement>(null)
126
+ const nextRef = React.useRef<HTMLButtonElement>(null)
127
+ const moved = React.useRef(false)
128
+
129
+ const stepsKey = list.map((s) => s.title).join("|")
130
+ React.useLayoutEffect(() => {
131
+ if (!open) {
132
+ setLive([])
133
+ // Back to what started the guide, unless the person has already moved focus somewhere else on the page.
134
+ const back = returnTo.current
135
+ returnTo.current = null
136
+ if (back?.isConnected) {
137
+ requestAnimationFrame(() => {
138
+ const now = document.activeElement
139
+ if (!now || now === document.body) back.focus()
140
+ })
141
+ }
142
+ return
143
+ }
144
+ // Remember what started the guide before anything moves focus. A preview was started by nobody.
145
+ returnTo.current = !preview && document.activeElement instanceof HTMLElement ? document.activeElement : null
146
+ const found = list.flatMap((step) => { const el = resolve(step.target); return isGuideTargetAvailable(el) ? [{ step, el }] : [] })
147
+ if (found.length === 0) {
148
+ console.warn("GuideCue: no step has a visible target, so the guide did not open.")
149
+ onOpenChange(false)
150
+ return
151
+ }
152
+ setLive(found)
153
+ setIndex(0)
154
+ setAnnounce("")
155
+ moved.current = false
156
+ // eslint-disable-next-line react-hooks/exhaustive-deps
157
+ }, [open, stepsKey])
158
+
159
+ const current = live[index]
160
+ const total = live.length
161
+ const last = index === total - 1
162
+
163
+ // Keep the target in view; respect reduced motion.
164
+ React.useEffect(() => {
165
+ if (!open || !current) return
166
+ // A preview opens in place: no scrolling until the person moves through the steps.
167
+ if (!preview || moved.current) current.el.scrollIntoView({ block: "nearest", inline: "nearest", behavior: reducedMotion() ? "auto" : "smooth" })
168
+ // Announce step changes only; on open, focus on the heading already reads the first step.
169
+ if (moved.current) setAnnounce(`${label}, step ${index + 1} of ${total}: ${current.step.title}`)
170
+ // On a step change, keep focus inside the cue: if the pressed button went away (Back on step 1), go to Next.
171
+ if (moved.current) {
172
+ requestAnimationFrame(() => {
173
+ const content = headingRef.current?.closest("[data-slot='guide-cue']")
174
+ if (!content?.contains(document.activeElement)) nextRef.current?.focus()
175
+ })
176
+ }
177
+ }, [open, current, index, total, label])
178
+
179
+ // A guide given more than three steps keeps the first three: split it, or write a help article instead.
180
+ React.useEffect(() => {
181
+ if (steps.length > GUIDE_MAX_STEPS) console.warn(`GuideCue: ${steps.length} steps given; only the first ${GUIDE_MAX_STEPS} are shown.`)
182
+ }, [steps.length])
183
+
184
+ const close = React.useCallback(() => onOpenChange(false), [onOpenChange])
185
+ const go = (to: number) => { moved.current = true; setIndex(to) }
186
+
187
+ if (!open || !current) return null
188
+ const { step, el } = current
189
+ const virtualRef = { current: el }
190
+
191
+ return (
192
+ <>
193
+ <TargetRing el={el} />
194
+ <Popover open onOpenChange={(o) => { if (!o) close() }}>
195
+ <PopoverAnchor virtualRef={virtualRef} />
196
+ <PopoverContent
197
+ data-slot="guide-cue"
198
+ side={step.side ?? "bottom"}
199
+ align={step.align ?? "start"}
200
+ sideOffset={8}
201
+ updatePositionStrategy="always"
202
+ aria-modal="false"
203
+ aria-labelledby={`${id}-title`}
204
+ aria-describedby={`${id}-body`}
205
+ // Focus goes to the heading, so the title is read first; it returns to what started the guide.
206
+ onOpenAutoFocus={(e) => { e.preventDefault(); if (!preview) headingRef.current?.focus() }}
207
+ onCloseAutoFocus={(e) => e.preventDefault()}
208
+ // Not modal: people can try the highlighted control without the guide closing.
209
+ onInteractOutside={(e) => e.preventDefault()}
210
+ className={cn("grid w-72 max-w-(--radix-popover-content-available-width) gap-2 text-base", className)}
211
+ >
212
+ <div className="flex items-start gap-2">
213
+ <h2 ref={headingRef} id={`${id}-title`} tabIndex={-1} className="min-w-0 flex-1 pt-0.5 text-base font-semibold text-ink outline-none">
214
+ {step.title}
215
+ </h2>
216
+ <Tooltip>
217
+ <TooltipTrigger asChild>
218
+ <Button variant="ghost" size="icon-xs" aria-label="Close guide" onClick={close} className="-mt-0.5 -mr-1 text-ink-3">
219
+ <X aria-hidden="true" />
220
+ </Button>
221
+ </TooltipTrigger>
222
+ <TooltipContent>Close guide <Kbd>Esc</Kbd></TooltipContent>
223
+ </Tooltip>
224
+ </div>
225
+ <div id={`${id}-body`} className="text-ink-2">{step.body}</div>
226
+ <div className="flex flex-wrap items-center gap-2 pt-1">
227
+ {total > 1 && <span className="tabular flex-1 text-sm text-ink-2">Step {index + 1} of {total}</span>}
228
+ {total === 1 && <span className="flex-1" />}
229
+ {index > 0 && <Button variant="outline" size="sm" onClick={() => go(index - 1)}>Back</Button>}
230
+ <Button
231
+ ref={nextRef}
232
+ size="sm"
233
+ onClick={() => { if (last) { onFinish?.(); close() } else go(index + 1) }}
234
+ >
235
+ {last ? "Done" : "Next"}
236
+ </Button>
237
+ </div>
238
+ <span className="sr-only" aria-live="polite" aria-atomic="true">{announce}</span>
239
+ </PopoverContent>
240
+ </Popover>
241
+ </>
242
+ )
243
+ }
244
+
245
+ /* ---------------------------------------------------------------- hook */
246
+
247
+ /**
248
+ * State for one guide. `start` is what a "Show me" or Start button calls; spread `cueProps` on GuideCue.
249
+ * `finished` turns true once Done is pressed on the last step, so the starting point can say so.
250
+ */
251
+ export function useGuide(steps: GuideStep[], options: { onFinish?: () => void; label?: string; /** Docs and design previews only: start open, without taking focus. See GuideCue `preview`. */ preview?: boolean } = {}) {
252
+ const [open, setOpen] = React.useState(!!options.preview)
253
+ // Quiet only for the first, unrequested opening; once the person starts or closes it, it behaves normally.
254
+ const [quiet, setQuiet] = React.useState(!!options.preview)
255
+ const [finished, setFinished] = React.useState(false)
256
+ const start = React.useCallback(() => { setQuiet(false); setOpen(true) }, [])
257
+ // A guide given more than three steps keeps the first three: split it, or write a help article instead.
258
+ React.useEffect(() => {
259
+ if (steps.length > GUIDE_MAX_STEPS) console.warn(`GuideCue: ${steps.length} steps given; only the first ${GUIDE_MAX_STEPS} are shown.`)
260
+ }, [steps.length])
261
+
262
+ const close = React.useCallback(() => { setQuiet(false); setOpen(false) }, [])
263
+ const { onFinish, label } = options
264
+ const cueProps: GuideCueProps = {
265
+ steps,
266
+ open,
267
+ onOpenChange: (o) => { if (!o) setQuiet(false); setOpen(o) },
268
+ preview: quiet,
269
+ onFinish: () => { setFinished(true); onFinish?.() },
270
+ label,
271
+ }
272
+ return { open, finished, start, close, cueProps }
273
+ }
@@ -0,0 +1,113 @@
1
+ /**
2
+ * TwoUX composition: the one place for things that need the person. List and detail side by side,
3
+ * Inbox / Snoozed / Done tabs with counts, items that block an agent first ("Needs you"), J/K to move,
4
+ * E to mark done, ⇧H to snooze. Acting never leaves the Inbox; the next item opens after Done.
5
+ */
6
+ import { Check, Clock } from "lucide-react"
7
+ import * as React from "react"
8
+ import { cn } from "../lib/cn"
9
+ import { Badge } from "./badge"
10
+ import { Button } from "./button"
11
+ import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuTrigger } from "./dropdown-menu"
12
+ import { Tabs, TabsList, TabsTrigger } from "./tabs"
13
+
14
+ export type InboxItem = { id: string; title: string; source: string; time: string; needsYou?: boolean; state: "inbox" | "snoozed" | "done"; snoozedUntil?: string }
15
+ export const SNOOZE_TIMES = ["In an hour", "Tomorrow, 9:00", "Next week", "In a month"] as const
16
+
17
+ export function Inbox({
18
+ items,
19
+ onDone,
20
+ onSnooze,
21
+ renderDetail,
22
+ className,
23
+ }: {
24
+ items: InboxItem[]
25
+ onDone: (id: string) => void
26
+ onSnooze: (id: string, until: string) => void
27
+ renderDetail: (item: InboxItem) => React.ReactNode
28
+ className?: string
29
+ }) {
30
+ const [tab, setTab] = React.useState<InboxItem["state"]>("inbox")
31
+ const list = items.filter((i) => i.state === tab).sort((a, b) => Number(!!b.needsYou) - Number(!!a.needsYou))
32
+ const [activeId, setActiveId] = React.useState<string | null>(list[0]?.id ?? null)
33
+ const [snoozeOpen, setSnoozeOpen] = React.useState(false)
34
+ const active = list.find((i) => i.id === activeId) ?? list[0] ?? null
35
+ const idx = active ? list.indexOf(active) : -1
36
+ const count = (s: InboxItem["state"]) => items.filter((i) => i.state === s).length
37
+ const listRef = React.useRef<HTMLUListElement>(null)
38
+
39
+ const next = () => list[idx + 1]?.id ?? list[idx - 1]?.id ?? null
40
+ const done = () => { if (!active) return; const n = next(); onDone(active.id); setActiveId(n) }
41
+ const snooze = (until: string) => { if (!active) return; const n = next(); onSnooze(active.id, until); setActiveId(n); setSnoozeOpen(false) }
42
+
43
+ const onKey = (e: React.KeyboardEvent) => {
44
+ if (e.metaKey || e.ctrlKey || e.altKey || (e.target as HTMLElement).closest("input, textarea")) return
45
+ const k = e.key.toLowerCase()
46
+ if (k === "j" || e.key === "ArrowDown") { e.preventDefault(); setActiveId(list[Math.min(list.length - 1, idx + 1)]?.id ?? null) }
47
+ else if (k === "k" || e.key === "ArrowUp") { e.preventDefault(); setActiveId(list[Math.max(0, idx - 1)]?.id ?? null) }
48
+ else if (k === "e" && tab !== "done") { e.preventDefault(); done() }
49
+ else if (k === "h" && e.shiftKey && tab === "inbox") { e.preventDefault(); setSnoozeOpen(true) }
50
+ }
51
+
52
+ return (
53
+ <div data-slot="inbox" className={cn("grid min-h-0 grid-cols-[minmax(0,320px)_minmax(0,1fr)] overflow-hidden rounded-sm border border-line bg-surface max-md:grid-cols-1", className)}>
54
+ <div className="flex min-h-0 flex-col border-r border-line max-md:border-r-0 max-md:border-b">
55
+ <Tabs value={tab} onValueChange={(v) => { setTab(v as InboxItem["state"]); setActiveId(null) }} className="px-3 pt-2">
56
+ <TabsList aria-label="Inbox views">
57
+ <TabsTrigger value="inbox" count={count("inbox")}>Inbox</TabsTrigger>
58
+ <TabsTrigger value="snoozed" count={count("snoozed")}>Snoozed</TabsTrigger>
59
+ <TabsTrigger value="done" count={count("done")}>Done</TabsTrigger>
60
+ </TabsList>
61
+ </Tabs>
62
+ {list.length === 0 ? (
63
+ <p className="px-4 py-8 text-center text-base text-ink-2">{tab === "inbox" ? "You're all caught up." : tab === "snoozed" ? "Nothing snoozed." : "Nothing done yet."}</p>
64
+ ) : (
65
+ <ul ref={listRef} role="listbox" aria-label="Items" aria-activedescendant={active ? `inbox-${active.id}` : undefined} tabIndex={0} onKeyDown={onKey} className="min-h-0 flex-1 overflow-y-auto py-1 outline-none focus-visible:ring-2 focus-visible:ring-primary/40 focus-visible:ring-inset">
66
+ {list.map((it) => (
67
+ <li
68
+ key={it.id}
69
+ id={`inbox-${it.id}`}
70
+ role="option"
71
+ aria-selected={active?.id === it.id}
72
+ onClick={() => setActiveId(it.id)}
73
+ className={cn("mx-1 grid cursor-default gap-0.5 rounded-xs px-2 py-2", active?.id === it.id ? "bg-selected" : "hover:bg-hover")}
74
+ >
75
+ <span className="flex items-center gap-2">
76
+ <span className="min-w-0 flex-1 truncate text-base font-medium text-ink">{it.title}</span>
77
+ <span className="tabular shrink-0 text-sm text-ink-3">{it.state === "snoozed" && it.snoozedUntil ? it.snoozedUntil : it.time}</span>
78
+ </span>
79
+ <span className="flex items-center gap-2">
80
+ <span className="min-w-0 flex-1 truncate text-sm text-ink-2">{it.source}</span>
81
+ {it.needsYou && it.state === "inbox" && <Badge variant="warning" dot>Needs you</Badge>}
82
+ </span>
83
+ </li>
84
+ ))}
85
+ </ul>
86
+ )}
87
+ <p className="border-t border-line px-3 py-2 text-xs text-ink-3">J K to move · E done · ⇧H snooze</p>
88
+ </div>
89
+ <div className="flex min-h-0 flex-col">
90
+ {active ? (
91
+ <>
92
+ <div className="flex items-start gap-2 border-b border-line px-4 py-2">
93
+ <h2 className="min-w-0 flex-1 pt-1 text-md font-semibold text-ink">{active.title}</h2>
94
+ {tab === "inbox" && (
95
+ <DropdownMenu open={snoozeOpen} onOpenChange={setSnoozeOpen}>
96
+ <DropdownMenuTrigger asChild><Button variant="ghost" size="sm" shortcut="⇧H"><Clock />Snooze</Button></DropdownMenuTrigger>
97
+ <DropdownMenuContent align="end">
98
+ <DropdownMenuLabel>Snooze until</DropdownMenuLabel>
99
+ {SNOOZE_TIMES.map((t) => <DropdownMenuItem key={t} onSelect={() => snooze(t)}>{t}</DropdownMenuItem>)}
100
+ </DropdownMenuContent>
101
+ </DropdownMenu>
102
+ )}
103
+ {tab !== "done" && <Button variant="outline" size="sm" shortcut="E" onClick={done}><Check />Done</Button>}
104
+ </div>
105
+ <div className="min-h-0 flex-1 overflow-y-auto p-4">{renderDetail(active)}</div>
106
+ </>
107
+ ) : (
108
+ <div className="grid flex-1 place-items-center p-8 text-base text-ink-3">Select an item to see it here.</div>
109
+ )}
110
+ </div>
111
+ </div>
112
+ )
113
+ }
@@ -0,0 +1,176 @@
1
+ /**
2
+ * Generated from shadcn/ui new-york-v4 "input-group" by scripts/tailor-shadcn.mjs. Do not edit by hand.
3
+ * TwoUX changes:
4
+ * - Compact 32px
5
+ * - Light edge darkens on hover
6
+ */
7
+ "use client"
8
+
9
+ import * as React from "react"
10
+ import { cva, type VariantProps } from "class-variance-authority"
11
+ import { cn } from "../lib/cn"
12
+
13
+ import { Button } from "./button"
14
+ import { Input } from "./input"
15
+ import { Textarea } from "./textarea"
16
+
17
+ function InputGroup({ className, ...props }: React.ComponentProps<"div">) {
18
+ return (
19
+ <div
20
+ data-slot="input-group"
21
+ role="group"
22
+ className={cn(
23
+ "group/input-group relative flex w-full items-center rounded-sm border border-input bg-background shadow-xs hover:border-input-hover transition-[color,background-color,border-color,box-shadow] duration-(--duration-fast) ease-(--ease-out) outline-none",
24
+ "h-8 min-w-0 has-[>textarea]:h-auto",
25
+
26
+ // Variants based on alignment.
27
+ "has-[>[data-align=inline-start]]:[&>input]:pl-2",
28
+ "has-[>[data-align=inline-end]]:[&>input]:pr-2",
29
+ "has-[>[data-align=block-start]]:h-auto has-[>[data-align=block-start]]:flex-col has-[>[data-align=block-start]]:[&>input]:pb-3",
30
+ "has-[>[data-align=block-end]]:h-auto has-[>[data-align=block-end]]:flex-col has-[>[data-align=block-end]]:[&>input]:pt-3",
31
+
32
+ // Focus state.
33
+ "has-[[data-slot=input-group-control]:focus-visible]:border-ring has-[[data-slot=input-group-control]:focus-visible]:ring-[3px] has-[[data-slot=input-group-control]:focus-visible]:ring-ring/50",
34
+
35
+ // Error state.
36
+ "has-[[data-slot][aria-invalid=true]:not(:focus-visible)]:border-destructive-edge",
37
+
38
+ className
39
+ )}
40
+ {...props}
41
+ />
42
+ )
43
+ }
44
+
45
+ const inputGroupAddonVariants = cva(
46
+ "flex h-auto cursor-text items-center justify-center gap-2 py-1.5 text-base font-medium text-muted-foreground select-none group-data-[disabled=true]/input-group:opacity-50 [&>kbd]:rounded-xs [&>svg:not([class*='size-'])]:size-4",
47
+ {
48
+ variants: {
49
+ align: {
50
+ "inline-start":
51
+ "order-first pl-3 has-[>button]:ml-[-0.45rem] has-[>kbd]:ml-[-0.35rem]",
52
+ "inline-end":
53
+ "order-last pr-3 has-[>button]:mr-[-0.45rem] has-[>kbd]:mr-[-0.35rem]",
54
+ "block-start":
55
+ "order-first w-full justify-start px-3 pt-3 group-has-[>input]/input-group:pt-2.5 [.border-b]:pb-3",
56
+ "block-end":
57
+ "order-last w-full justify-start px-3 pb-3 group-has-[>input]/input-group:pb-2.5 [.border-t]:pt-3",
58
+ },
59
+ },
60
+ defaultVariants: {
61
+ align: "inline-start",
62
+ },
63
+ }
64
+ )
65
+
66
+ function InputGroupAddon({
67
+ className,
68
+ align = "inline-start",
69
+ ...props
70
+ }: React.ComponentProps<"div"> & VariantProps<typeof inputGroupAddonVariants>) {
71
+ return (
72
+ <div
73
+ role="group"
74
+ data-slot="input-group-addon"
75
+ data-align={align}
76
+ className={cn(inputGroupAddonVariants({ align }), className)}
77
+ onClick={(e) => {
78
+ if ((e.target as HTMLElement).closest("button")) {
79
+ return
80
+ }
81
+ e.currentTarget.parentElement?.querySelector("input")?.focus()
82
+ }}
83
+ {...props}
84
+ />
85
+ )
86
+ }
87
+
88
+ const inputGroupButtonVariants = cva(
89
+ "flex items-center gap-2 text-base shadow-none",
90
+ {
91
+ variants: {
92
+ size: {
93
+ xs: "h-6 gap-1 rounded-xs px-2 has-[>svg]:px-2 [&>svg:not([class*='size-'])]:size-3.5",
94
+ sm: "h-8 gap-1.5 rounded-sm px-2.5 has-[>svg]:px-2.5",
95
+ "icon-xs":
96
+ "size-6 rounded-xs p-0 has-[>svg]:p-0",
97
+ "icon-sm": "size-8 p-0 has-[>svg]:p-0",
98
+ },
99
+ },
100
+ defaultVariants: {
101
+ size: "xs",
102
+ },
103
+ }
104
+ )
105
+
106
+ function InputGroupButton({
107
+ className,
108
+ type = "button",
109
+ variant = "ghost",
110
+ size = "xs",
111
+ ...props
112
+ }: Omit<React.ComponentProps<typeof Button>, "size"> &
113
+ VariantProps<typeof inputGroupButtonVariants>) {
114
+ return (
115
+ <Button
116
+ type={type}
117
+ data-size={size}
118
+ variant={variant}
119
+ className={cn(inputGroupButtonVariants({ size }), className)}
120
+ {...props}
121
+ />
122
+ )
123
+ }
124
+
125
+ function InputGroupText({ className, ...props }: React.ComponentProps<"span">) {
126
+ return (
127
+ <span
128
+ className={cn(
129
+ "flex items-center gap-2 text-base text-muted-foreground [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4",
130
+ className
131
+ )}
132
+ {...props}
133
+ />
134
+ )
135
+ }
136
+
137
+ function InputGroupInput({
138
+ className,
139
+ ...props
140
+ }: React.ComponentProps<"input">) {
141
+ return (
142
+ <Input
143
+ data-slot="input-group-control"
144
+ className={cn(
145
+ "flex-1 rounded-none border-0 bg-transparent shadow-none focus-visible:ring-0",
146
+ className
147
+ )}
148
+ {...props}
149
+ />
150
+ )
151
+ }
152
+
153
+ function InputGroupTextarea({
154
+ className,
155
+ ...props
156
+ }: React.ComponentProps<"textarea">) {
157
+ return (
158
+ <Textarea
159
+ data-slot="input-group-control"
160
+ className={cn(
161
+ "flex-1 resize-none rounded-none border-0 bg-transparent py-3 shadow-none focus-visible:ring-0",
162
+ className
163
+ )}
164
+ {...props}
165
+ />
166
+ )
167
+ }
168
+
169
+ export {
170
+ InputGroup,
171
+ InputGroupAddon,
172
+ InputGroupButton,
173
+ InputGroupText,
174
+ InputGroupInput,
175
+ InputGroupTextarea,
176
+ }
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Generated from shadcn/ui new-york-v4 "input" by scripts/tailor-shadcn.mjs. Do not edit by hand.
3
+ * TwoUX changes:
4
+ * - Compact 32px, light edge that darkens on hover
5
+ * - One size on every screen
6
+ */
7
+ import * as React from "react"
8
+ import { cn } from "../lib/cn"
9
+
10
+ function Input({ className, type, ...props }: React.ComponentProps<"input">) {
11
+ return (
12
+ <input
13
+ type={type}
14
+ data-slot="input"
15
+ className={cn(
16
+ "h-8 w-full min-w-0 rounded-sm border border-input bg-background px-2.5 py-1 text-base shadow-xs hover:border-input-hover transition-[color,background-color,border-color,box-shadow] duration-(--duration-fast) ease-(--ease-out) outline-none selection:bg-primary selection:text-primary-foreground file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-base file:font-medium file:text-foreground placeholder:text-muted-foreground disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50",
17
+ "focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50",
18
+ "aria-invalid:not-focus-visible:border-destructive-edge",
19
+ className
20
+ )}
21
+ {...props}
22
+ />
23
+ )
24
+ }
25
+
26
+ export { Input }
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Generated from shadcn/ui new-york-v4 "kbd" by scripts/tailor-shadcn.mjs. Do not edit by hand.
3
+ * TwoUX changes:
4
+ * - Inside a primary or destructive button, the key sits on a darker tint so white text keeps 4.5:1
5
+ */
6
+ import { cn } from "../lib/cn"
7
+
8
+ function Kbd({ className, ...props }: React.ComponentProps<"kbd">) {
9
+ return (
10
+ <kbd
11
+ data-slot="kbd"
12
+ className={cn(
13
+ "pointer-events-none inline-flex h-5 w-fit min-w-5 items-center justify-center gap-1 rounded-xs bg-muted px-1 font-sans text-sm font-medium text-muted-foreground select-none",
14
+ "[&_svg:not([class*='size-'])]:size-3",
15
+ "[[data-slot=button][data-variant=default]_&]:bg-black/20 [[data-slot=button][data-variant=default]_&]:text-primary-foreground [[data-slot=button][data-variant=destructive]_&]:bg-black/20 [[data-slot=button][data-variant=destructive]_&]:text-white",
16
+ "[[data-slot=tooltip-content]_&]:bg-background/20 [[data-slot=tooltip-content]_&]:text-background",
17
+ className
18
+ )}
19
+ {...props}
20
+ />
21
+ )
22
+ }
23
+
24
+ function KbdGroup({ className, ...props }: React.ComponentProps<"div">) {
25
+ return (
26
+ <kbd
27
+ data-slot="kbd-group"
28
+ className={cn("inline-flex items-center gap-1", className)}
29
+ {...props}
30
+ />
31
+ )
32
+ }
33
+
34
+ export { Kbd, KbdGroup }
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Generated from shadcn/ui new-york-v4 "label" by scripts/tailor-shadcn.mjs. Do not edit by hand.
3
+ * TwoUX changes: shared changes only (imports, role tokens for light and dark, type scale).
4
+ */
5
+ "use client"
6
+
7
+ import * as React from "react"
8
+ import { cn } from "../lib/cn"
9
+ import { Label as LabelPrimitive } from "radix-ui"
10
+
11
+ function Label({
12
+ className,
13
+ ...props
14
+ }: React.ComponentProps<typeof LabelPrimitive.Root>) {
15
+ return (
16
+ <LabelPrimitive.Root
17
+ data-slot="label"
18
+ className={cn(
19
+ "flex items-center gap-2 text-base leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50",
20
+ className
21
+ )}
22
+ {...props}
23
+ />
24
+ )
25
+ }
26
+
27
+ export { Label }