@multicomma/arkcn 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.
Files changed (237) hide show
  1. package/README.md +78 -0
  2. package/bin/arkcn.mjs +78 -0
  3. package/cli/commands.mjs +251 -0
  4. package/cli/project.mjs +168 -0
  5. package/cli/registry.mjs +50 -0
  6. package/cli/ui.mjs +46 -0
  7. package/mcp/server.mjs +149 -0
  8. package/package.json +201 -0
  9. package/registry/base.css +680 -0
  10. package/registry/index.json +1833 -0
  11. package/registry/items/accordion.json +18 -0
  12. package/registry/items/activity-feed.json +18 -0
  13. package/registry/items/alert-dialog.json +18 -0
  14. package/registry/items/alert.json +17 -0
  15. package/registry/items/angle-slider.json +17 -0
  16. package/registry/items/app-shell.json +28 -0
  17. package/registry/items/aspect-ratio.json +13 -0
  18. package/registry/items/attachment.json +19 -0
  19. package/registry/items/avatar.json +17 -0
  20. package/registry/items/badge.json +18 -0
  21. package/registry/items/breadcrumb.json +18 -0
  22. package/registry/items/bubble.json +18 -0
  23. package/registry/items/button-group.json +19 -0
  24. package/registry/items/button.json +18 -0
  25. package/registry/items/calendar.json +19 -0
  26. package/registry/items/canvas.json +24 -0
  27. package/registry/items/card.json +15 -0
  28. package/registry/items/carousel.json +19 -0
  29. package/registry/items/cascader.json +23 -0
  30. package/registry/items/chart.json +17 -0
  31. package/registry/items/chat.json +23 -0
  32. package/registry/items/checkbox.json +18 -0
  33. package/registry/items/clipboard.json +19 -0
  34. package/registry/items/collapsible.json +15 -0
  35. package/registry/items/color-picker.json +19 -0
  36. package/registry/items/combobox.json +20 -0
  37. package/registry/items/command.json +21 -0
  38. package/registry/items/comment-thread.json +25 -0
  39. package/registry/items/context-menu.json +18 -0
  40. package/registry/items/controllable.json +14 -0
  41. package/registry/items/data-grid.json +24 -0
  42. package/registry/items/data-table-adapter.json +14 -0
  43. package/registry/items/data-table.json +32 -0
  44. package/registry/items/date-input.json +17 -0
  45. package/registry/items/dialog.json +19 -0
  46. package/registry/items/direction.json +13 -0
  47. package/registry/items/drag-preview.json +16 -0
  48. package/registry/items/drawer.json +18 -0
  49. package/registry/items/dropdown-menu.json +18 -0
  50. package/registry/items/editable.json +17 -0
  51. package/registry/items/empty.json +17 -0
  52. package/registry/items/field.json +19 -0
  53. package/registry/items/file-upload.json +19 -0
  54. package/registry/items/floating-panel.json +19 -0
  55. package/registry/items/floating-toolbar.json +16 -0
  56. package/registry/items/form.json +14 -0
  57. package/registry/items/format.json +15 -0
  58. package/registry/items/gantt.json +22 -0
  59. package/registry/items/history.json +14 -0
  60. package/registry/items/hotkeys.json +18 -0
  61. package/registry/items/hover-card.json +17 -0
  62. package/registry/items/image-cropper.json +17 -0
  63. package/registry/items/input-group.json +20 -0
  64. package/registry/items/input-otp.json +18 -0
  65. package/registry/items/input.json +15 -0
  66. package/registry/items/item.json +19 -0
  67. package/registry/items/json-tree-view.json +17 -0
  68. package/registry/items/kanban.json +26 -0
  69. package/registry/items/kbd.json +15 -0
  70. package/registry/items/label.json +15 -0
  71. package/registry/items/live-region.json +16 -0
  72. package/registry/items/marker.json +18 -0
  73. package/registry/items/marquee.json +17 -0
  74. package/registry/items/menubar.json +19 -0
  75. package/registry/items/message.json +15 -0
  76. package/registry/items/navigation-menu.json +19 -0
  77. package/registry/items/node-graph.json +22 -0
  78. package/registry/items/number-input.json +18 -0
  79. package/registry/items/pagination.json +18 -0
  80. package/registry/items/password-input.json +18 -0
  81. package/registry/items/popconfirm.json +20 -0
  82. package/registry/items/popover.json +18 -0
  83. package/registry/items/progress.json +17 -0
  84. package/registry/items/qr-code.json +17 -0
  85. package/registry/items/query-builder.json +23 -0
  86. package/registry/items/radio-group.json +17 -0
  87. package/registry/items/rating-group.json +18 -0
  88. package/registry/items/resizable.json +17 -0
  89. package/registry/items/rich-text-editor.json +37 -0
  90. package/registry/items/row-layout.json +15 -0
  91. package/registry/items/scheduler.json +23 -0
  92. package/registry/items/scroll-area.json +17 -0
  93. package/registry/items/segment-group.json +17 -0
  94. package/registry/items/select.json +19 -0
  95. package/registry/items/separator.json +15 -0
  96. package/registry/items/sheet.json +19 -0
  97. package/registry/items/sidebar.json +27 -0
  98. package/registry/items/signature-pad.json +19 -0
  99. package/registry/items/skeleton.json +15 -0
  100. package/registry/items/slider.json +17 -0
  101. package/registry/items/sonner.json +17 -0
  102. package/registry/items/spinner.json +17 -0
  103. package/registry/items/steps.json +19 -0
  104. package/registry/items/swap.json +17 -0
  105. package/registry/items/switch.json +17 -0
  106. package/registry/items/table.json +16 -0
  107. package/registry/items/tabs.json +18 -0
  108. package/registry/items/tags-input.json +18 -0
  109. package/registry/items/textarea.json +15 -0
  110. package/registry/items/time.json +14 -0
  111. package/registry/items/timer.json +18 -0
  112. package/registry/items/toc.json +17 -0
  113. package/registry/items/toggle-group.json +19 -0
  114. package/registry/items/toggle.json +18 -0
  115. package/registry/items/tooltip.json +17 -0
  116. package/registry/items/tour.json +19 -0
  117. package/registry/items/transfer-list.json +24 -0
  118. package/registry/items/tree-select.json +24 -0
  119. package/registry/items/tree-view.json +18 -0
  120. package/registry/items/use-mobile.json +13 -0
  121. package/registry/items/utils.json +17 -0
  122. package/registry/items/virtual-list.json +16 -0
  123. package/src/components/ui/accordion.tsx +111 -0
  124. package/src/components/ui/activity-feed.tsx +228 -0
  125. package/src/components/ui/alert-dialog.tsx +204 -0
  126. package/src/components/ui/alert.tsx +55 -0
  127. package/src/components/ui/angle-slider.tsx +117 -0
  128. package/src/components/ui/app-shell.tsx +985 -0
  129. package/src/components/ui/aspect-ratio.tsx +35 -0
  130. package/src/components/ui/attachment.tsx +193 -0
  131. package/src/components/ui/avatar.tsx +94 -0
  132. package/src/components/ui/badge.tsx +45 -0
  133. package/src/components/ui/breadcrumb.tsx +93 -0
  134. package/src/components/ui/bubble.tsx +119 -0
  135. package/src/components/ui/button-group.tsx +79 -0
  136. package/src/components/ui/button.tsx +65 -0
  137. package/src/components/ui/calendar.tsx +172 -0
  138. package/src/components/ui/canvas.tsx +723 -0
  139. package/src/components/ui/card.tsx +69 -0
  140. package/src/components/ui/carousel.tsx +228 -0
  141. package/src/components/ui/cascader.tsx +754 -0
  142. package/src/components/ui/chart.tsx +303 -0
  143. package/src/components/ui/chat.tsx +544 -0
  144. package/src/components/ui/checkbox.tsx +94 -0
  145. package/src/components/ui/clipboard.tsx +94 -0
  146. package/src/components/ui/collapsible.tsx +32 -0
  147. package/src/components/ui/color-picker.tsx +441 -0
  148. package/src/components/ui/combobox.tsx +400 -0
  149. package/src/components/ui/command.tsx +292 -0
  150. package/src/components/ui/comment-thread.tsx +923 -0
  151. package/src/components/ui/context-menu.tsx +331 -0
  152. package/src/components/ui/data-grid.tsx +1227 -0
  153. package/src/components/ui/data-table.tsx +993 -0
  154. package/src/components/ui/date-input.tsx +103 -0
  155. package/src/components/ui/dialog.tsx +159 -0
  156. package/src/components/ui/direction.tsx +32 -0
  157. package/src/components/ui/drawer.tsx +200 -0
  158. package/src/components/ui/dropdown-menu.tsx +329 -0
  159. package/src/components/ui/editable.tsx +90 -0
  160. package/src/components/ui/empty.tsx +86 -0
  161. package/src/components/ui/field.tsx +218 -0
  162. package/src/components/ui/file-upload.tsx +204 -0
  163. package/src/components/ui/floating-panel.tsx +199 -0
  164. package/src/components/ui/floating-toolbar.tsx +79 -0
  165. package/src/components/ui/format.tsx +22 -0
  166. package/src/components/ui/gantt.tsx +950 -0
  167. package/src/components/ui/hotkeys.tsx +340 -0
  168. package/src/components/ui/hover-card.tsx +83 -0
  169. package/src/components/ui/image-cropper.tsx +100 -0
  170. package/src/components/ui/input-group.tsx +133 -0
  171. package/src/components/ui/input-otp.tsx +82 -0
  172. package/src/components/ui/input.tsx +18 -0
  173. package/src/components/ui/item.tsx +174 -0
  174. package/src/components/ui/json-tree-view.tsx +37 -0
  175. package/src/components/ui/kanban.tsx +884 -0
  176. package/src/components/ui/kbd.tsx +20 -0
  177. package/src/components/ui/label.tsx +30 -0
  178. package/src/components/ui/live-region.tsx +54 -0
  179. package/src/components/ui/marker.tsx +66 -0
  180. package/src/components/ui/marquee.tsx +61 -0
  181. package/src/components/ui/menubar.tsx +456 -0
  182. package/src/components/ui/message.tsx +74 -0
  183. package/src/components/ui/navigation-menu.tsx +202 -0
  184. package/src/components/ui/node-graph.tsx +1723 -0
  185. package/src/components/ui/number-input.tsx +148 -0
  186. package/src/components/ui/pagination.tsx +88 -0
  187. package/src/components/ui/password-input.tsx +104 -0
  188. package/src/components/ui/popconfirm.tsx +149 -0
  189. package/src/components/ui/popover.tsx +126 -0
  190. package/src/components/ui/progress.tsx +114 -0
  191. package/src/components/ui/qr-code.tsx +51 -0
  192. package/src/components/ui/query-builder.tsx +938 -0
  193. package/src/components/ui/radio-group.tsx +108 -0
  194. package/src/components/ui/rating-group.tsx +91 -0
  195. package/src/components/ui/resizable.tsx +41 -0
  196. package/src/components/ui/rich-text-editor.tsx +845 -0
  197. package/src/components/ui/scheduler.tsx +1164 -0
  198. package/src/components/ui/scroll-area.tsx +82 -0
  199. package/src/components/ui/segment-group.tsx +95 -0
  200. package/src/components/ui/select.tsx +354 -0
  201. package/src/components/ui/separator.tsx +32 -0
  202. package/src/components/ui/sheet.tsx +130 -0
  203. package/src/components/ui/sidebar.tsx +665 -0
  204. package/src/components/ui/signature-pad.tsx +112 -0
  205. package/src/components/ui/skeleton.tsx +7 -0
  206. package/src/components/ui/slider.tsx +183 -0
  207. package/src/components/ui/sonner.tsx +37 -0
  208. package/src/components/ui/spinner.tsx +16 -0
  209. package/src/components/ui/steps.tsx +152 -0
  210. package/src/components/ui/swap.tsx +33 -0
  211. package/src/components/ui/switch.tsx +83 -0
  212. package/src/components/ui/table.tsx +72 -0
  213. package/src/components/ui/tabs.tsx +100 -0
  214. package/src/components/ui/tags-input.tsx +153 -0
  215. package/src/components/ui/textarea.tsx +17 -0
  216. package/src/components/ui/timer.tsx +67 -0
  217. package/src/components/ui/toc.tsx +64 -0
  218. package/src/components/ui/toggle-group.tsx +89 -0
  219. package/src/components/ui/toggle.tsx +46 -0
  220. package/src/components/ui/tooltip.tsx +113 -0
  221. package/src/components/ui/tour.tsx +168 -0
  222. package/src/components/ui/transfer-list.tsx +700 -0
  223. package/src/components/ui/tree-select.tsx +638 -0
  224. package/src/components/ui/tree-view.tsx +217 -0
  225. package/src/components/ui/virtual-list.tsx +343 -0
  226. package/src/hooks/use-mobile.ts +19 -0
  227. package/src/lib/controllable.ts +31 -0
  228. package/src/lib/data-table-adapter.ts +67 -0
  229. package/src/lib/drag-preview.ts +39 -0
  230. package/src/lib/form.ts +39 -0
  231. package/src/lib/history.ts +55 -0
  232. package/src/lib/row-layout.ts +81 -0
  233. package/src/lib/table.ts +483 -0
  234. package/src/lib/time.ts +37 -0
  235. package/src/lib/utils.ts +6 -0
  236. package/src/styles/arkcn.css +764 -0
  237. package/src/styles/index.css +3 -0
@@ -0,0 +1,340 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { cn } from "@/lib/utils"
5
+ import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "@/components/ui/dialog"
6
+ import { Kbd, KbdGroup } from "@/components/ui/kbd"
7
+
8
+ /* -------------------------------- parsing -------------------------------- */
9
+
10
+ const isMac = () => typeof navigator !== "undefined" && /mac|iphone|ipad|ipod/i.test(navigator.platform)
11
+
12
+ type Chord = { key: string; mod: boolean; ctrl: boolean; meta: boolean; alt: boolean; shift: boolean }
13
+
14
+ const keyAliases: Record<string, string> = {
15
+ esc: "escape",
16
+ return: "enter",
17
+ space: " ",
18
+ up: "arrowup",
19
+ down: "arrowdown",
20
+ left: "arrowleft",
21
+ right: "arrowright",
22
+ plus: "+",
23
+ del: "delete",
24
+ }
25
+
26
+ /** "mod+shift+k" → chord; "g d" → sequence of chords. */
27
+ function parseHotkey(hotkey: string): Chord[] {
28
+ return hotkey
29
+ .trim()
30
+ .split(/\s+/)
31
+ .map((part) => {
32
+ const tokens = part.split("+").map((t) => t.toLowerCase())
33
+ // "+" itself is written as "plus"
34
+ const key = tokens.pop() ?? ""
35
+ const chord: Chord = {
36
+ key: keyAliases[key] ?? key,
37
+ mod: false,
38
+ ctrl: false,
39
+ meta: false,
40
+ alt: false,
41
+ shift: false,
42
+ }
43
+ for (const t of tokens) {
44
+ if (t === "mod" || t === "cmd" || t === "command") chord.mod = true
45
+ else if (t === "ctrl" || t === "control") chord.ctrl = true
46
+ else if (t === "meta" || t === "win" || t === "super") chord.meta = true
47
+ else if (t === "alt" || t === "option") chord.alt = true
48
+ else if (t === "shift") chord.shift = true
49
+ }
50
+ return chord
51
+ })
52
+ }
53
+
54
+ function matchesChord(event: KeyboardEvent, chord: Chord, mac: boolean) {
55
+ const key = event.key.length === 1 ? event.key.toLowerCase() : event.key.toLowerCase()
56
+ if (key !== chord.key) return false
57
+ const wantMeta = chord.meta || (chord.mod && mac)
58
+ const wantCtrl = chord.ctrl || (chord.mod && !mac)
59
+ if (event.metaKey !== wantMeta || event.ctrlKey !== wantCtrl || event.altKey !== chord.alt) return false
60
+ // Punctuation like "?" already implies Shift on most layouts, so only enforce Shift for letters.
61
+ const letter = /^[a-z0-9]$/.test(chord.key) || chord.key.length > 1
62
+ if (letter && event.shiftKey !== chord.shift) return false
63
+ return true
64
+ }
65
+
66
+ const symbol: Record<string, string> = {
67
+ mod: "⌘",
68
+ meta: "⌘",
69
+ ctrl: "⌃",
70
+ alt: "⌥",
71
+ shift: "⇧",
72
+ enter: "↵",
73
+ escape: "Esc",
74
+ backspace: "⌫",
75
+ delete: "⌦",
76
+ arrowup: "↑",
77
+ arrowdown: "↓",
78
+ arrowleft: "←",
79
+ arrowright: "→",
80
+ " ": "Space",
81
+ tab: "Tab",
82
+ }
83
+ const winSymbol: Record<string, string> = {
84
+ ...symbol,
85
+ mod: "Ctrl",
86
+ meta: "Win",
87
+ ctrl: "Ctrl",
88
+ alt: "Alt",
89
+ shift: "Shift",
90
+ }
91
+
92
+ /** Platform-aware key labels: "mod+k" → ["⌘", "K"] on Mac, ["Ctrl", "K"] elsewhere. Sequences give one array per chord. */
93
+ function formatHotkey(hotkey: string, mac = isMac()): string[][] {
94
+ const table = mac ? symbol : winSymbol
95
+ return parseHotkey(hotkey).map((chord) => {
96
+ const parts: string[] = []
97
+ if (chord.ctrl) parts.push(table.ctrl)
98
+ if (chord.alt) parts.push(table.alt)
99
+ if (chord.shift) parts.push(table.shift)
100
+ if (chord.mod) parts.push(table.mod)
101
+ if (chord.meta && !chord.mod) parts.push(table.meta)
102
+ parts.push(table[chord.key] ?? (chord.key.length === 1 ? chord.key.toUpperCase() : chord.key))
103
+ return parts
104
+ })
105
+ }
106
+
107
+ /* -------------------------------- registry ------------------------------- */
108
+
109
+ type HotkeyEntry = {
110
+ id: string
111
+ hotkey: string
112
+ label: string
113
+ group: string
114
+ enabled: boolean
115
+ /** Fire even while typing in an input, textarea, or contenteditable. */
116
+ allowInInput: boolean
117
+ handler: (event: KeyboardEvent) => void
118
+ }
119
+
120
+ type HotkeysContextValue = {
121
+ entries: HotkeyEntry[]
122
+ register: (entry: HotkeyEntry) => () => void
123
+ dialogOpen: boolean
124
+ setDialogOpen: (open: boolean) => void
125
+ }
126
+
127
+ const HotkeysContext = React.createContext<HotkeysContextValue | null>(null)
128
+
129
+ function useHotkeysContext() {
130
+ const ctx = React.useContext(HotkeysContext)
131
+ if (!ctx) throw new Error("Hotkeys parts must be used inside <HotkeysProvider>")
132
+ return ctx
133
+ }
134
+
135
+ const isEditable = (target: EventTarget | null) => {
136
+ const el = target as HTMLElement | null
137
+ if (!el || !("tagName" in el)) return false
138
+ const tag = el.tagName
139
+ return tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT" || el.isContentEditable
140
+ }
141
+
142
+ /**
143
+ * One document-level keydown listener for every registered hotkey. Supports chords
144
+ * ("mod+k"), sequences ("g d", one second between keys), and a built-in "?" that opens
145
+ * `HotkeysDialog` when one is rendered.
146
+ */
147
+ function HotkeysProvider({
148
+ children,
149
+ dialogHotkey = "?",
150
+ sequenceTimeout = 1000,
151
+ }: {
152
+ children: React.ReactNode
153
+ /** Opens the shortcuts dialog; `null` disables it. */
154
+ dialogHotkey?: string | null
155
+ sequenceTimeout?: number
156
+ }) {
157
+ const [entries, setEntries] = React.useState<HotkeyEntry[]>([])
158
+ const [dialogOpen, setDialogOpen] = React.useState(false)
159
+ const entriesRef = React.useRef(entries)
160
+ entriesRef.current = entries
161
+ const buffer = React.useRef<{ keys: KeyboardEvent[]; at: number }>({ keys: [], at: 0 })
162
+
163
+ const register = React.useCallback((entry: HotkeyEntry) => {
164
+ setEntries((prev) => [...prev.filter((e) => e.id !== entry.id), entry])
165
+ return () => setEntries((prev) => prev.filter((e) => e.id !== entry.id))
166
+ }, [])
167
+
168
+ const mac = React.useMemo(isMac, [])
169
+ React.useEffect(() => {
170
+ const down = (event: KeyboardEvent) => {
171
+ if (event.defaultPrevented || event.isComposing) return
172
+ if (["Shift", "Control", "Alt", "Meta"].includes(event.key)) return
173
+ const editing = isEditable(event.target)
174
+ const now = Date.now()
175
+ const buf = buffer.current
176
+ if (now - buf.at > sequenceTimeout) buf.keys = []
177
+ buf.keys.push(event)
178
+ buf.at = now
179
+ if (buf.keys.length > 4) buf.keys.shift()
180
+
181
+ const candidates = [...entriesRef.current]
182
+ if (dialogHotkey) {
183
+ candidates.push({
184
+ id: "hotkeys:dialog",
185
+ hotkey: dialogHotkey,
186
+ label: "Keyboard shortcuts",
187
+ group: "General",
188
+ enabled: true,
189
+ allowInInput: false,
190
+ handler: () => setDialogOpen((open) => !open),
191
+ })
192
+ }
193
+ let partial = false
194
+ for (const entry of candidates) {
195
+ if (!entry.enabled || (editing && !entry.allowInInput)) continue
196
+ const chords = parseHotkey(entry.hotkey)
197
+ const tail = buf.keys.slice(-chords.length)
198
+ if (tail.length === chords.length && chords.every((c, i) => matchesChord(tail[i], c, mac))) {
199
+ event.preventDefault()
200
+ buf.keys = []
201
+ entry.handler(event)
202
+ return
203
+ }
204
+ // Is the buffer a prefix of a longer sequence? Then keep it.
205
+ if (chords.length > 1 && buf.keys.length < chords.length) {
206
+ const head = chords.slice(0, buf.keys.length)
207
+ if (head.every((c, i) => matchesChord(buf.keys[i], c, mac))) partial = true
208
+ }
209
+ }
210
+ if (!partial) buf.keys = []
211
+ }
212
+ document.addEventListener("keydown", down)
213
+ return () => document.removeEventListener("keydown", down)
214
+ }, [dialogHotkey, sequenceTimeout, mac])
215
+
216
+ const ctx = React.useMemo<HotkeysContextValue>(
217
+ () => ({ entries, register, dialogOpen, setDialogOpen }),
218
+ [entries, register, dialogOpen]
219
+ )
220
+ return <HotkeysContext.Provider value={ctx}>{children}</HotkeysContext.Provider>
221
+ }
222
+
223
+ /** Register a hotkey for the lifetime of the component. Listed in `HotkeysDialog` when it has a label. */
224
+ function useHotkey(
225
+ hotkey: string,
226
+ handler: (event: KeyboardEvent) => void,
227
+ options: { label?: string; group?: string; enabled?: boolean; allowInInput?: boolean } = {}
228
+ ) {
229
+ const { register } = useHotkeysContext()
230
+ const { label = "", group = "General", enabled = true, allowInInput = false } = options
231
+ const id = React.useId()
232
+ const handlerRef = React.useRef(handler)
233
+ handlerRef.current = handler
234
+ React.useEffect(
235
+ () =>
236
+ register({
237
+ id,
238
+ hotkey,
239
+ label,
240
+ group,
241
+ enabled,
242
+ allowInInput,
243
+ handler: (event) => handlerRef.current(event),
244
+ }),
245
+ [register, id, hotkey, label, group, enabled, allowInInput]
246
+ )
247
+ }
248
+
249
+ /** Every registered hotkey with a label, grouped, for rendering a cheat sheet. */
250
+ function useHotkeys() {
251
+ const { entries } = useHotkeysContext()
252
+ return React.useMemo(() => {
253
+ const groups = new Map<string, HotkeyEntry[]>()
254
+ for (const e of entries) {
255
+ if (!e.label) continue
256
+ const list = groups.get(e.group) ?? []
257
+ list.push(e)
258
+ groups.set(e.group, list)
259
+ }
260
+ return Array.from(groups, ([group, hotkeys]) => ({ group, hotkeys }))
261
+ }, [entries])
262
+ }
263
+
264
+ /* ---------------------------------- parts -------------------------------- */
265
+
266
+ /** Renders a hotkey as Kbd caps ("mod+k" → ⌘ K; "g d" → G then D). */
267
+ function HotkeyKbd({ hotkey, className, ...props }: React.ComponentProps<"span"> & { hotkey: string }) {
268
+ const chords = formatHotkey(hotkey)
269
+ return (
270
+ <span data-slot="hotkey-kbd" className={cn("inline-flex items-center gap-1", className)} {...props}>
271
+ {chords.map((keys, i) => (
272
+ <React.Fragment key={i}>
273
+ {i > 0 && <span className="text-xs text-muted-foreground">then</span>}
274
+ <KbdGroup>
275
+ {keys.map((k, j) => (
276
+ <Kbd key={j}>{k}</Kbd>
277
+ ))}
278
+ </KbdGroup>
279
+ </React.Fragment>
280
+ ))}
281
+ </span>
282
+ )
283
+ }
284
+
285
+ /** The shortcuts cheat sheet. Opens from the provider's `dialogHotkey` ("?" by default) or `useHotkeysDialog()`. */
286
+ function HotkeysDialog({
287
+ title = "Keyboard shortcuts",
288
+ description = "Shortcuts work anywhere on the page except while typing.",
289
+ children,
290
+ className,
291
+ }: {
292
+ title?: React.ReactNode
293
+ description?: React.ReactNode
294
+ children?: React.ReactNode
295
+ className?: string
296
+ }) {
297
+ const { dialogOpen, setDialogOpen } = useHotkeysContext()
298
+ const registered = useHotkeys()
299
+ const groups = React.useMemo(() => {
300
+ const self = { id: "hotkeys:dialog", hotkey: "?", label: "Show this dialog" }
301
+ const general = registered.find((g) => g.group === "General")
302
+ if (general) return registered.map((g) => (g === general ? { ...g, hotkeys: [...g.hotkeys, self] } : g))
303
+ return [...registered, { group: "General", hotkeys: [self] }]
304
+ }, [registered])
305
+ return (
306
+ <Dialog open={dialogOpen} onOpenChange={({ open }) => setDialogOpen(open)}>
307
+ <DialogContent data-slot="hotkeys-dialog" className={cn("sm:max-w-lg", className)}>
308
+ <DialogHeader>
309
+ <DialogTitle>{title}</DialogTitle>
310
+ <DialogDescription>{description}</DialogDescription>
311
+ </DialogHeader>
312
+ <div className="grid gap-5 sm:grid-cols-2">
313
+ {groups.map(({ group, hotkeys }) => (
314
+ <div key={group} data-slot="hotkeys-dialog-group" className="flex flex-col gap-1.5">
315
+ <h4 className="text-xs font-medium text-muted-foreground uppercase">{group}</h4>
316
+ {hotkeys.map((h) => (
317
+ <div
318
+ key={h.id}
319
+ data-slot="hotkeys-dialog-item"
320
+ className="flex items-center justify-between gap-3 text-sm"
321
+ >
322
+ <span className="truncate">{h.label}</span>
323
+ <HotkeyKbd hotkey={h.hotkey} />
324
+ </div>
325
+ ))}
326
+ </div>
327
+ ))}
328
+ </div>
329
+ {children}
330
+ </DialogContent>
331
+ </Dialog>
332
+ )
333
+ }
334
+
335
+ function useHotkeysDialog() {
336
+ const { dialogOpen, setDialogOpen } = useHotkeysContext()
337
+ return { open: dialogOpen, setOpen: setDialogOpen }
338
+ }
339
+
340
+ export { HotkeyKbd, HotkeysDialog, HotkeysProvider, formatHotkey, parseHotkey, useHotkey, useHotkeys, useHotkeysDialog }
@@ -0,0 +1,83 @@
1
+ import * as React from "react"
2
+ import { cn } from "@/lib/utils"
3
+ import { HoverCard as HoverCardPrimitive, Portal as PortalPrimitive } from "@ark-ui/react"
4
+
5
+ function HoverCard({
6
+ positioning,
7
+ lazyMount = true,
8
+ unmountOnExit = true,
9
+ ...props
10
+ }: React.ComponentProps<typeof HoverCardPrimitive.Root>) {
11
+ return (
12
+ <HoverCardPrimitive.Root
13
+ positioning={{ placement: "bottom", gutter: 4, ...positioning }}
14
+ lazyMount={lazyMount}
15
+ unmountOnExit={unmountOnExit}
16
+ {...props}
17
+ />
18
+ )
19
+ }
20
+
21
+ function HoverCardTrigger({ ...props }: React.ComponentProps<typeof HoverCardPrimitive.Trigger>) {
22
+ return <HoverCardPrimitive.Trigger data-slot="hover-card-trigger" {...props} />
23
+ }
24
+
25
+ function HoverCardPortal({ ...props }: React.ComponentProps<typeof PortalPrimitive>) {
26
+ return <PortalPrimitive {...props} />
27
+ }
28
+
29
+ function HoverCardContext({ ...props }: React.ComponentProps<typeof HoverCardPrimitive.Context>) {
30
+ return <HoverCardPrimitive.Context {...props} />
31
+ }
32
+
33
+ function HoverCardPositioner({ className, ...props }: React.ComponentProps<typeof HoverCardPrimitive.Positioner>) {
34
+ return (
35
+ <HoverCardPrimitive.Positioner
36
+ data-slot="hover-card-positioner"
37
+ className={cn("[--z-index:50]", className)}
38
+ {...props}
39
+ />
40
+ )
41
+ }
42
+
43
+ function HoverCardArrow({ className, ...props }: React.ComponentProps<typeof HoverCardPrimitive.Arrow>) {
44
+ return (
45
+ <HoverCardPrimitive.Arrow
46
+ data-slot="hover-card-arrow"
47
+ className={cn("[--arrow-background:var(--color-popover)] [--arrow-size:0.625rem]", className)}
48
+ {...props}
49
+ >
50
+ <HoverCardPrimitive.ArrowTip
51
+ data-slot="hover-card-arrow-tip"
52
+ className="border-t border-l border-foreground/10"
53
+ />
54
+ </HoverCardPrimitive.Arrow>
55
+ )
56
+ }
57
+
58
+ function HoverCardContent({ className, ...props }: React.ComponentProps<typeof HoverCardPrimitive.Content>) {
59
+ return (
60
+ <HoverCardPortal>
61
+ <HoverCardPositioner>
62
+ <HoverCardPrimitive.Content
63
+ data-slot="hover-card-content"
64
+ className={cn(
65
+ "z-50 w-64 origin-(--transform-origin) rounded-lg bg-popover p-2.5 text-sm text-popover-foreground shadow-md ring-1 ring-foreground/10 outline-hidden duration-100 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-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95",
66
+ className
67
+ )}
68
+ {...props}
69
+ />
70
+ </HoverCardPositioner>
71
+ </HoverCardPortal>
72
+ )
73
+ }
74
+
75
+ export {
76
+ HoverCard,
77
+ HoverCardArrow,
78
+ HoverCardContent,
79
+ HoverCardContext,
80
+ HoverCardPortal,
81
+ HoverCardPositioner,
82
+ HoverCardTrigger,
83
+ }
@@ -0,0 +1,100 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { cn } from "@/lib/utils"
5
+ import { ImageCropper as ImageCropperPrimitive } from "@ark-ui/react"
6
+
7
+ const handlePositions = ["n", "e", "s", "w", "ne", "se", "sw", "nw"] as const
8
+
9
+ function ImageCropper({ className, ...props }: React.ComponentProps<typeof ImageCropperPrimitive.Root>) {
10
+ return (
11
+ <ImageCropperPrimitive.Root
12
+ data-slot="image-cropper"
13
+ className={cn("flex w-full flex-col gap-2", className)}
14
+ {...props}
15
+ />
16
+ )
17
+ }
18
+
19
+ function ImageCropperContext({ ...props }: React.ComponentProps<typeof ImageCropperPrimitive.Context>) {
20
+ return <ImageCropperPrimitive.Context {...props} />
21
+ }
22
+
23
+ function ImageCropperViewport({ className, ...props }: React.ComponentProps<typeof ImageCropperPrimitive.Viewport>) {
24
+ return (
25
+ <ImageCropperPrimitive.Viewport
26
+ data-slot="image-cropper-viewport"
27
+ className={cn(
28
+ "relative h-72 w-full touch-none overflow-hidden rounded-xl border bg-muted select-none",
29
+ className
30
+ )}
31
+ {...props}
32
+ />
33
+ )
34
+ }
35
+
36
+ function ImageCropperImage({ className, ...props }: React.ComponentProps<typeof ImageCropperPrimitive.Image>) {
37
+ return (
38
+ <ImageCropperPrimitive.Image data-slot="image-cropper-image" className={cn("max-w-none", className)} {...props} />
39
+ )
40
+ }
41
+
42
+ function ImageCropperSelection({
43
+ className,
44
+ children,
45
+ ...props
46
+ }: React.ComponentProps<typeof ImageCropperPrimitive.Selection>) {
47
+ return (
48
+ <ImageCropperPrimitive.Selection
49
+ data-slot="image-cropper-selection"
50
+ className={cn(
51
+ "shadow-[0_0_0_9999px_rgba(0,0,0,0.45)] outline-1 outline-white/80 data-[shape=circle]:rounded-full",
52
+ className
53
+ )}
54
+ {...props}
55
+ >
56
+ {children ?? (
57
+ <>
58
+ <ImageCropperGrid axis="horizontal" />
59
+ <ImageCropperGrid axis="vertical" />
60
+ {handlePositions.map((position) => (
61
+ <ImageCropperHandle key={position} position={position} />
62
+ ))}
63
+ </>
64
+ )}
65
+ </ImageCropperPrimitive.Selection>
66
+ )
67
+ }
68
+
69
+ function ImageCropperGrid({ className, ...props }: React.ComponentProps<typeof ImageCropperPrimitive.Grid>) {
70
+ return (
71
+ <ImageCropperPrimitive.Grid
72
+ data-slot="image-cropper-grid"
73
+ className={cn("stroke-white/50", className)}
74
+ {...props}
75
+ />
76
+ )
77
+ }
78
+
79
+ function ImageCropperHandle({ className, ...props }: React.ComponentProps<typeof ImageCropperPrimitive.Handle>) {
80
+ return (
81
+ <ImageCropperPrimitive.Handle
82
+ data-slot="image-cropper-handle"
83
+ className={cn(
84
+ "after:absolute after:top-1/2 after:left-1/2 after:size-2.5 after:-translate-1/2 after:rounded-full after:border after:border-ring after:bg-white data-[axis=x]:after:h-4 data-[axis=x]:after:w-1 data-[axis=y]:after:h-1 data-[axis=y]:after:w-4",
85
+ className
86
+ )}
87
+ {...props}
88
+ />
89
+ )
90
+ }
91
+
92
+ export {
93
+ ImageCropper,
94
+ ImageCropperContext,
95
+ ImageCropperGrid,
96
+ ImageCropperHandle,
97
+ ImageCropperImage,
98
+ ImageCropperSelection,
99
+ ImageCropperViewport,
100
+ }
@@ -0,0 +1,133 @@
1
+ import * as React from "react"
2
+ import { cva, type VariantProps } from "class-variance-authority"
3
+ import { cn } from "@/lib/utils"
4
+
5
+ import { Button } from "@/components/ui/button"
6
+ import { Input } from "@/components/ui/input"
7
+ import { Textarea } from "@/components/ui/textarea"
8
+
9
+ function InputGroup({ className, ...props }: React.ComponentProps<"div">) {
10
+ return (
11
+ <div
12
+ data-slot="input-group"
13
+ role="group"
14
+ className={cn(
15
+ "group/input-group relative flex h-8 w-full min-w-0 items-center rounded-lg border border-input transition-colors outline-none in-data-[slot=combobox-content]:focus-within:border-inherit in-data-[slot=combobox-content]:focus-within:ring-0 has-disabled:bg-input/50 has-disabled:opacity-50 has-[[data-slot=input-group-control]:focus-visible]:border-ring has-[[data-slot=input-group-control]:focus-visible]:ring-3 has-[[data-slot=input-group-control]:focus-visible]:ring-ring/50 has-[[data-slot][aria-invalid=true]]:border-destructive has-[[data-slot][aria-invalid=true]]:ring-3 has-[[data-slot][aria-invalid=true]]:ring-destructive/20 has-[>[data-align=block-end]]:h-auto has-[>[data-align=block-end]]:flex-col has-[>[data-align=block-start]]:h-auto has-[>[data-align=block-start]]:flex-col has-[>textarea]:h-auto dark:bg-input/30 dark:has-disabled:bg-input/80 dark:has-[[data-slot][aria-invalid=true]]:ring-destructive/40 has-[>[data-align=block-end]]:[&>input]:pt-3 has-[>[data-align=block-start]]:[&>input]:pb-3 has-[>[data-align=inline-end]]:[&>input]:pr-1.5 has-[>[data-align=inline-start]]:[&>input]:pl-1.5",
16
+ className
17
+ )}
18
+ {...props}
19
+ />
20
+ )
21
+ }
22
+
23
+ const inputGroupAddonVariants = cva(
24
+ "flex h-auto cursor-text items-center justify-center gap-2 py-1.5 text-sm font-medium text-muted-foreground select-none group-data-[disabled=true]/input-group:opacity-50 [&>kbd]:rounded-[calc(var(--radius)-5px)] [&>svg:not([class*='size-'])]:size-4",
25
+ {
26
+ variants: {
27
+ align: {
28
+ "inline-start": "order-first pl-2 has-[>button]:ml-[-0.3rem] has-[>kbd]:ml-[-0.15rem]",
29
+ "inline-end": "order-last pr-2 has-[>button]:mr-[-0.3rem] has-[>kbd]:mr-[-0.15rem]",
30
+ "block-start":
31
+ "order-first w-full justify-start px-2.5 pt-2 group-has-[>input]/input-group:pt-2 [.border-b]:pb-2",
32
+ "block-end": "order-last w-full justify-start px-2.5 pb-2 group-has-[>input]/input-group:pb-2 [.border-t]:pt-2",
33
+ },
34
+ },
35
+ defaultVariants: {
36
+ align: "inline-start",
37
+ },
38
+ }
39
+ )
40
+
41
+ function InputGroupAddon({
42
+ className,
43
+ align = "inline-start",
44
+ ...props
45
+ }: React.ComponentProps<"div"> & VariantProps<typeof inputGroupAddonVariants>) {
46
+ return (
47
+ <div
48
+ role="group"
49
+ data-slot="input-group-addon"
50
+ data-align={align}
51
+ className={cn(inputGroupAddonVariants({ align }), className)}
52
+ onClick={(e) => {
53
+ if ((e.target as HTMLElement).closest("button")) {
54
+ return
55
+ }
56
+ e.currentTarget.parentElement?.querySelector("input")?.focus()
57
+ }}
58
+ {...props}
59
+ />
60
+ )
61
+ }
62
+
63
+ const inputGroupTriggerVariants = cva("flex items-center gap-2 text-sm shadow-none", {
64
+ variants: {
65
+ size: {
66
+ xs: "h-6 gap-1 rounded-[calc(var(--radius)-3px)] px-1.5 [&>svg:not([class*='size-'])]:size-3.5",
67
+ sm: "",
68
+ "icon-xs": "size-6 rounded-[calc(var(--radius)-3px)] p-0 has-[>svg]:p-0",
69
+ "icon-sm": "size-8 p-0 has-[>svg]:p-0",
70
+ },
71
+ },
72
+ defaultVariants: {
73
+ size: "xs",
74
+ },
75
+ })
76
+
77
+ function InputGroupTrigger({
78
+ className,
79
+ type = "button",
80
+ variant = "ghost",
81
+ size = "xs",
82
+ ...props
83
+ }: Omit<React.ComponentProps<typeof Button>, "size"> & VariantProps<typeof inputGroupTriggerVariants>) {
84
+ return (
85
+ <Button
86
+ type={type}
87
+ data-size={size}
88
+ variant={variant}
89
+ className={cn(inputGroupTriggerVariants({ size }), className)}
90
+ {...props}
91
+ />
92
+ )
93
+ }
94
+
95
+ function InputGroupText({ className, ...props }: React.ComponentProps<"span">) {
96
+ return (
97
+ <span
98
+ className={cn(
99
+ "flex items-center gap-2 text-sm text-muted-foreground [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4",
100
+ className
101
+ )}
102
+ {...props}
103
+ />
104
+ )
105
+ }
106
+
107
+ function InputGroupInput({ className, ...props }: React.ComponentProps<"input">) {
108
+ return (
109
+ <Input
110
+ data-slot="input-group-control"
111
+ className={cn(
112
+ "flex-1 rounded-none border-0 bg-transparent shadow-none ring-0 focus-visible:ring-0 disabled:bg-transparent aria-invalid:ring-0 dark:bg-transparent dark:disabled:bg-transparent",
113
+ className
114
+ )}
115
+ {...props}
116
+ />
117
+ )
118
+ }
119
+
120
+ function InputGroupTextarea({ className, ...props }: React.ComponentProps<"textarea">) {
121
+ return (
122
+ <Textarea
123
+ data-slot="input-group-control"
124
+ className={cn(
125
+ "flex-1 resize-none rounded-none border-0 bg-transparent py-2 shadow-none ring-0 focus-visible:ring-0 disabled:bg-transparent aria-invalid:ring-0 dark:bg-transparent dark:disabled:bg-transparent",
126
+ className
127
+ )}
128
+ {...props}
129
+ />
130
+ )
131
+ }
132
+
133
+ export { InputGroup, InputGroupAddon, InputGroupTrigger, InputGroupText, InputGroupInput, InputGroupTextarea }