@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.
- package/README.md +78 -0
- package/bin/arkcn.mjs +78 -0
- package/cli/commands.mjs +251 -0
- package/cli/project.mjs +168 -0
- package/cli/registry.mjs +50 -0
- package/cli/ui.mjs +46 -0
- package/mcp/server.mjs +149 -0
- package/package.json +201 -0
- package/registry/base.css +680 -0
- package/registry/index.json +1833 -0
- package/registry/items/accordion.json +18 -0
- package/registry/items/activity-feed.json +18 -0
- package/registry/items/alert-dialog.json +18 -0
- package/registry/items/alert.json +17 -0
- package/registry/items/angle-slider.json +17 -0
- package/registry/items/app-shell.json +28 -0
- package/registry/items/aspect-ratio.json +13 -0
- package/registry/items/attachment.json +19 -0
- package/registry/items/avatar.json +17 -0
- package/registry/items/badge.json +18 -0
- package/registry/items/breadcrumb.json +18 -0
- package/registry/items/bubble.json +18 -0
- package/registry/items/button-group.json +19 -0
- package/registry/items/button.json +18 -0
- package/registry/items/calendar.json +19 -0
- package/registry/items/canvas.json +24 -0
- package/registry/items/card.json +15 -0
- package/registry/items/carousel.json +19 -0
- package/registry/items/cascader.json +23 -0
- package/registry/items/chart.json +17 -0
- package/registry/items/chat.json +23 -0
- package/registry/items/checkbox.json +18 -0
- package/registry/items/clipboard.json +19 -0
- package/registry/items/collapsible.json +15 -0
- package/registry/items/color-picker.json +19 -0
- package/registry/items/combobox.json +20 -0
- package/registry/items/command.json +21 -0
- package/registry/items/comment-thread.json +25 -0
- package/registry/items/context-menu.json +18 -0
- package/registry/items/controllable.json +14 -0
- package/registry/items/data-grid.json +24 -0
- package/registry/items/data-table-adapter.json +14 -0
- package/registry/items/data-table.json +32 -0
- package/registry/items/date-input.json +17 -0
- package/registry/items/dialog.json +19 -0
- package/registry/items/direction.json +13 -0
- package/registry/items/drag-preview.json +16 -0
- package/registry/items/drawer.json +18 -0
- package/registry/items/dropdown-menu.json +18 -0
- package/registry/items/editable.json +17 -0
- package/registry/items/empty.json +17 -0
- package/registry/items/field.json +19 -0
- package/registry/items/file-upload.json +19 -0
- package/registry/items/floating-panel.json +19 -0
- package/registry/items/floating-toolbar.json +16 -0
- package/registry/items/form.json +14 -0
- package/registry/items/format.json +15 -0
- package/registry/items/gantt.json +22 -0
- package/registry/items/history.json +14 -0
- package/registry/items/hotkeys.json +18 -0
- package/registry/items/hover-card.json +17 -0
- package/registry/items/image-cropper.json +17 -0
- package/registry/items/input-group.json +20 -0
- package/registry/items/input-otp.json +18 -0
- package/registry/items/input.json +15 -0
- package/registry/items/item.json +19 -0
- package/registry/items/json-tree-view.json +17 -0
- package/registry/items/kanban.json +26 -0
- package/registry/items/kbd.json +15 -0
- package/registry/items/label.json +15 -0
- package/registry/items/live-region.json +16 -0
- package/registry/items/marker.json +18 -0
- package/registry/items/marquee.json +17 -0
- package/registry/items/menubar.json +19 -0
- package/registry/items/message.json +15 -0
- package/registry/items/navigation-menu.json +19 -0
- package/registry/items/node-graph.json +22 -0
- package/registry/items/number-input.json +18 -0
- package/registry/items/pagination.json +18 -0
- package/registry/items/password-input.json +18 -0
- package/registry/items/popconfirm.json +20 -0
- package/registry/items/popover.json +18 -0
- package/registry/items/progress.json +17 -0
- package/registry/items/qr-code.json +17 -0
- package/registry/items/query-builder.json +23 -0
- package/registry/items/radio-group.json +17 -0
- package/registry/items/rating-group.json +18 -0
- package/registry/items/resizable.json +17 -0
- package/registry/items/rich-text-editor.json +37 -0
- package/registry/items/row-layout.json +15 -0
- package/registry/items/scheduler.json +23 -0
- package/registry/items/scroll-area.json +17 -0
- package/registry/items/segment-group.json +17 -0
- package/registry/items/select.json +19 -0
- package/registry/items/separator.json +15 -0
- package/registry/items/sheet.json +19 -0
- package/registry/items/sidebar.json +27 -0
- package/registry/items/signature-pad.json +19 -0
- package/registry/items/skeleton.json +15 -0
- package/registry/items/slider.json +17 -0
- package/registry/items/sonner.json +17 -0
- package/registry/items/spinner.json +17 -0
- package/registry/items/steps.json +19 -0
- package/registry/items/swap.json +17 -0
- package/registry/items/switch.json +17 -0
- package/registry/items/table.json +16 -0
- package/registry/items/tabs.json +18 -0
- package/registry/items/tags-input.json +18 -0
- package/registry/items/textarea.json +15 -0
- package/registry/items/time.json +14 -0
- package/registry/items/timer.json +18 -0
- package/registry/items/toc.json +17 -0
- package/registry/items/toggle-group.json +19 -0
- package/registry/items/toggle.json +18 -0
- package/registry/items/tooltip.json +17 -0
- package/registry/items/tour.json +19 -0
- package/registry/items/transfer-list.json +24 -0
- package/registry/items/tree-select.json +24 -0
- package/registry/items/tree-view.json +18 -0
- package/registry/items/use-mobile.json +13 -0
- package/registry/items/utils.json +17 -0
- package/registry/items/virtual-list.json +16 -0
- package/src/components/ui/accordion.tsx +111 -0
- package/src/components/ui/activity-feed.tsx +228 -0
- package/src/components/ui/alert-dialog.tsx +204 -0
- package/src/components/ui/alert.tsx +55 -0
- package/src/components/ui/angle-slider.tsx +117 -0
- package/src/components/ui/app-shell.tsx +985 -0
- package/src/components/ui/aspect-ratio.tsx +35 -0
- package/src/components/ui/attachment.tsx +193 -0
- package/src/components/ui/avatar.tsx +94 -0
- package/src/components/ui/badge.tsx +45 -0
- package/src/components/ui/breadcrumb.tsx +93 -0
- package/src/components/ui/bubble.tsx +119 -0
- package/src/components/ui/button-group.tsx +79 -0
- package/src/components/ui/button.tsx +65 -0
- package/src/components/ui/calendar.tsx +172 -0
- package/src/components/ui/canvas.tsx +723 -0
- package/src/components/ui/card.tsx +69 -0
- package/src/components/ui/carousel.tsx +228 -0
- package/src/components/ui/cascader.tsx +754 -0
- package/src/components/ui/chart.tsx +303 -0
- package/src/components/ui/chat.tsx +544 -0
- package/src/components/ui/checkbox.tsx +94 -0
- package/src/components/ui/clipboard.tsx +94 -0
- package/src/components/ui/collapsible.tsx +32 -0
- package/src/components/ui/color-picker.tsx +441 -0
- package/src/components/ui/combobox.tsx +400 -0
- package/src/components/ui/command.tsx +292 -0
- package/src/components/ui/comment-thread.tsx +923 -0
- package/src/components/ui/context-menu.tsx +331 -0
- package/src/components/ui/data-grid.tsx +1227 -0
- package/src/components/ui/data-table.tsx +993 -0
- package/src/components/ui/date-input.tsx +103 -0
- package/src/components/ui/dialog.tsx +159 -0
- package/src/components/ui/direction.tsx +32 -0
- package/src/components/ui/drawer.tsx +200 -0
- package/src/components/ui/dropdown-menu.tsx +329 -0
- package/src/components/ui/editable.tsx +90 -0
- package/src/components/ui/empty.tsx +86 -0
- package/src/components/ui/field.tsx +218 -0
- package/src/components/ui/file-upload.tsx +204 -0
- package/src/components/ui/floating-panel.tsx +199 -0
- package/src/components/ui/floating-toolbar.tsx +79 -0
- package/src/components/ui/format.tsx +22 -0
- package/src/components/ui/gantt.tsx +950 -0
- package/src/components/ui/hotkeys.tsx +340 -0
- package/src/components/ui/hover-card.tsx +83 -0
- package/src/components/ui/image-cropper.tsx +100 -0
- package/src/components/ui/input-group.tsx +133 -0
- package/src/components/ui/input-otp.tsx +82 -0
- package/src/components/ui/input.tsx +18 -0
- package/src/components/ui/item.tsx +174 -0
- package/src/components/ui/json-tree-view.tsx +37 -0
- package/src/components/ui/kanban.tsx +884 -0
- package/src/components/ui/kbd.tsx +20 -0
- package/src/components/ui/label.tsx +30 -0
- package/src/components/ui/live-region.tsx +54 -0
- package/src/components/ui/marker.tsx +66 -0
- package/src/components/ui/marquee.tsx +61 -0
- package/src/components/ui/menubar.tsx +456 -0
- package/src/components/ui/message.tsx +74 -0
- package/src/components/ui/navigation-menu.tsx +202 -0
- package/src/components/ui/node-graph.tsx +1723 -0
- package/src/components/ui/number-input.tsx +148 -0
- package/src/components/ui/pagination.tsx +88 -0
- package/src/components/ui/password-input.tsx +104 -0
- package/src/components/ui/popconfirm.tsx +149 -0
- package/src/components/ui/popover.tsx +126 -0
- package/src/components/ui/progress.tsx +114 -0
- package/src/components/ui/qr-code.tsx +51 -0
- package/src/components/ui/query-builder.tsx +938 -0
- package/src/components/ui/radio-group.tsx +108 -0
- package/src/components/ui/rating-group.tsx +91 -0
- package/src/components/ui/resizable.tsx +41 -0
- package/src/components/ui/rich-text-editor.tsx +845 -0
- package/src/components/ui/scheduler.tsx +1164 -0
- package/src/components/ui/scroll-area.tsx +82 -0
- package/src/components/ui/segment-group.tsx +95 -0
- package/src/components/ui/select.tsx +354 -0
- package/src/components/ui/separator.tsx +32 -0
- package/src/components/ui/sheet.tsx +130 -0
- package/src/components/ui/sidebar.tsx +665 -0
- package/src/components/ui/signature-pad.tsx +112 -0
- package/src/components/ui/skeleton.tsx +7 -0
- package/src/components/ui/slider.tsx +183 -0
- package/src/components/ui/sonner.tsx +37 -0
- package/src/components/ui/spinner.tsx +16 -0
- package/src/components/ui/steps.tsx +152 -0
- package/src/components/ui/swap.tsx +33 -0
- package/src/components/ui/switch.tsx +83 -0
- package/src/components/ui/table.tsx +72 -0
- package/src/components/ui/tabs.tsx +100 -0
- package/src/components/ui/tags-input.tsx +153 -0
- package/src/components/ui/textarea.tsx +17 -0
- package/src/components/ui/timer.tsx +67 -0
- package/src/components/ui/toc.tsx +64 -0
- package/src/components/ui/toggle-group.tsx +89 -0
- package/src/components/ui/toggle.tsx +46 -0
- package/src/components/ui/tooltip.tsx +113 -0
- package/src/components/ui/tour.tsx +168 -0
- package/src/components/ui/transfer-list.tsx +700 -0
- package/src/components/ui/tree-select.tsx +638 -0
- package/src/components/ui/tree-view.tsx +217 -0
- package/src/components/ui/virtual-list.tsx +343 -0
- package/src/hooks/use-mobile.ts +19 -0
- package/src/lib/controllable.ts +31 -0
- package/src/lib/data-table-adapter.ts +67 -0
- package/src/lib/drag-preview.ts +39 -0
- package/src/lib/form.ts +39 -0
- package/src/lib/history.ts +55 -0
- package/src/lib/row-layout.ts +81 -0
- package/src/lib/table.ts +483 -0
- package/src/lib/time.ts +37 -0
- package/src/lib/utils.ts +6 -0
- package/src/styles/arkcn.css +764 -0
- package/src/styles/index.css +3 -0
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "virtual-list",
|
|
3
|
+
"type": "ui",
|
|
4
|
+
"description": "-------------------------------- context --------------------------------",
|
|
5
|
+
"dependencies": {},
|
|
6
|
+
"registryDependencies": [
|
|
7
|
+
"utils"
|
|
8
|
+
],
|
|
9
|
+
"files": [
|
|
10
|
+
{
|
|
11
|
+
"path": "components/ui/virtual-list.tsx",
|
|
12
|
+
"content": "\"use client\"\n\nimport * as React from \"react\"\nimport { cn } from \"@/lib/utils\"\n\n/* -------------------------------- context -------------------------------- */\n\ntype VirtualItem = {\n index: number\n key: React.Key\n /** Offset from the top of the content, in px. */\n start: number\n size: number\n end: number\n /** False until the row's real height has been measured. */\n measured: boolean\n}\n\ntype ScrollAlign = \"auto\" | \"start\" | \"center\" | \"end\"\n\ntype VirtualListContextValue = {\n count: number\n items: VirtualItem[]\n totalSize: number\n /** First and last rendered index (overscan included), or null when empty. */\n range: { start: number; end: number } | null\n measure: (index: number, size: number) => void\n scrollToIndex: (index: number, options?: { align?: ScrollAlign; behavior?: ScrollBehavior }) => void\n scrollToOffset: (offset: number, behavior?: ScrollBehavior) => void\n viewportRef: React.RefObject<HTMLDivElement | null>\n getItemKey: (index: number) => React.Key\n}\n\nconst VirtualListContext = React.createContext<VirtualListContextValue | null>(null)\n\nfunction useVirtualList() {\n const ctx = React.useContext(VirtualListContext)\n if (!ctx) throw new Error(\"VirtualList parts must be used inside <VirtualList>\")\n return ctx\n}\n\n/* -------------------------------- helpers -------------------------------- */\n\n/** Largest index whose start is <= offset. */\nfunction findIndex(starts: Float64Array, count: number, offset: number) {\n let lo = 0\n let hi = count - 1\n while (lo < hi) {\n const mid = (lo + hi + 1) >> 1\n if (starts[mid] <= offset) lo = mid\n else hi = mid - 1\n }\n return lo\n}\n\n/* ---------------------------------- root --------------------------------- */\n\ntype VirtualListProps = Omit<React.ComponentProps<\"div\">, \"children\"> & {\n count: number\n /** Row height before measurement (a number, or per index). */\n estimateSize?: number | ((index: number) => number)\n /** Rows rendered beyond the visible window on each side. */\n overscan?: number\n gap?: number\n paddingStart?: number\n paddingEnd?: number\n getItemKey?: (index: number) => React.Key\n /** Fires when the rendered window changes. */\n onRangeChange?: (range: { start: number; end: number } | null) => void\n children: React.ReactNode\n}\n\nfunction VirtualList({\n count,\n estimateSize = 36,\n overscan = 6,\n gap = 0,\n paddingStart = 0,\n paddingEnd = 0,\n getItemKey = (index) => index,\n onRangeChange,\n className,\n children,\n ...props\n}: VirtualListProps) {\n const viewportRef = React.useRef<HTMLDivElement | null>(null)\n const sizes = React.useRef(new Map<number, number>())\n const [version, setVersion] = React.useState(0)\n const [scrollTop, setScrollTop] = React.useState(0)\n const [viewportSize, setViewportSize] = React.useState(0)\n const estimate = React.useCallback(\n (index: number) => (typeof estimateSize === \"function\" ? estimateSize(index) : estimateSize),\n [estimateSize]\n )\n\n // Prefix sums of row starts; recomputed when a measurement or the count changes.\n const layout = React.useMemo(() => {\n const starts = new Float64Array(count)\n let offset = paddingStart\n for (let i = 0; i < count; i++) {\n starts[i] = offset\n offset += (sizes.current.get(i) ?? estimate(i)) + (i < count - 1 ? gap : 0)\n }\n return { starts, totalSize: offset + paddingEnd }\n // `version` is the measurement tick.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [count, estimate, gap, paddingStart, paddingEnd, version])\n\n const range = React.useMemo(() => {\n if (count === 0 || viewportSize === 0) return count === 0 ? null : { start: 0, end: Math.min(count - 1, overscan) }\n const first = findIndex(layout.starts, count, scrollTop)\n const last = findIndex(layout.starts, count, scrollTop + viewportSize)\n return { start: Math.max(0, first - overscan), end: Math.min(count - 1, last + overscan) }\n }, [count, viewportSize, scrollTop, layout, overscan])\n\n const items = React.useMemo(() => {\n if (!range) return []\n const out: VirtualItem[] = []\n for (let i = range.start; i <= range.end; i++) {\n const measured = sizes.current.has(i)\n const size = sizes.current.get(i) ?? estimate(i)\n out.push({ index: i, key: getItemKey(i), start: layout.starts[i], size, end: layout.starts[i] + size, measured })\n }\n return out\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [range, layout, estimate, getItemKey, version])\n\n const onRangeRef = React.useRef(onRangeChange)\n onRangeRef.current = onRangeChange\n const lastRange = React.useRef<string>(\"\")\n React.useEffect(() => {\n const key = range ? `${range.start}-${range.end}` : \"none\"\n if (key === lastRange.current) return\n lastRange.current = key\n onRangeRef.current?.(range)\n }, [range])\n\n // Drop stale measurements when the list shrinks.\n React.useEffect(() => {\n let dirty = false\n for (const index of sizes.current.keys()) {\n if (index >= count) {\n sizes.current.delete(index)\n dirty = true\n }\n }\n if (dirty) setVersion((v) => v + 1)\n }, [count])\n\n const measure = React.useCallback((index: number, size: number) => {\n const prev = sizes.current.get(index)\n if (prev !== undefined && Math.abs(prev - size) < 0.5) return\n sizes.current.set(index, size)\n setVersion((v) => v + 1)\n }, [])\n\n React.useLayoutEffect(() => {\n const el = viewportRef.current\n if (!el) return\n const update = () => setViewportSize(el.clientHeight)\n update()\n const ro = new ResizeObserver(update)\n ro.observe(el)\n return () => ro.disconnect()\n }, [])\n\n const scrollToOffset = React.useCallback((offset: number, behavior: ScrollBehavior = \"auto\") => {\n viewportRef.current?.scrollTo({ top: offset, behavior })\n }, [])\n\n const pending = React.useRef<{ index: number; align: ScrollAlign; tries: number } | null>(null)\n const offsetFor = React.useCallback(\n (index: number, align: ScrollAlign) => {\n const el = viewportRef.current\n if (!el) return 0\n const start = layout.starts[index] ?? 0\n const size = sizes.current.get(index) ?? estimate(index)\n const height = el.clientHeight\n let resolved = align\n if (align === \"auto\") {\n if (start < el.scrollTop) resolved = \"start\"\n else if (start + size > el.scrollTop + height) resolved = \"end\"\n else return el.scrollTop\n }\n if (resolved === \"start\") return start\n if (resolved === \"end\") return start + size - height\n return start + size / 2 - height / 2\n },\n [layout, estimate]\n )\n\n const scrollToIndex = React.useCallback(\n (index: number, options?: { align?: ScrollAlign; behavior?: ScrollBehavior }) => {\n const clamped = Math.max(0, Math.min(count - 1, index))\n const align = options?.align ?? \"auto\"\n pending.current = { index: clamped, align, tries: 0 }\n scrollToOffset(offsetFor(clamped, align), options?.behavior)\n },\n [count, offsetFor, scrollToOffset]\n )\n\n // Estimated rows shift as they get measured: re-aim at the target a few times.\n React.useLayoutEffect(() => {\n const p = pending.current\n const el = viewportRef.current\n if (!p || !el) return\n if (p.tries >= 4 || !sizes.current.has(p.index)) {\n if (p.tries >= 4) pending.current = null\n return\n }\n const target = offsetFor(p.index, p.align === \"auto\" ? \"start\" : p.align)\n if (Math.abs(el.scrollTop - target) > 1) {\n p.tries += 1\n el.scrollTop = target\n } else pending.current = null\n })\n\n const ctx: VirtualListContextValue = {\n count,\n items,\n totalSize: layout.totalSize,\n range,\n measure,\n scrollToIndex,\n scrollToOffset,\n viewportRef,\n getItemKey,\n }\n\n return (\n <VirtualListContext.Provider value={ctx}>\n <VirtualListScroll onScroll={setScrollTop} />\n <div data-slot=\"virtual-list\" className={cn(\"flex min-h-0 flex-col\", className)} {...props}>\n {children}\n </div>\n </VirtualListContext.Provider>\n )\n}\n\n/** Attaches the scroll listener once the viewport mounts (kept out of the root's render). */\nfunction VirtualListScroll({ onScroll }: { onScroll: (top: number) => void }) {\n const ctx = useVirtualList()\n React.useEffect(() => {\n const el = ctx.viewportRef.current\n if (!el) return\n const handler = () => onScroll(el.scrollTop)\n el.addEventListener(\"scroll\", handler, { passive: true })\n return () => el.removeEventListener(\"scroll\", handler)\n }, [ctx.viewportRef, onScroll])\n return null\n}\n\n/* --------------------------------- parts --------------------------------- */\n\nfunction VirtualListViewport({ className, ...props }: React.ComponentProps<\"div\">) {\n const ctx = useVirtualList()\n return (\n <div\n ref={ctx.viewportRef}\n data-slot=\"virtual-list-viewport\"\n className={cn(\"relative min-h-0 overflow-y-auto overscroll-contain\", className)}\n {...props}\n />\n )\n}\n\n/** Sized to the whole list so the scrollbar is right; rows position inside it. */\nfunction VirtualListContent({ className, style, ...props }: React.ComponentProps<\"div\">) {\n const ctx = useVirtualList()\n return (\n <div\n data-slot=\"virtual-list-content\"\n className={cn(\"relative w-full\", className)}\n style={{ height: ctx.totalSize, ...style }}\n {...props}\n />\n )\n}\n\n/** Renders the visible window. The child renders one row and must return a `VirtualListItem`. */\nfunction VirtualListItems({ children }: { children: (item: VirtualItem) => React.ReactNode }) {\n const ctx = useVirtualList()\n return (\n <>\n {ctx.items.map((item) => (\n <React.Fragment key={item.key}>{children(item)}</React.Fragment>\n ))}\n </>\n )\n}\n\nfunction VirtualListItem({ index, className, style, ...props }: React.ComponentProps<\"div\"> & { index: number }) {\n const { items, measure } = useVirtualList()\n const item = items.find((i) => i.index === index)\n const ref = React.useCallback(\n (el: HTMLDivElement | null) => {\n if (!el) return\n const run = () => measure(index, el.getBoundingClientRect().height)\n run()\n const ro = new ResizeObserver(run)\n ro.observe(el)\n // React 19 ref cleanup\n return () => ro.disconnect()\n },\n [measure, index]\n )\n return (\n <div\n ref={ref}\n data-slot=\"virtual-list-item\"\n data-index={index}\n className={cn(\"absolute inset-x-0 top-0\", className)}\n style={{ transform: `translateY(${item?.start ?? 0}px)`, ...style }}\n {...props}\n />\n )\n}\n\nfunction VirtualListEmpty({ className, children, ...props }: React.ComponentProps<\"div\">) {\n const ctx = useVirtualList()\n if (ctx.count > 0) return null\n return (\n <div\n data-slot=\"virtual-list-empty\"\n className={cn(\"flex flex-1 items-center justify-center py-8 text-sm text-muted-foreground\", className)}\n {...props}\n >\n {children ?? \"Nothing to show\"}\n </div>\n )\n}\n\nexport {\n VirtualList,\n VirtualListContent,\n VirtualListEmpty,\n VirtualListItem,\n VirtualListItems,\n VirtualListViewport,\n useVirtualList,\n type VirtualItem,\n type VirtualListProps,\n}\n"
|
|
13
|
+
}
|
|
14
|
+
],
|
|
15
|
+
"docs": "## Virtual list\n`src/components/ui/virtual-list.tsx` is a dependency-free windowed list. `VirtualList { count,\nestimateSize (number or per index), overscan, gap, paddingStart/End, getItemKey, onRangeChange }`\nowns scroll offset, viewport size, and measured row heights (prefix sums + binary search; rows\nmeasure themselves with a ResizeObserver and estimates are replaced as rows appear). Parts:\n`VirtualListViewport` (the scroll container; **the consumer gives it a height**, it must never be\n`flex-1` in an auto-height column or it grows to the whole content and renders every row) >\n`VirtualListContent` (sized to `totalSize`) > `VirtualListItems` (render-prop child gets\n`{ index, key, start, size, measured }` and returns a `VirtualListItem index=`, absolutely\npositioned via translateY, `data-index`), `VirtualListEmpty`. `useVirtualList()` exposes `items`,\n`range`, `totalSize`, `scrollToIndex(index, { align: auto|start|center|end })` (re-aims a few times\nwhile estimated rows get measured) and `scrollToOffset`. Keyboard is the consumer's: keep focus on\nthe viewport with `aria-activedescendant` and call `scrollToIndex` for the active row, as the\ncontacts listbox in `src/demo/virtual-list/` does (`/audit-log`, \"Audit log\": a 100k-line log with mixed\nheights and go-to-line, and a 10k-contact listbox with search)."
|
|
16
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
import { cn } from "@/lib/utils"
|
|
3
|
+
import { Accordion as AccordionPrimitive } from "@ark-ui/react"
|
|
4
|
+
import { ChevronDownIcon, ChevronUpIcon } from "lucide-react"
|
|
5
|
+
|
|
6
|
+
function Accordion({
|
|
7
|
+
className,
|
|
8
|
+
lazyMount = true,
|
|
9
|
+
unmountOnExit = true,
|
|
10
|
+
...props
|
|
11
|
+
}: React.ComponentProps<typeof AccordionPrimitive.Root>) {
|
|
12
|
+
return (
|
|
13
|
+
<AccordionPrimitive.Root
|
|
14
|
+
data-slot="accordion"
|
|
15
|
+
lazyMount={lazyMount}
|
|
16
|
+
unmountOnExit={unmountOnExit}
|
|
17
|
+
className={cn("flex w-full flex-col", className)}
|
|
18
|
+
{...props}
|
|
19
|
+
/>
|
|
20
|
+
)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function AccordionContext({ ...props }: React.ComponentProps<typeof AccordionPrimitive.Context>) {
|
|
24
|
+
return <AccordionPrimitive.Context {...props} />
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function AccordionItem({ className, ...props }: React.ComponentProps<typeof AccordionPrimitive.Item>) {
|
|
28
|
+
return (
|
|
29
|
+
<AccordionPrimitive.Item data-slot="accordion-item" className={cn("not-last:border-b", className)} {...props} />
|
|
30
|
+
)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function AccordionItemContext({ ...props }: React.ComponentProps<typeof AccordionPrimitive.ItemContext>) {
|
|
34
|
+
return <AccordionPrimitive.ItemContext {...props} />
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function AccordionTrigger({
|
|
38
|
+
className,
|
|
39
|
+
children,
|
|
40
|
+
...props
|
|
41
|
+
}: React.ComponentProps<typeof AccordionPrimitive.ItemTrigger>) {
|
|
42
|
+
return (
|
|
43
|
+
<AccordionPrimitive.ItemTrigger
|
|
44
|
+
data-slot="accordion-trigger"
|
|
45
|
+
className={cn(
|
|
46
|
+
"group/accordion-trigger relative flex w-full flex-1 items-start justify-between rounded-lg border border-transparent py-2.5 text-left text-sm font-medium transition-all outline-none hover:underline focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 focus-visible:after:border-ring disabled:pointer-events-none disabled:opacity-50 **:data-[slot=accordion-trigger-icon]:ml-auto **:data-[slot=accordion-trigger-icon]:size-4 **:data-[slot=accordion-trigger-icon]:text-muted-foreground",
|
|
47
|
+
className
|
|
48
|
+
)}
|
|
49
|
+
{...props}
|
|
50
|
+
>
|
|
51
|
+
{children}
|
|
52
|
+
<ChevronDownIcon
|
|
53
|
+
data-slot="accordion-trigger-icon"
|
|
54
|
+
className="pointer-events-none shrink-0 group-aria-expanded/accordion-trigger:hidden"
|
|
55
|
+
/>
|
|
56
|
+
<ChevronUpIcon
|
|
57
|
+
data-slot="accordion-trigger-icon"
|
|
58
|
+
className="pointer-events-none hidden shrink-0 group-aria-expanded/accordion-trigger:inline"
|
|
59
|
+
/>
|
|
60
|
+
</AccordionPrimitive.ItemTrigger>
|
|
61
|
+
)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function AccordionItemIndicator({
|
|
65
|
+
className,
|
|
66
|
+
...props
|
|
67
|
+
}: React.ComponentProps<typeof AccordionPrimitive.ItemIndicator>) {
|
|
68
|
+
return (
|
|
69
|
+
<AccordionPrimitive.ItemIndicator
|
|
70
|
+
data-slot="accordion-item-indicator"
|
|
71
|
+
className={cn(
|
|
72
|
+
"ml-auto size-4 shrink-0 text-muted-foreground transition-transform data-open:rotate-180",
|
|
73
|
+
className
|
|
74
|
+
)}
|
|
75
|
+
{...props}
|
|
76
|
+
/>
|
|
77
|
+
)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function AccordionContent({
|
|
81
|
+
className,
|
|
82
|
+
children,
|
|
83
|
+
...props
|
|
84
|
+
}: React.ComponentProps<typeof AccordionPrimitive.ItemContent>) {
|
|
85
|
+
return (
|
|
86
|
+
<AccordionPrimitive.ItemContent
|
|
87
|
+
data-slot="accordion-content"
|
|
88
|
+
className="overflow-hidden text-sm [--accordion-panel-height:var(--height)] data-open:animate-accordion-down data-closed:animate-accordion-up"
|
|
89
|
+
{...props}
|
|
90
|
+
>
|
|
91
|
+
<div
|
|
92
|
+
className={cn(
|
|
93
|
+
"pt-0 pb-2.5 [&_a]:underline [&_a]:underline-offset-3 [&_a]:hover:text-foreground [&_p:not(:last-child)]:mb-4",
|
|
94
|
+
className
|
|
95
|
+
)}
|
|
96
|
+
>
|
|
97
|
+
{children}
|
|
98
|
+
</div>
|
|
99
|
+
</AccordionPrimitive.ItemContent>
|
|
100
|
+
)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export {
|
|
104
|
+
Accordion,
|
|
105
|
+
AccordionContent,
|
|
106
|
+
AccordionContext,
|
|
107
|
+
AccordionItem,
|
|
108
|
+
AccordionItemContext,
|
|
109
|
+
AccordionItemIndicator,
|
|
110
|
+
AccordionTrigger,
|
|
111
|
+
}
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
import { Button } from "@/components/ui/button"
|
|
3
|
+
import { formatRelativeTime } from "@/lib/time"
|
|
4
|
+
import { cn } from "@/lib/utils"
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* ActivityFeed — a compositional, data-agnostic timeline of events. The
|
|
8
|
+
* consumer maps its events to items; the feed adds the connector line, the
|
|
9
|
+
* marker column, relative times and the WAI-ARIA feed pattern (articles are
|
|
10
|
+
* focusable; PageUp/PageDown move between them, Ctrl+Home/End jump).
|
|
11
|
+
*
|
|
12
|
+
* Anatomy:
|
|
13
|
+
* ActivityFeed
|
|
14
|
+
* ActivityFeedGroup > ActivityFeedGroupLabel + ActivityFeedItems
|
|
15
|
+
* ActivityFeedItem value=…
|
|
16
|
+
* ActivityFeedItemMarker dot / icon / avatar on the line
|
|
17
|
+
* ActivityFeedItemContent
|
|
18
|
+
* ActivityFeedItemHeader > ActivityFeedItemActor … ActivityFeedItemTime date=…
|
|
19
|
+
* ActivityFeedItemBody
|
|
20
|
+
* ActivityFeedItemActions
|
|
21
|
+
* ActivityFeedLoadMoreTrigger / ActivityFeedEmpty
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
function ActivityFeed({ className, busy, onKeyDown, ...props }: React.ComponentProps<"div"> & { busy?: boolean }) {
|
|
25
|
+
const ref = React.useRef<HTMLDivElement>(null)
|
|
26
|
+
return (
|
|
27
|
+
<div
|
|
28
|
+
ref={ref}
|
|
29
|
+
data-slot="activity-feed"
|
|
30
|
+
role="feed"
|
|
31
|
+
aria-busy={busy || undefined}
|
|
32
|
+
className={cn("flex flex-col gap-6", className)}
|
|
33
|
+
onKeyDown={(event) => {
|
|
34
|
+
onKeyDown?.(event)
|
|
35
|
+
if (event.defaultPrevented) return
|
|
36
|
+
const articles = Array.from(ref.current?.querySelectorAll<HTMLElement>("[data-slot=activity-feed-item]") ?? [])
|
|
37
|
+
const current = (event.target as HTMLElement).closest<HTMLElement>("[data-slot=activity-feed-item]")
|
|
38
|
+
const index = current ? articles.indexOf(current) : -1
|
|
39
|
+
const meta = event.metaKey || event.ctrlKey
|
|
40
|
+
let next: number | null = null
|
|
41
|
+
if (event.key === "PageDown") next = Math.min(articles.length - 1, index + 1)
|
|
42
|
+
else if (event.key === "PageUp") next = Math.max(0, index - 1)
|
|
43
|
+
else if (event.key === "Home" && meta) next = 0
|
|
44
|
+
else if (event.key === "End" && meta) next = articles.length - 1
|
|
45
|
+
if (next === null || !articles[next]) return
|
|
46
|
+
event.preventDefault()
|
|
47
|
+
articles[next].focus()
|
|
48
|
+
}}
|
|
49
|
+
{...props}
|
|
50
|
+
/>
|
|
51
|
+
)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function ActivityFeedGroup({ className, ...props }: React.ComponentProps<"section">) {
|
|
55
|
+
return <section data-slot="activity-feed-group" className={cn("flex flex-col gap-3", className)} {...props} />
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function ActivityFeedGroupLabel({ className, ...props }: React.ComponentProps<"h3">) {
|
|
59
|
+
return (
|
|
60
|
+
<h3
|
|
61
|
+
data-slot="activity-feed-group-label"
|
|
62
|
+
className={cn("text-xs font-medium tracking-wide text-muted-foreground uppercase", className)}
|
|
63
|
+
{...props}
|
|
64
|
+
/>
|
|
65
|
+
)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** The list; draws the vertical connector behind the markers. */
|
|
69
|
+
function ActivityFeedItems({ className, ...props }: React.ComponentProps<"ol">) {
|
|
70
|
+
return (
|
|
71
|
+
<ol
|
|
72
|
+
data-slot="activity-feed-items"
|
|
73
|
+
className={cn(
|
|
74
|
+
"relative flex flex-col gap-5 before:absolute before:inset-y-2 before:inset-s-4 before:w-px before:-translate-x-1/2 before:bg-border",
|
|
75
|
+
className
|
|
76
|
+
)}
|
|
77
|
+
{...props}
|
|
78
|
+
/>
|
|
79
|
+
)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function ActivityFeedItem({ value, className, ...props }: React.ComponentProps<"li"> & { value: string }) {
|
|
83
|
+
return (
|
|
84
|
+
<li
|
|
85
|
+
data-slot="activity-feed-item"
|
|
86
|
+
data-value={value}
|
|
87
|
+
role="article"
|
|
88
|
+
tabIndex={0}
|
|
89
|
+
className={cn(
|
|
90
|
+
"group/activity relative flex gap-3 rounded-lg outline-none focus-visible:ring-2 focus-visible:ring-ring/50",
|
|
91
|
+
className
|
|
92
|
+
)}
|
|
93
|
+
{...props}
|
|
94
|
+
/>
|
|
95
|
+
)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Sits on the line: pass an icon, an Avatar, or nothing for a dot. */
|
|
99
|
+
function ActivityFeedItemMarker({ className, children, ...props }: React.ComponentProps<"div">) {
|
|
100
|
+
return (
|
|
101
|
+
<div
|
|
102
|
+
data-slot="activity-feed-item-marker"
|
|
103
|
+
className={cn(
|
|
104
|
+
"relative z-10 flex size-8 shrink-0 items-center justify-center rounded-full bg-background ring-4 ring-background",
|
|
105
|
+
"[&>svg]:size-4 [&>svg]:text-muted-foreground",
|
|
106
|
+
!children && "after:size-2 after:rounded-full after:bg-muted-foreground/60",
|
|
107
|
+
children && "border bg-muted",
|
|
108
|
+
className
|
|
109
|
+
)}
|
|
110
|
+
{...props}
|
|
111
|
+
>
|
|
112
|
+
{children}
|
|
113
|
+
</div>
|
|
114
|
+
)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function ActivityFeedItemContent({ className, ...props }: React.ComponentProps<"div">) {
|
|
118
|
+
return (
|
|
119
|
+
<div
|
|
120
|
+
data-slot="activity-feed-item-content"
|
|
121
|
+
className={cn("flex min-w-0 flex-1 flex-col gap-1.5 pt-1.5", className)}
|
|
122
|
+
{...props}
|
|
123
|
+
/>
|
|
124
|
+
)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function ActivityFeedItemHeader({ className, ...props }: React.ComponentProps<"div">) {
|
|
128
|
+
return (
|
|
129
|
+
<div
|
|
130
|
+
data-slot="activity-feed-item-header"
|
|
131
|
+
className={cn("flex flex-wrap items-baseline gap-x-1.5 gap-y-0.5 text-sm text-muted-foreground", className)}
|
|
132
|
+
{...props}
|
|
133
|
+
/>
|
|
134
|
+
)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function ActivityFeedItemActor({ className, ...props }: React.ComponentProps<"span">) {
|
|
138
|
+
return (
|
|
139
|
+
<span data-slot="activity-feed-item-actor" className={cn("font-medium text-foreground", className)} {...props} />
|
|
140
|
+
)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** `<time>` with a relative label by default; the full date is in the title. */
|
|
144
|
+
function ActivityFeedItemTime({
|
|
145
|
+
date,
|
|
146
|
+
now,
|
|
147
|
+
className,
|
|
148
|
+
children,
|
|
149
|
+
...props
|
|
150
|
+
}: Omit<React.ComponentProps<"time">, "dateTime"> & { date: Date; now?: Date }) {
|
|
151
|
+
return (
|
|
152
|
+
<time
|
|
153
|
+
data-slot="activity-feed-item-time"
|
|
154
|
+
dateTime={date.toISOString()}
|
|
155
|
+
title={date.toLocaleString()}
|
|
156
|
+
className={cn("text-xs whitespace-nowrap text-muted-foreground/80", className)}
|
|
157
|
+
{...props}
|
|
158
|
+
>
|
|
159
|
+
{children ?? formatRelativeTime(date, { now })}
|
|
160
|
+
</time>
|
|
161
|
+
)
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** Optional detail block: a quote, a diff, a preview card. */
|
|
165
|
+
function ActivityFeedItemBody({ className, ...props }: React.ComponentProps<"div">) {
|
|
166
|
+
return (
|
|
167
|
+
<div
|
|
168
|
+
data-slot="activity-feed-item-body"
|
|
169
|
+
className={cn("rounded-lg border bg-muted/40 px-3 py-2 text-sm", className)}
|
|
170
|
+
{...props}
|
|
171
|
+
/>
|
|
172
|
+
)
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function ActivityFeedItemActions({ className, ...props }: React.ComponentProps<"div">) {
|
|
176
|
+
return (
|
|
177
|
+
<div
|
|
178
|
+
data-slot="activity-feed-item-actions"
|
|
179
|
+
className={cn("flex items-center gap-1 text-xs", className)}
|
|
180
|
+
{...props}
|
|
181
|
+
/>
|
|
182
|
+
)
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function ActivityFeedLoadMoreTrigger({ asChild, children, className, ...props }: React.ComponentProps<typeof Button>) {
|
|
186
|
+
return (
|
|
187
|
+
<Button
|
|
188
|
+
data-slot="activity-feed-load-more-trigger"
|
|
189
|
+
variant="outline"
|
|
190
|
+
size="sm"
|
|
191
|
+
asChild={asChild}
|
|
192
|
+
className={cn("self-center", className)}
|
|
193
|
+
{...props}
|
|
194
|
+
>
|
|
195
|
+
{asChild ? children : (children ?? "Load older activity")}
|
|
196
|
+
</Button>
|
|
197
|
+
)
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function ActivityFeedEmpty({ className, ...props }: React.ComponentProps<"div">) {
|
|
201
|
+
return (
|
|
202
|
+
<div
|
|
203
|
+
data-slot="activity-feed-empty"
|
|
204
|
+
className={cn(
|
|
205
|
+
"flex h-32 flex-col items-center justify-center gap-1 rounded-lg border border-dashed text-sm text-muted-foreground",
|
|
206
|
+
className
|
|
207
|
+
)}
|
|
208
|
+
{...props}
|
|
209
|
+
/>
|
|
210
|
+
)
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
export {
|
|
214
|
+
ActivityFeed,
|
|
215
|
+
ActivityFeedGroup,
|
|
216
|
+
ActivityFeedGroupLabel,
|
|
217
|
+
ActivityFeedItems,
|
|
218
|
+
ActivityFeedItem,
|
|
219
|
+
ActivityFeedItemMarker,
|
|
220
|
+
ActivityFeedItemContent,
|
|
221
|
+
ActivityFeedItemHeader,
|
|
222
|
+
ActivityFeedItemActor,
|
|
223
|
+
ActivityFeedItemTime,
|
|
224
|
+
ActivityFeedItemBody,
|
|
225
|
+
ActivityFeedItemActions,
|
|
226
|
+
ActivityFeedLoadMoreTrigger,
|
|
227
|
+
ActivityFeedEmpty,
|
|
228
|
+
}
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
import { cn } from "@/lib/utils"
|
|
3
|
+
import { Dialog as AlertDialogPrimitive, Portal as PortalPrimitive } from "@ark-ui/react"
|
|
4
|
+
|
|
5
|
+
import { Button } from "@/components/ui/button"
|
|
6
|
+
|
|
7
|
+
function getDefaultInitialFocusEl() {
|
|
8
|
+
return document.querySelector<HTMLElement>(
|
|
9
|
+
'[data-slot="alert-dialog-content"][data-state="open"] [data-slot="alert-dialog-cancel"]'
|
|
10
|
+
)
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function AlertDialog({
|
|
14
|
+
role = "alertdialog",
|
|
15
|
+
closeOnInteractOutside = false,
|
|
16
|
+
initialFocusEl = getDefaultInitialFocusEl,
|
|
17
|
+
lazyMount = true,
|
|
18
|
+
unmountOnExit = true,
|
|
19
|
+
...props
|
|
20
|
+
}: React.ComponentProps<typeof AlertDialogPrimitive.Root>) {
|
|
21
|
+
return (
|
|
22
|
+
<AlertDialogPrimitive.Root
|
|
23
|
+
role={role}
|
|
24
|
+
closeOnInteractOutside={closeOnInteractOutside}
|
|
25
|
+
initialFocusEl={initialFocusEl}
|
|
26
|
+
lazyMount={lazyMount}
|
|
27
|
+
unmountOnExit={unmountOnExit}
|
|
28
|
+
{...props}
|
|
29
|
+
/>
|
|
30
|
+
)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function AlertDialogTrigger({ ...props }: React.ComponentProps<typeof AlertDialogPrimitive.Trigger>) {
|
|
34
|
+
return <AlertDialogPrimitive.Trigger data-slot="alert-dialog-trigger" {...props} />
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function AlertDialogPortal({ ...props }: React.ComponentProps<typeof PortalPrimitive>) {
|
|
38
|
+
return <PortalPrimitive {...props} />
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function AlertDialogContext({ ...props }: React.ComponentProps<typeof AlertDialogPrimitive.Context>) {
|
|
42
|
+
return <AlertDialogPrimitive.Context {...props} />
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function AlertDialogOverlay({ className, ...props }: React.ComponentProps<typeof AlertDialogPrimitive.Backdrop>) {
|
|
46
|
+
return (
|
|
47
|
+
<AlertDialogPrimitive.Backdrop
|
|
48
|
+
data-slot="alert-dialog-overlay"
|
|
49
|
+
className={cn(
|
|
50
|
+
"fixed inset-0 z-50 bg-black/10 duration-100 supports-backdrop-filter:backdrop-blur-xs data-open:animate-in data-open:fade-in-0 data-closed:animate-out data-closed:fade-out-0",
|
|
51
|
+
className
|
|
52
|
+
)}
|
|
53
|
+
{...props}
|
|
54
|
+
/>
|
|
55
|
+
)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function AlertDialogPositioner({ className, ...props }: React.ComponentProps<typeof AlertDialogPrimitive.Positioner>) {
|
|
59
|
+
return (
|
|
60
|
+
<AlertDialogPrimitive.Positioner
|
|
61
|
+
data-slot="alert-dialog-positioner"
|
|
62
|
+
className={cn("fixed inset-0 z-50 flex items-center justify-center", className)}
|
|
63
|
+
{...props}
|
|
64
|
+
/>
|
|
65
|
+
)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function AlertDialogContent({
|
|
69
|
+
className,
|
|
70
|
+
size = "default",
|
|
71
|
+
...props
|
|
72
|
+
}: React.ComponentProps<typeof AlertDialogPrimitive.Content> & {
|
|
73
|
+
size?: "default" | "sm"
|
|
74
|
+
}) {
|
|
75
|
+
return (
|
|
76
|
+
<AlertDialogPortal>
|
|
77
|
+
<AlertDialogOverlay />
|
|
78
|
+
<AlertDialogPositioner>
|
|
79
|
+
<AlertDialogPrimitive.Content
|
|
80
|
+
data-slot="alert-dialog-content"
|
|
81
|
+
data-size={size}
|
|
82
|
+
className={cn(
|
|
83
|
+
"group/alert-dialog-content relative grid w-full gap-4 rounded-xl bg-popover p-4 text-popover-foreground ring-1 ring-foreground/10 duration-100 outline-none data-[size=default]:max-w-xs data-[size=sm]:max-w-xs data-[size=default]:sm:max-w-sm 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",
|
|
84
|
+
className
|
|
85
|
+
)}
|
|
86
|
+
{...props}
|
|
87
|
+
/>
|
|
88
|
+
</AlertDialogPositioner>
|
|
89
|
+
</AlertDialogPortal>
|
|
90
|
+
)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function AlertDialogHeader({ className, ...props }: React.ComponentProps<"div">) {
|
|
94
|
+
return (
|
|
95
|
+
<div
|
|
96
|
+
data-slot="alert-dialog-header"
|
|
97
|
+
className={cn(
|
|
98
|
+
"grid grid-rows-[auto_1fr] place-items-center gap-1.5 text-center has-data-[slot=alert-dialog-media]:grid-rows-[auto_auto_1fr] has-data-[slot=alert-dialog-media]:gap-x-4 sm:group-data-[size=default]/alert-dialog-content:place-items-start sm:group-data-[size=default]/alert-dialog-content:text-left sm:group-data-[size=default]/alert-dialog-content:has-data-[slot=alert-dialog-media]:grid-rows-[auto_1fr]",
|
|
99
|
+
className
|
|
100
|
+
)}
|
|
101
|
+
{...props}
|
|
102
|
+
/>
|
|
103
|
+
)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function AlertDialogFooter({ className, ...props }: React.ComponentProps<"div">) {
|
|
107
|
+
return (
|
|
108
|
+
<div
|
|
109
|
+
data-slot="alert-dialog-footer"
|
|
110
|
+
className={cn(
|
|
111
|
+
"-mx-4 -mb-4 flex flex-col-reverse gap-2 rounded-b-xl border-t bg-muted/50 p-4 group-data-[size=sm]/alert-dialog-content:grid group-data-[size=sm]/alert-dialog-content:grid-cols-2 sm:flex-row sm:justify-end",
|
|
112
|
+
className
|
|
113
|
+
)}
|
|
114
|
+
{...props}
|
|
115
|
+
/>
|
|
116
|
+
)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function AlertDialogMedia({ className, ...props }: React.ComponentProps<"div">) {
|
|
120
|
+
return (
|
|
121
|
+
<div
|
|
122
|
+
data-slot="alert-dialog-media"
|
|
123
|
+
className={cn(
|
|
124
|
+
"mb-2 inline-flex size-10 items-center justify-center rounded-md bg-muted sm:group-data-[size=default]/alert-dialog-content:row-span-2 *:[svg:not([class*='size-'])]:size-6",
|
|
125
|
+
className
|
|
126
|
+
)}
|
|
127
|
+
{...props}
|
|
128
|
+
/>
|
|
129
|
+
)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function AlertDialogTitle({ className, ...props }: React.ComponentProps<typeof AlertDialogPrimitive.Title>) {
|
|
133
|
+
return (
|
|
134
|
+
<AlertDialogPrimitive.Title
|
|
135
|
+
data-slot="alert-dialog-title"
|
|
136
|
+
className={cn(
|
|
137
|
+
"font-heading text-base font-medium sm:group-data-[size=default]/alert-dialog-content:group-has-data-[slot=alert-dialog-media]/alert-dialog-content:col-start-2",
|
|
138
|
+
className
|
|
139
|
+
)}
|
|
140
|
+
{...props}
|
|
141
|
+
/>
|
|
142
|
+
)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function AlertDialogDescription({
|
|
146
|
+
className,
|
|
147
|
+
...props
|
|
148
|
+
}: React.ComponentProps<typeof AlertDialogPrimitive.Description>) {
|
|
149
|
+
return (
|
|
150
|
+
<AlertDialogPrimitive.Description
|
|
151
|
+
data-slot="alert-dialog-description"
|
|
152
|
+
className={cn(
|
|
153
|
+
"text-sm text-balance text-muted-foreground md:text-pretty *:[a]:underline *:[a]:underline-offset-3 *:[a]:hover:text-foreground",
|
|
154
|
+
className
|
|
155
|
+
)}
|
|
156
|
+
{...props}
|
|
157
|
+
/>
|
|
158
|
+
)
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function AlertDialogAction({
|
|
162
|
+
className,
|
|
163
|
+
variant = "default",
|
|
164
|
+
size = "default",
|
|
165
|
+
...props
|
|
166
|
+
}: React.ComponentProps<typeof AlertDialogPrimitive.CloseTrigger> &
|
|
167
|
+
Pick<React.ComponentProps<typeof Button>, "variant" | "size">) {
|
|
168
|
+
return (
|
|
169
|
+
<Button variant={variant} size={size} asChild>
|
|
170
|
+
<AlertDialogPrimitive.CloseTrigger data-slot="alert-dialog-action" className={cn(className)} {...props} />
|
|
171
|
+
</Button>
|
|
172
|
+
)
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function AlertDialogCancel({
|
|
176
|
+
className,
|
|
177
|
+
variant = "outline",
|
|
178
|
+
size = "default",
|
|
179
|
+
...props
|
|
180
|
+
}: React.ComponentProps<typeof AlertDialogPrimitive.CloseTrigger> &
|
|
181
|
+
Pick<React.ComponentProps<typeof Button>, "variant" | "size">) {
|
|
182
|
+
return (
|
|
183
|
+
<Button variant={variant} size={size} asChild>
|
|
184
|
+
<AlertDialogPrimitive.CloseTrigger data-slot="alert-dialog-cancel" className={cn(className)} {...props} />
|
|
185
|
+
</Button>
|
|
186
|
+
)
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export {
|
|
190
|
+
AlertDialog,
|
|
191
|
+
AlertDialogAction,
|
|
192
|
+
AlertDialogCancel,
|
|
193
|
+
AlertDialogContent,
|
|
194
|
+
AlertDialogContext,
|
|
195
|
+
AlertDialogDescription,
|
|
196
|
+
AlertDialogFooter,
|
|
197
|
+
AlertDialogHeader,
|
|
198
|
+
AlertDialogMedia,
|
|
199
|
+
AlertDialogOverlay,
|
|
200
|
+
AlertDialogPortal,
|
|
201
|
+
AlertDialogPositioner,
|
|
202
|
+
AlertDialogTitle,
|
|
203
|
+
AlertDialogTrigger,
|
|
204
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
import { cva, type VariantProps } from "class-variance-authority"
|
|
3
|
+
import { cn } from "@/lib/utils"
|
|
4
|
+
|
|
5
|
+
const alertVariants = cva(
|
|
6
|
+
"group/alert relative grid w-full gap-0.5 rounded-lg border px-2.5 py-2 text-left text-sm has-data-[slot=alert-action]:relative has-data-[slot=alert-action]:pr-18 has-[>svg]:grid-cols-[auto_1fr] has-[>svg]:gap-x-2 *:[svg]:row-span-2 *:[svg]:translate-y-0.5 *:[svg]:text-current *:[svg:not([class*='size-'])]:size-4",
|
|
7
|
+
{
|
|
8
|
+
variants: {
|
|
9
|
+
variant: {
|
|
10
|
+
default: "bg-card text-card-foreground",
|
|
11
|
+
destructive:
|
|
12
|
+
"bg-card text-destructive *:data-[slot=alert-description]:text-destructive/90 *:[svg]:text-current",
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
defaultVariants: {
|
|
16
|
+
variant: "default",
|
|
17
|
+
},
|
|
18
|
+
}
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
function Alert({ className, variant, ...props }: React.ComponentProps<"div"> & VariantProps<typeof alertVariants>) {
|
|
22
|
+
return <div data-slot="alert" role="alert" className={cn(alertVariants({ variant }), className)} {...props} />
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function AlertTitle({ className, ...props }: React.ComponentProps<"div">) {
|
|
26
|
+
return (
|
|
27
|
+
<div
|
|
28
|
+
data-slot="alert-title"
|
|
29
|
+
className={cn(
|
|
30
|
+
"font-medium group-has-[>svg]/alert:col-start-2 [&_a]:underline [&_a]:underline-offset-3 [&_a]:hover:text-foreground",
|
|
31
|
+
className
|
|
32
|
+
)}
|
|
33
|
+
{...props}
|
|
34
|
+
/>
|
|
35
|
+
)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function AlertDescription({ className, ...props }: React.ComponentProps<"div">) {
|
|
39
|
+
return (
|
|
40
|
+
<div
|
|
41
|
+
data-slot="alert-description"
|
|
42
|
+
className={cn(
|
|
43
|
+
"text-sm text-balance text-muted-foreground md:text-pretty [&_a]:underline [&_a]:underline-offset-3 [&_a]:hover:text-foreground [&_p:not(:last-child)]:mb-4",
|
|
44
|
+
className
|
|
45
|
+
)}
|
|
46
|
+
{...props}
|
|
47
|
+
/>
|
|
48
|
+
)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function AlertAction({ className, ...props }: React.ComponentProps<"div">) {
|
|
52
|
+
return <div data-slot="alert-action" className={cn("absolute top-2 right-2", className)} {...props} />
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export { Alert, AlertTitle, AlertDescription, AlertAction }
|