@pihanga2/shadcn 0.1.0 → 0.2.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 (66) hide show
  1. package/AGENT.building-cards.md +413 -0
  2. package/AGENT.md +50 -0
  3. package/AGENT.using-cards.md +1242 -0
  4. package/cards/box/box.types.d.ts +1 -4
  5. package/cards/box/box.types.js.map +1 -1
  6. package/cards/core-index.js +6 -6
  7. package/cards/fileDrop/fileDrop.component.d.ts +5 -0
  8. package/cards/fileDrop/fileDrop.types.d.ts +43 -0
  9. package/cards/fileDrop/index.d.ts +2 -0
  10. package/cards/icons.js +7 -3
  11. package/cards/icons.js.map +1 -1
  12. package/cards/index.d.ts +1 -0
  13. package/cards/stack/stack.types.d.ts +19 -22
  14. package/cards/stack/stack.types.js +1 -1
  15. package/cards/stack/stack.types.js.map +1 -1
  16. package/cards/tabs/index.js +4 -4
  17. package/cards/tabs/tabs.types.d.ts +2 -0
  18. package/cards/tabs/tabs.types.js +2 -2
  19. package/cards/tabs/tabs.types.js.map +1 -1
  20. package/cards/types.d.ts +4 -0
  21. package/cards/types.js.map +1 -1
  22. package/package.json +9 -1
  23. package/pihanga-shadcn.css +1 -1
  24. package/mountain-snow.svg +0 -4
  25. package/r/badge.json +0 -35
  26. package/r/box.json +0 -26
  27. package/r/button.json +0 -39
  28. package/r/checkbox.json +0 -31
  29. package/r/conditional.json +0 -30
  30. package/r/dataTable.json +0 -43
  31. package/r/dialog.json +0 -36
  32. package/r/dropDownMenu.json +0 -47
  33. package/r/field.json +0 -35
  34. package/r/flexGrid.json +0 -32
  35. package/r/form.json +0 -37
  36. package/r/framework.json +0 -28
  37. package/r/graphin.json +0 -42
  38. package/r/input.json +0 -35
  39. package/r/jsonViewer.json +0 -40
  40. package/r/list.json +0 -39
  41. package/r/loadingOverlay.json +0 -39
  42. package/r/loadingSkeleton.json +0 -30
  43. package/r/markdownViewer.json +0 -51
  44. package/r/menu.json +0 -34
  45. package/r/modeToggle.json +0 -36
  46. package/r/navbarSearch.json +0 -31
  47. package/r/pageWithNavbar.json +0 -46
  48. package/r/pasteTarget.json +0 -41
  49. package/r/pihanga-base.json +0 -11
  50. package/r/pihanga-cards-icons.json +0 -16
  51. package/r/pihanga-cards-types.json +0 -16
  52. package/r/pihanga-hook-use-is-touch-device.json +0 -15
  53. package/r/pihanga-lib-utils.json +0 -18
  54. package/r/pihanga-theme-provider.json +0 -25
  55. package/r/pihanga-ui-extras.json +0 -68
  56. package/r/registry.json +0 -171
  57. package/r/resizable.json +0 -35
  58. package/r/select.json +0 -35
  59. package/r/stack.json +0 -27
  60. package/r/stepper.json +0 -39
  61. package/r/switch.json +0 -36
  62. package/r/tabs.json +0 -33
  63. package/r/textField.json +0 -32
  64. package/r/toast.json +0 -32
  65. package/r/toggleGroup.json +0 -35
  66. package/r/typography.json +0 -31
@@ -1,46 +0,0 @@
1
- {
2
- "$schema": "https://ui.shadcn.com/schema/registry-item.json",
3
- "name": "pageWithNavbar",
4
- "type": "registry:component",
5
- "description": "Pihanga pageWithNavbar card component",
6
- "dependencies": [
7
- "@radix-ui/react-dialog@^1.1.15",
8
- "class-variance-authority@^0.7.1",
9
- "clsx@^2.1.1",
10
- "lucide-react@^0.513.0",
11
- "radix-ui@^1.4.3"
12
- ],
13
- "registryDependencies": [
14
- "https://ivcap-works.github.io/pihanga-shadcn/r/pihanga-base.json",
15
- "https://ivcap-works.github.io/pihanga-shadcn/r/pihanga-ui-extras.json",
16
- "sheet",
17
- "https://ivcap-works.github.io/pihanga-shadcn/r/pihanga-cards-icons.json",
18
- "https://ivcap-works.github.io/pihanga-shadcn/r/pihanga-cards-types.json"
19
- ],
20
- "files": [
21
- {
22
- "path": "cards/pageWithNavbar/index.ts",
23
- "content": "import {\n actionTypesToEvents,\n createCardDeclaration,\n registerCardComponent,\n} from \"@pihanga2/core\";\n\nimport {Component} from \"./pageWithNavbar.component\";\nimport {PAGE_WITH_NAVBAR_ACTION} from \"./pageWithNavbar.type\";\n\nexport * from \"./pageWithNavbar.type\";\n\nconst CARD_NAME = \"shad/pageWithNavbar\";\n\nexport const SdPageWithNavbar = createCardDeclaration(CARD_NAME);\n\nregisterCardComponent({\n name: CARD_NAME,\n component: Component,\n events: actionTypesToEvents(PAGE_WITH_NAVBAR_ACTION),\n});\n",
24
- "type": "registry:component",
25
- "target": "src/cards/pageWithNavbar/index.ts"
26
- },
27
- {
28
- "path": "cards/pageWithNavbar/pageWithNavbar.component.tsx",
29
- "content": "import {Menu} from \"lucide-react\";\nimport clsx from \"clsx\";\nimport {useEffect, useRef, useState} from \"react\";\n\nimport {Button} from \"@/components/ui/button\";\nimport {Sheet, SheetContent, SheetTrigger} from \"@/components/ui/sheet\";\nimport {Card, type PiCardProps} from \"@pihanga2/core\";\nimport {getIconElement} from \"@/cards/icons\";\nimport {ScreenSize, SCREEN_SIZE_ORDER, SCREEN_SIZE_WIDTHS} from \"@/cards/types\";\nimport type {\n PageWithNavbarEvents,\n PageWithNavbarProps,\n ResponsiveCardRef,\n} from \"./pageWithNavbar.type\";\nimport type {SizeMap} from \"@/cards/types\";\nimport type {PiCardRef} from \"@pihanga2/core\";\n\nimport \"./pageWithNavbar.css\";\n\n// ---------------------------------------------------------------------------\n// Style override type — consumers can extend this to add Tailwind class names\n// to any structural section of the layout.\n// ---------------------------------------------------------------------------\n\nexport type ShadStyle = {\n shad?: {\n window?: string;\n page?: string;\n header?: string;\n main?: string;\n footer?: string;\n };\n};\n\n// ---------------------------------------------------------------------------\n// Screen-size hook\n// ---------------------------------------------------------------------------\n\nfunction getCurrentScreenSize(): ScreenSize {\n const w = typeof window !== \"undefined\" ? window.innerWidth : 0;\n // Walk breakpoints from largest to smallest\n for (let i = SCREEN_SIZE_ORDER.length - 1; i >= 0; i--) {\n const size = SCREEN_SIZE_ORDER[i];\n if (w >= SCREEN_SIZE_WIDTHS[size]) return size;\n }\n return ScreenSize.XS;\n}\n\nfunction useScreenSize(): ScreenSize {\n const [size, setSize] = useState<ScreenSize>(getCurrentScreenSize);\n\n useEffect(() => {\n const handleResize = () => setSize(getCurrentScreenSize());\n window.addEventListener(\"resize\", handleResize);\n return () => window.removeEventListener(\"resize\", handleResize);\n }, []);\n\n return size;\n}\n\n// ---------------------------------------------------------------------------\n// Responsive card-ref resolution\n// ---------------------------------------------------------------------------\n\nfunction isSizeMap(ref: ResponsiveCardRef): ref is SizeMap<PiCardRef> {\n return typeof ref === \"object\" && ref !== null;\n}\n\n/**\n * Resolve a {@link ResponsiveCardRef} to a concrete `PiCardRef` (or\n * `undefined`) for the given screen size.\n *\n * Uses a **mobile-first cascade**: the entry with the largest breakpoint\n * that is still ≤ `currentSize` wins; falls back to `default` otherwise.\n */\nfunction resolveCardRef(\n ref: ResponsiveCardRef | undefined,\n currentSize: ScreenSize,\n): PiCardRef | undefined {\n if (!ref) return undefined;\n if (!isSizeMap(ref)) return ref; // plain PiCardRef (string or PiCardDef)\n\n // Cascade: find the highest breakpoint that is ≤ current size\n const currentIdx = SCREEN_SIZE_ORDER.indexOf(currentSize);\n for (let i = currentIdx; i >= 0; i--) {\n const size = SCREEN_SIZE_ORDER[i];\n if (Object.prototype.hasOwnProperty.call(ref, size)) {\n const val = ref[size];\n // null means \"explicitly hidden at this breakpoint\"\n return val ?? undefined;\n }\n }\n // Fall back to the default entry\n return ref.default ?? undefined;\n}\n\n// ---------------------------------------------------------------------------\n// Component\n// ---------------------------------------------------------------------------\n\nexport const Component = (\n props: PiCardProps<PageWithNavbarProps<ShadStyle>, PageWithNavbarEvents>,\n): React.ReactNode => {\n const {\n title,\n iconName,\n main,\n footer,\n navLinks = [],\n scrollResetKey,\n headerLeftCard,\n headerRightCard,\n style,\n onNavigateTo,\n cardName,\n } = props;\n\n const screenSize = useScreenSize();\n const cl = style?.shad ?? {};\n const mainRef = useRef<HTMLElement | null>(null);\n\n // Scroll main back to top when the logical \"page\" changes\n useEffect(() => {\n mainRef.current?.scrollTo({top: 0, behavior: \"auto\"});\n }, [scrollResetKey]);\n\n const resolvedLeftCard = resolveCardRef(headerLeftCard, screenSize);\n const resolvedRightCard = resolveCardRef(headerRightCard, screenSize);\n const hasNavLinks = navLinks.length > 0;\n\n // ------------------------------------------------------------------\n // Sub-components\n // ------------------------------------------------------------------\n\n /** Title section: optional icon + text */\n function TitleSection({className}: {className?: string}) {\n const IconEl = iconName ? getIconElement(iconName) : undefined;\n return (\n <div className={clsx(\"title-section\", className)}>\n {IconEl && <IconEl className=\"title-icon\" aria-hidden />}\n <span className=\"title-text\">{title}</span>\n </div>\n );\n }\n\n /** Nav bar rendered on medium screens and above */\n function NavMd() {\n return (\n <nav className=\"nav-md\" aria-label=\"Main navigation\">\n <TitleSection />\n {navLinks.map((el) => (\n <Button\n key={el.id}\n onClick={() => onNavigateTo({id: el.id})}\n variant=\"link\"\n className=\"nav-link\"\n >\n {el.title ?? capitalize(el.id)}\n </Button>\n ))}\n </nav>\n );\n }\n\n /** Hamburger trigger that opens a slide-out sheet on small screens */\n function NavSm() {\n return (\n <Sheet>\n <SheetTrigger asChild>\n <Button\n variant=\"outline\"\n size=\"icon\"\n className=\"shrink-0 md:hidden\"\n aria-label=\"Toggle navigation menu\"\n >\n <Menu className=\"h-5 w-5\" aria-hidden />\n </Button>\n </SheetTrigger>\n <SheetContent side=\"left\">\n <nav className=\"nav-sm\" aria-label=\"Mobile navigation\">\n <TitleSection />\n {navLinks.map((el) => (\n <Button\n key={el.id}\n onClick={() => onNavigateTo({id: el.id})}\n variant=\"link\"\n className=\"nav-link\"\n >\n {el.title ?? capitalize(el.id)}\n </Button>\n ))}\n </nav>\n </SheetContent>\n </Sheet>\n );\n }\n\n // ------------------------------------------------------------------\n // Layout\n // ------------------------------------------------------------------\n\n return (\n <div className={clsx(\"window\", cl.window)} data-pihanga={cardName}>\n <div className={clsx(\"page\", cl.page)}>\n {/* ── Header ─────────────────────────────────────────────── */}\n <header className={clsx(\"header\", cl.header)}>\n {/* Medium+ nav (title + links) */}\n <NavMd />\n\n {/* Small-screen hamburger (only when there are nav links) */}\n {hasNavLinks && <NavSm />}\n\n {/* On small screens with no nav links, show the title inline.\n Wrap in a plain div so md:hidden isn't overridden by the\n .title-section CSS class that sets display:inline-flex. */}\n {!hasNavLinks && (\n <div className=\"md:hidden\">\n <TitleSection />\n </div>\n )}\n\n {/* Push action cards to the right */}\n <div className=\"header-actions\">\n {resolvedLeftCard && (\n <Card cardName={resolvedLeftCard} parentCard={cardName} />\n )}\n {resolvedRightCard && (\n <Card cardName={resolvedRightCard} parentCard={cardName} />\n )}\n </div>\n </header>\n\n {/* ── Main ───────────────────────────────────────────────── */}\n <main className={clsx(\"main\", cl.main)} ref={mainRef}>\n <Card cardName={main} parentCard={cardName} />\n </main>\n\n {/* ── Footer (optional) ──────────────────────────────────── */}\n {footer && (\n <footer className={clsx(\"footer\", cl.footer)}>\n <Card cardName={footer} parentCard={cardName} />\n </footer>\n )}\n </div>\n </div>\n );\n};\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\nfunction capitalize(str: string): string {\n if (!str) return \"\";\n return str.charAt(0).toLocaleUpperCase() + str.slice(1);\n}\n",
30
- "type": "registry:component",
31
- "target": "src/cards/pageWithNavbar/pageWithNavbar.component.tsx"
32
- },
33
- {
34
- "path": "cards/pageWithNavbar/pageWithNavbar.css",
35
- "content": "@import \"tailwindcss\";\n\n/* ============================================================\n Outermost wrapper — centres the page horizontally\n ============================================================ */\n.window {\n @apply flex w-full justify-center;\n}\n\n/* ============================================================\n Page grid: header / main / footer (footer row is auto so it\n collapses gracefully when the footer element is absent)\n ============================================================ */\n.page {\n @apply w-full min-h-screen h-screen;\n display: grid;\n grid-template-areas:\n \"header\"\n \"main\"\n \"footer\";\n grid-template-rows: 4rem 1fr auto;\n grid-template-columns: 100%;\n}\n\n/* ============================================================\n Header\n ============================================================ */\n.header {\n @apply flex items-center gap-4 border-b bg-(--background) px-4 md:px-6;\n grid-area: header;\n}\n\n/* Title section (icon + text) */\n.title-section {\n @apply inline-flex items-center gap-2;\n}\n\n.title-section .title-icon {\n @apply h-6 w-6 shrink-0;\n}\n\n.title-section .title-text {\n @apply text-lg font-semibold text-(--foreground);\n}\n\n/* Medium-and-above horizontal nav */\n.nav-md {\n @apply hidden md:flex flex-row items-center gap-5 lg:gap-6;\n\n .nav-link {\n @apply text-(--muted-foreground) transition-colors hover:text-(--foreground) px-0;\n }\n}\n\n/* Small-screen slide-out nav content */\n.nav-sm {\n @apply grid gap-6 text-lg font-medium p-4;\n\n .nav-link {\n @apply text-(--muted-foreground) hover:text-(--foreground) justify-start px-0 text-lg;\n }\n}\n\n/* Right-aligned action cards area */\n.header-actions {\n @apply ml-auto flex items-center gap-2 lg:gap-4;\n}\n\n/* ============================================================\n Main — scrollable content area\n ============================================================ */\n.main {\n @apply bg-(--background) p-4 py-2 md:p-8 md:py-4;\n grid-area: main;\n overflow-y: auto;\n min-height: 0;\n}\n\n/* ============================================================\n Footer (optional)\n ============================================================ */\n.footer {\n @apply bg-(--background) text-(--muted-foreground) px-4 py-2 md:px-8 md:py-4;\n grid-area: footer;\n}",
36
- "type": "registry:component",
37
- "target": "src/cards/pageWithNavbar/pageWithNavbar.css"
38
- },
39
- {
40
- "path": "cards/pageWithNavbar/pageWithNavbar.type.ts",
41
- "content": "import {\n createCardDeclaration,\n createOnAction,\n type PiCardRef,\n registerActions,\n} from \"@pihanga2/core\";\nimport type {SizeMap} from \"@/cards/types\";\n\nexport const PAGE_WITH_NAVBAR_CARD = \"pageWithNavbar\";\nexport const PageWithNavbar = createCardDeclaration<\n PageWithNavbarProps,\n PageWithNavbarEvents\n>(PAGE_WITH_NAVBAR_CARD);\n\nexport const PAGE_WITH_NAVBAR_ACTION = registerActions(PAGE_WITH_NAVBAR_CARD, [\n \"navigate_to\",\n]);\n\nexport const onPageWithNavbarNavigateTo =\n createOnAction<PageWithNavbarNavigateToEvent>(\n PAGE_WITH_NAVBAR_ACTION.NAVIGATE_TO,\n );\n\n/**\n * A card reference that can optionally vary by screen size.\n *\n * Pass a plain `PiCardRef` string to always show the same card, or a\n * {@link SizeMap} to show different cards (or none) at each breakpoint.\n *\n * @example\n * // Always show the same card\n * headerRightCard: \"myAccountCard\"\n *\n * // Show on md and above only\n * headerRightCard: { [ScreenSize.MD]: \"myAccountCard\" }\n *\n * // Different card per breakpoint\n * headerLeftCard: {\n * [ScreenSize.XS]: \"mobileSearchCard\",\n * [ScreenSize.MD]: \"desktopSearchCard\",\n * }\n */\nexport type ResponsiveCardRef = PiCardRef | SizeMap<PiCardRef>;\n\nexport type PageWithNavbarProps<S = unknown> = {\n /** Page title displayed in the header. */\n title: string;\n\n /**\n * Name of an icon registered via {@link registerIcon} in `icons.ts`.\n * When provided, the icon is rendered next to the title in the header.\n */\n iconName?: string;\n\n /** The main content card. */\n main: PiCardRef;\n\n /**\n * Optional footer card.\n * When omitted the footer row is not rendered.\n */\n footer?: PiCardRef;\n\n /**\n * Navigation links rendered in the header nav bar.\n * On small screens they are collapsed behind a hamburger menu.\n */\n navLinks?: NavLink[];\n\n /**\n * When this value changes the main content area scrolls back to the top.\n * Set it to any string that is unique per logical \"page load\" — typically\n * the URL or ID of the currently loaded resource (e.g. \"foo.xml\").\n */\n scrollResetKey?: string;\n\n /**\n * Optional card shown on the left side of the header actions area.\n * Supports size-dependent rendering via {@link ResponsiveCardRef}.\n */\n headerLeftCard?: ResponsiveCardRef;\n\n /**\n * Optional card shown on the right side of the header actions area.\n * Supports size-dependent rendering via {@link ResponsiveCardRef}.\n */\n headerRightCard?: ResponsiveCardRef;\n\n /** Additional style overrides passed through to the component. */\n style?: S;\n\n /** Additional CSS class names applied to the outermost wrapper. */\n className?: string;\n};\n\nexport type NavLink = {\n id: string;\n title?: string;\n};\n\nexport type PageWithNavbarNavigateToEvent = {id: string};\n\nexport type PageWithNavbarEvents = {\n onNavigateTo: PageWithNavbarNavigateToEvent;\n};\n",
42
- "type": "registry:component",
43
- "target": "src/cards/pageWithNavbar/pageWithNavbar.type.ts"
44
- }
45
- ]
46
- }
@@ -1,41 +0,0 @@
1
- {
2
- "$schema": "https://ui.shadcn.com/schema/registry-item.json",
3
- "name": "pasteTarget",
4
- "type": "registry:component",
5
- "description": "Pihanga pasteTarget card component",
6
- "dependencies": [
7
- "class-variance-authority@^0.7.1",
8
- "clsx@^2.1.1",
9
- "radix-ui@^1.4.3"
10
- ],
11
- "registryDependencies": [
12
- "https://ivcap-works.github.io/pihanga-shadcn/r/pihanga-base.json",
13
- "https://ivcap-works.github.io/pihanga-shadcn/r/pihanga-ui-extras.json"
14
- ],
15
- "files": [
16
- {
17
- "path": "cards/pasteTarget/index.ts",
18
- "content": "import {actionTypesToEvents, registerCardComponent} from \"@pihanga2/core\";\n\nimport {PasteTargetComponent} from \"./pasteTarget.component\";\nimport {PASTE_TARGET_ACTION, PASTE_TARGET_CARD} from \"./pasteTarget.types\";\n\nexport * from \"./pasteTarget.types\";\n\nregisterCardComponent({\n name: PASTE_TARGET_CARD,\n component: PasteTargetComponent,\n events: actionTypesToEvents(PASTE_TARGET_ACTION),\n});\n",
19
- "type": "registry:component",
20
- "target": "src/cards/pasteTarget/index.ts"
21
- },
22
- {
23
- "path": "cards/pasteTarget/pasteTarget.component.tsx",
24
- "content": "import {\n DEF_ERROR_COLOR,\n DEF_EVENT_DURATION_SECONDS,\n DEF_HEIGHT,\n DEF_OVERLAY_OPACITY,\n DEF_PASTE_FIRST_REMINDER,\n DEF_SUCCESS_COLOR,\n PasteItem,\n PasteTargetEvents,\n PasteTargetProps,\n} from \"@/cards/pasteTarget/pasteTarget.types\";\nimport {PiCardProps} from \"@pihanga2/core\";\nimport React, {useRef, useState} from \"react\";\n\nimport \"./pasteTarget.css\";\nimport clsx from \"clsx\";\nimport {Button} from \"@/components/ui/button\";\n\n// /* 3. Stretch the child to cover the parent entirely */\n// top: 0,\n// left: 0,\n// right: 0,\n// bottom: 0,\n\n// /* 4. Ensure the child is visually stacked above the parent's content */\n// z-index: 10,\n\n// /* Optional: Make the overlay visible (e.g., semi-transparent black) */\n// overlayOpacity: \"rgba(0, 0, 0, 0.5)\";\n// }\n\nexport const PasteTargetComponent = (\n props: PiCardProps<PasteTargetProps, PasteTargetEvents>,\n): React.ReactNode => {\n const {\n title, // = \"Paste content right here\",\n description,\n pasteFirstReminder = DEF_PASTE_FIRST_REMINDER,\n withUpload,\n\n height = DEF_HEIGHT,\n\n onPastedContent,\n\n successColor = DEF_SUCCESS_COLOR,\n errorColor = DEF_ERROR_COLOR,\n eventDurationSeconds = DEF_EVENT_DURATION_SECONDS,\n eventOpacity = DEF_OVERLAY_OPACITY,\n cardName,\n className,\n _cls,\n } = props;\n const [overlayOpacity, setOverlayOpacity] = useState<number>(0);\n const [overlayBackground, setOverlayBackground] = useState<string>(\"gray\");\n const timerRef = React.useRef<number | null>(null);\n const textareaRef = useRef<HTMLTextAreaElement>(null);\n const [isFocused, setIsFocused] = useState<boolean>(false);\n\n const overlayStyle: React.CSSProperties = {\n backgroundColor: overlayBackground,\n opacity: overlayOpacity,\n };\n\n const handlePaste = (event: React.ClipboardEvent<HTMLTextAreaElement>) => {\n if (timerRef.current) {\n clearTimeout(timerRef.current);\n }\n\n event.preventDefault();\n const clipboardData =\n event.clipboardData ||\n (window as Window & {clipboardData?: DataTransfer}).clipboardData;\n\n if (clipboardData) {\n const pItems = clipboardData.items;\n const items: PasteItem[] = [];\n for (let i = 0; i < pItems.length; i++) {\n const pi = pItems[i];\n const mimeType = pi.type;\n const content = clipboardData.getData(pi.type);\n if (!(content == \"\" || content == \"\\n\")) {\n items.push({mimeType, content});\n }\n }\n onPastedContent({items});\n setOverlayBackground(successColor);\n } else {\n setOverlayBackground(errorColor);\n }\n setOverlayOpacity(eventOpacity);\n if (textareaRef.current) textareaRef.current.blur();\n\n // Set a timeout to switch the color back after 'n' seconds\n const newTimerId = setTimeout(() => {\n setOverlayOpacity(0);\n timerRef.current = null; // Clear the ref after the timeout executes\n }, eventDurationSeconds * 1000); // Convert seconds to milliseconds\n\n // Save the new timer ID to the ref (unknown necessary type casting)\n timerRef.current = newTimerId as unknown as number;\n };\n\n // Cleanup function to clear the timer when the component unmounts\n React.useEffect(() => {\n return () => {\n const ref = timerRef.current;\n if (ref) {\n clearTimeout(ref);\n }\n };\n }, []); // runs only on mount/unmount\n\n function renderPasteHandler() {\n return (\n <textarea\n onPaste={handlePaste}\n onFocus={() => {\n console.log(\"...focused\");\n setIsFocused(true);\n }}\n onBlur={() => setIsFocused(false)}\n style={overlayStyle}\n // 🔑 CRUCIAL: Auto-focus the element so it's ready to receive the paste event.\n // autoFocus\n readOnly\n ref={textareaRef}\n />\n );\n }\n\n function onUpload(ev: React.MouseEvent<HTMLButtonElement>) {\n console.log(\"Clicked!!!\");\n ev.stopPropagation();\n ev.preventDefault();\n }\n\n function renderTitle() {\n if (title) {\n return <h2 className=\"paste-target-msg-title\">{title}</h2>;\n } else {\n return (\n <div className=\"paste-target-msg-title\">\n Paste\n {!isFocused && withUpload && (\n <>\n &nbsp;or&nbsp;\n <Button\n onClick={onUpload}\n style={{position: \"relative\", zIndex: 30}}\n >\n Upload\n </Button>\n </>\n )}\n &nbsp;Specification here\n </div>\n );\n }\n }\n\n const cn = _cls(\"root\", className || \"pihanga-paste-target\");\n\n return (\n <div\n className={clsx(cn, isFocused && \"pihanga-paste-target-focused\")}\n data-pihanga={cardName}\n style={{position: \"relative\", height}}\n >\n <div className=\"paste-target-label\">\n {renderTitle()}\n {description && (\n <span className=\"paste-target-msg-desc\">{description}</span>\n )}\n <div className=\"paste-target-focus-reminder\">{pasteFirstReminder}</div>\n </div>\n {renderPasteHandler()}\n </div>\n );\n};\n",
25
- "type": "registry:component",
26
- "target": "src/cards/pasteTarget/pasteTarget.component.tsx"
27
- },
28
- {
29
- "path": "cards/pasteTarget/pasteTarget.css",
30
- "content": ".pihanga-paste-target {\n\n border: dashed 2px #999;\n\n .paste-target-msg-title {\n font-size: 1.2em\n }\n\n .paste-target-label {\n position: absolute;\n\n /* Stretch the child to cover the parent entirely */\n top: 0;\n left: 0;\n right: 0;\n bottom: 0;\n\n display: flex;\n flex-direction: column;\n justify-content: center;\n align-items: center;\n text-align: center;\n width: 100%;\n }\n\n textarea {\n position: absolute;\n\n /* Stretch the child to cover the parent entirely */\n top: 0;\n left: 0;\n right: 0;\n bottom: 0;\n\n /* Ensure the child is visually stacked above the parent's content */\n z-index: 10;\n\n cursor: pointer;\n }\n\n textarea:focus {\n cursor: copy;\n\n /* Remove the default focus outline for a cleaner look (optional but common) */\n outline: none;\n }\n}\n\n.pihanga-paste-target-focused {\n /* Change the border color to highlight the active element */\n border-color: #007bff;\n border-width: 3px;\n\n .paste-target-focus-reminder {\n display: none\n }\n}",
31
- "type": "registry:component",
32
- "target": "src/cards/pasteTarget/pasteTarget.css"
33
- },
34
- {
35
- "path": "cards/pasteTarget/pasteTarget.types.ts",
36
- "content": " \nimport {\n createCardDeclaration,\n createOnAction,\n registerActions,\n} from \"@pihanga2/core\";\n\nexport const PASTE_TARGET_CARD = \"paste-target\";\nexport const PasteTarget = createCardDeclaration<\n PasteTargetProps,\n PasteTargetEvents\n>(PASTE_TARGET_CARD);\n\nexport const PASTE_TARGET_ACTION = registerActions(PASTE_TARGET_CARD, [\n \"pasted_content\",\n \"error\",\n]);\n\nexport const onContentPasted = createOnAction<CloseEvent>(\n PASTE_TARGET_ACTION.PASTED_CONTENT\n);\n\nexport const onPasteError = createOnAction<PasteTargetErrorEvent>(\n PASTE_TARGET_ACTION.ERROR\n);\n\nexport const DEF_HEIGHT = \"120px\";\nexport const DEF_SUCCESS_COLOR = \"lightgreen\";\nexport const DEF_ERROR_COLOR = \"lightred\";\nexport const DEF_EVENT_DURATION_SECONDS = 1;\nexport const DEF_OVERLAY_OPACITY = 0.5;\n\nexport const DEF_PASTE_FIRST_REMINDER =\n \"(Before pasting, click this this box to get the focus)\";\n\nexport type PasteTargetProps = {\n fileTypes?: string[];\n title?: string;\n description?: string;\n pasteFirstReminder?: string;\n withUpload?: boolean;\n\n height?: string | number;\n\n successColor?: string;\n errorColor?: string;\n eventDurationSeconds?: number; // How long to show success/error overlay color\n eventOpacity?: number; // Opacity of overlay on paste event\n\n className?: string;\n};\n\nexport const DEF_PASTE_TARGET_FILE_TYPES = [\"JPG\", \"PNG\", \"GIF\"];\n\nexport type PasteItem = {\n mimeType: string;\n content: string;\n};\n\nexport type PasteTargetPastedEvent = {\n items: PasteItem[];\n};\n\nexport type PasteTargetErrorEvent = {\n error: string;\n};\n\nexport type PasteTargetEvents = {\n onPastedContent: PasteTargetPastedEvent;\n onError: PasteTargetErrorEvent;\n};\n",
37
- "type": "registry:component",
38
- "target": "src/cards/pasteTarget/pasteTarget.types.ts"
39
- }
40
- ]
41
- }
@@ -1,11 +0,0 @@
1
- {
2
- "$schema": "https://ui.shadcn.com/schema/registry-item.json",
3
- "name": "pihanga-base",
4
- "type": "registry:component",
5
- "description": "Pihanga core runtime dependency — no source files; installing it triggers automatic `npm install @pihanga2/core`.",
6
- "dependencies": [
7
- "@pihanga2/core"
8
- ],
9
- "registryDependencies": [],
10
- "files": []
11
- }
@@ -1,16 +0,0 @@
1
- {
2
- "$schema": "https://ui.shadcn.com/schema/registry-item.json",
3
- "name": "pihanga-cards-icons",
4
- "type": "registry:component",
5
- "description": "Pihanga icon registry — registerIcon / getIcon helpers",
6
- "dependencies": [],
7
- "registryDependencies": [],
8
- "files": [
9
- {
10
- "path": "cards/icons.ts",
11
- "content": "import React from \"react\";\n\ntype G = {\n name2icon: {[name: string]: React.ElementType};\n};\n\ndeclare global {\n interface Window {\n _PihangaIcons: G;\n }\n}\n\nwindow._PihangaIcons = window._PihangaIcons || {\n name2icon: {},\n};\n\nconst name2icon = window._PihangaIcons.name2icon;\n\n\n\nexport function registerIcon(name: string, el: React.ElementType): string {\n if (name2icon[name] !== undefined) {\n throw new Error(`icon '${name}' already registered`);\n }\n name2icon[name] = el;\n return name;\n}\n\nexport function getIconElement(name: string): React.ElementType | undefined {\n return name2icon[name];\n}\n\n// can't figure out what the proper return type is, SVvgIconT does\n// NOT work with <IconButton>\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport function getIcon(name: string, props?: any): React.ReactNode {\n const icon = name2icon[name];\n if (!icon) return null;\n const el = React.createElement(icon, props);\n return el;\n}\n",
12
- "type": "registry:component",
13
- "target": "src/cards/icons.ts"
14
- }
15
- ]
16
- }
@@ -1,16 +0,0 @@
1
- {
2
- "$schema": "https://ui.shadcn.com/schema/registry-item.json",
3
- "name": "pihanga-cards-types",
4
- "type": "registry:component",
5
- "description": "Shared Pihanga card type definitions (ScreenSize, VariantT, SizeT, …)",
6
- "dependencies": [],
7
- "registryDependencies": [],
8
- "files": [
9
- {
10
- "path": "cards/types.ts",
11
- "content": "/**\n * Screen size breakpoints matching Tailwind CSS defaults.\n *\n * Used with {@link SizeMap} to render different cards (or none at all)\n * depending on the viewport width.\n */\nexport enum ScreenSize {\n XS = \"xs\", // default / < 640px\n SM = \"sm\", // ≥ 640px\n MD = \"md\", // ≥ 768px\n LG = \"lg\", // ≥ 1024px\n XL = \"xl\", // ≥ 1280px\n XXL = \"2xl\", // ≥ 1536px\n}\n\n/**\n * Ascending order of breakpoints — used for cascade resolution.\n * @internal\n */\nexport const SCREEN_SIZE_ORDER: ScreenSize[] = [\n ScreenSize.XS,\n ScreenSize.SM,\n ScreenSize.MD,\n ScreenSize.LG,\n ScreenSize.XL,\n ScreenSize.XXL,\n];\n\n/**\n * Minimum viewport widths (px) for each breakpoint.\n * @internal\n */\nexport const SCREEN_SIZE_WIDTHS: Record<ScreenSize, number> = {\n [ScreenSize.XS]: 0,\n [ScreenSize.SM]: 640,\n [ScreenSize.MD]: 768,\n [ScreenSize.LG]: 1024,\n [ScreenSize.XL]: 1280,\n [ScreenSize.XXL]: 1536,\n};\n\n/**\n * Map a value {@link T} to specific screen-size breakpoints.\n *\n * Resolution follows a **mobile-first cascade**: the entry with the\n * largest breakpoint that is ≤ the current viewport size wins.\n *\n * ```ts\n * // Show \"desktopCard\" on md and above, nothing on xs/sm:\n * { [ScreenSize.MD]: \"desktopCard\" }\n *\n * // Show a different card per breakpoint:\n * { [ScreenSize.XS]: \"mobileCard\", [ScreenSize.MD]: \"desktopCard\" }\n *\n * // Explicitly hide on md and above:\n * { [ScreenSize.XS]: \"card\", [ScreenSize.MD]: null }\n * ```\n *\n * Set `default` as a final fallback when no breakpoint entry matches.\n */\nexport type SizeMap<T> = Partial<Record<ScreenSize, T | null>> & {\n default?: T | null;\n};\n\n// ---------------------------------------------------------------------------\n// Shadcn / Tailwind-native shared primitive types\n//\n// These types replace what was previously imported from `@pihanga2/cards` and\n// are expressed in terms of Shadcn's design system and Tailwind utility tokens.\n// ---------------------------------------------------------------------------\n\n/**\n * Shadcn button / badge visual variant.\n *\n * Maps directly to the `variant` prop accepted by `@/components/ui/button`\n * and `@/components/ui/badge`.\n */\nexport type VariantT =\n | \"default\"\n | \"destructive\"\n | \"outline\"\n | \"secondary\"\n | \"ghost\"\n | \"link\";\n\n/**\n * Shadcn component size token.\n *\n * Maps directly to the `size` prop accepted by `@/components/ui/button`.\n */\nexport type SizeT = \"default\" | \"sm\" | \"lg\" | \"icon\";\n\n/**\n * Tailwind colour token (e.g. `\"red\"`, `\"emerald\"`, `\"sky\"`) or an arbitrary\n * CSS colour string. Kept as `string` so consumers can pass any Tailwind\n * palette key without being locked to a finite union.\n */\nexport type ColorT = string;\n\n/**\n * A decorator displayed before or after a button / menu-item label.\n *\n * Expressed as a registered icon name (resolved at render time via the icon\n * registry) or an arbitrary Tailwind-class string for inline SVG icons.\n */\nexport type DecoratorT = string;\n",
12
- "type": "registry:component",
13
- "target": "src/cards/types.ts"
14
- }
15
- ]
16
- }
@@ -1,15 +0,0 @@
1
- {
2
- "$schema": "https://ui.shadcn.com/schema/registry-item.json",
3
- "name": "pihanga-hook-use-is-touch-device",
4
- "type": "registry:hook",
5
- "description": "Pihanga shared hook: use-is-touch-device",
6
- "dependencies": [],
7
- "registryDependencies": [],
8
- "files": [
9
- {
10
- "path": "hooks/use-is-touch-device.ts",
11
- "content": "'use client';\n\nimport * as React from 'react';\n\nexport function useIsTouchDevice() {\n const [isTouchDevice, setIsTouchDevice] = React.useState(false);\n\n React.useEffect(() => {\n function onResize() {\n setIsTouchDevice(\n 'ontouchstart' in window ||\n navigator.maxTouchPoints > 0 ||\n navigator.maxTouchPoints > 0\n );\n }\n\n window.addEventListener('resize', onResize);\n onResize();\n\n return () => {\n window.removeEventListener('resize', onResize);\n };\n }, []);\n\n return isTouchDevice;\n}\n",
12
- "type": "registry:hook"
13
- }
14
- ]
15
- }
@@ -1,18 +0,0 @@
1
- {
2
- "$schema": "https://ui.shadcn.com/schema/registry-item.json",
3
- "name": "pihanga-lib-utils",
4
- "type": "registry:lib",
5
- "description": "Tailwind CSS utility helper — the cn() function",
6
- "dependencies": [
7
- "clsx@^2.1.1",
8
- "tailwind-merge@^3.3.0"
9
- ],
10
- "registryDependencies": [],
11
- "files": [
12
- {
13
- "path": "lib/utils.ts",
14
- "content": "import {clsx, type ClassValue} from \"clsx\";\nimport {twMerge} from \"tailwind-merge\";\n\nexport function cn(...inputs: ClassValue[]) {\n return twMerge(clsx(inputs));\n}\n",
15
- "type": "registry:lib"
16
- }
17
- ]
18
- }
@@ -1,25 +0,0 @@
1
- {
2
- "$schema": "https://ui.shadcn.com/schema/registry-item.json",
3
- "name": "pihanga-theme-provider",
4
- "type": "registry:component",
5
- "description": "Pihanga theme provider — light / dark mode support",
6
- "dependencies": [],
7
- "registryDependencies": [],
8
- "files": [
9
- {
10
- "path": "components/theme-provider/index.ts",
11
- "content": "import {useContext} from \"react\";\nimport {ThemeProviderContext} from \"./theme-provider.types\";\n\nexport const useTheme = () => {\n const context = useContext(ThemeProviderContext);\n\n if (context === undefined)\n throw new Error(\"useTheme must be used within a ThemeProvider\");\n\n return context;\n};\n",
12
- "type": "registry:component"
13
- },
14
- {
15
- "path": "components/theme-provider/theme-provider.component.tsx",
16
- "content": "import {useEffect, useState} from \"react\";\nimport {\n ThemeProviderContext,\n type Theme,\n type ThemeProviderProps,\n} from \"./theme-provider.types\";\n\nexport function ThemeProvider({\n children,\n defaultTheme = \"system\",\n storageKey = \"vite-ui-theme\",\n ...props\n}: ThemeProviderProps) {\n const [theme, setTheme] = useState<Theme>(\n () => (localStorage.getItem(storageKey) as Theme) || defaultTheme\n );\n\n useEffect(() => {\n const root = window.document.documentElement;\n\n root.classList.remove(\"light\", \"dark\");\n\n if (theme === \"system\") {\n const systemTheme = window.matchMedia(\"(prefers-color-scheme: dark)\")\n .matches\n ? \"dark\"\n : \"light\";\n\n root.classList.add(systemTheme);\n return;\n }\n\n root.classList.add(theme);\n }, [theme]);\n\n const value = {\n theme,\n setTheme: (theme: Theme) => {\n localStorage.setItem(storageKey, theme);\n setTheme(theme);\n },\n };\n\n return (\n <ThemeProviderContext.Provider {...props} value={value}>\n {children}\n </ThemeProviderContext.Provider>\n );\n}\n",
17
- "type": "registry:component"
18
- },
19
- {
20
- "path": "components/theme-provider/theme-provider.types.ts",
21
- "content": "import {createContext} from \"react\";\n\nexport type Theme = \"dark\" | \"light\" | \"system\";\n\nexport type ThemeProviderProps = {\n children: React.ReactNode;\n defaultTheme?: Theme;\n storageKey?: string;\n};\n\ntype ThemeProviderState = {\n theme: Theme;\n setTheme: (theme: Theme) => void;\n};\n\nconst initialState: ThemeProviderState = {\n theme: \"system\",\n setTheme: () => null,\n};\n\nexport const ThemeProviderContext =\n createContext<ThemeProviderState>(initialState);\n",
22
- "type": "registry:component"
23
- }
24
- ]
25
- }
@@ -1,68 +0,0 @@
1
- {
2
- "$schema": "https://ui.shadcn.com/schema/registry-item.json",
3
- "name": "pihanga-ui-extras",
4
- "type": "registry:ui",
5
- "description": "Pihanga-custom UI primitives not available in the official shadcn/ui registry (button with LinkButton, customised dialog with DialogBody, spinner, field, stepper, toolbar, variant tables).",
6
- "dependencies": [
7
- "class-variance-authority@^0.7.1",
8
- "radix-ui@^1.4.3",
9
- "@radix-ui/react-dialog@^1.1.15",
10
- "lucide-react@^0.513.0",
11
- "@radix-ui/react-toolbar@^1.1.11",
12
- "@radix-ui/react-tooltip@^1.2.8"
13
- ],
14
- "registryDependencies": [
15
- "https://ivcap-works.github.io/pihanga-shadcn/r/pihanga-lib-utils.json",
16
- "label",
17
- "separator",
18
- "dropdown-menu",
19
- "tooltip"
20
- ],
21
- "files": [
22
- {
23
- "path": "components/ui/badge-variants.ts",
24
- "content": "import { cva } from \"class-variance-authority\"\n\nexport const badgeVariants = cva(\n \"inline-flex items-center justify-center rounded-full border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden\",\n {\n variants: {\n variant: {\n default:\n \"border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90\",\n secondary:\n \"border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90\",\n destructive:\n \"border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60\",\n outline:\n \"text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground\",\n },\n },\n defaultVariants: {\n variant: \"default\",\n },\n }\n)\n",
25
- "type": "registry:ui"
26
- },
27
- {
28
- "path": "components/ui/button-variants.ts",
29
- "content": "import { cva } from \"class-variance-authority\"\n\nexport const buttonVariants = cva(\n \"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive\",\n {\n variants: {\n variant: {\n default: \"bg-primary text-primary-foreground hover:bg-primary/90\",\n destructive:\n \"bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60\",\n outline:\n \"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50\",\n secondary:\n \"bg-secondary text-secondary-foreground hover:bg-secondary/80\",\n ghost:\n \"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50\",\n link: \"text-primary underline-offset-4 hover:underline\",\n },\n size: {\n default: \"h-9 px-4 py-2 has-[>svg]:px-3\",\n sm: \"h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5\",\n lg: \"h-10 rounded-md px-6 has-[>svg]:px-4\",\n icon: \"size-9\",\n \"icon-sm\": \"size-8\",\n \"icon-lg\": \"size-10\",\n },\n },\n defaultVariants: {\n variant: \"default\",\n size: \"default\",\n },\n }\n)\n",
30
- "type": "registry:ui"
31
- },
32
- {
33
- "path": "components/ui/button.tsx",
34
- "content": "import * as React from \"react\";\nimport {cva, type VariantProps} from \"class-variance-authority\";\nimport {Slot} from \"radix-ui\";\n\nimport {cn} from \"@/lib/utils\";\n\nconst buttonVariants = cva(\n \"inline-flex shrink-0 items-center justify-center gap-2 rounded-md text-sm font-medium whitespace-nowrap transition-all outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4\",\n {\n variants: {\n variant: {\n default: \"bg-primary text-primary-foreground hover:bg-primary/90\",\n destructive:\n \"bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:bg-destructive/60 dark:focus-visible:ring-destructive/40\",\n outline:\n \"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50\",\n secondary:\n \"bg-secondary text-secondary-foreground hover:bg-secondary/80\",\n ghost:\n \"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50\",\n link: \"text-primary underline-offset-4 hover:underline\",\n // Extended variants used by pi/button\n ghost2:\n \"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50\",\n ghost3:\n \"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50\",\n ghostActive: \"bg-accent text-accent-foreground\",\n brand: \"bg-primary text-primary-foreground hover:bg-primary/90\",\n nav: \"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50\",\n navAction:\n \"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50\",\n menuAction:\n \"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50\",\n blockAction:\n \"bg-secondary text-secondary-foreground hover:bg-secondary/80\",\n blockActionSecondary:\n \"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground\",\n primaryOutline:\n \"border border-primary text-primary hover:bg-primary hover:text-primary-foreground\",\n radio:\n \"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground\",\n none: \"\",\n },\n size: {\n default: \"h-9 px-4 py-2 has-[>svg]:px-3\",\n xs: \"h-6 gap-1 rounded-md px-2 text-xs has-[>svg]:px-1.5 [&_svg:not([class*='size-'])]:size-3\",\n sm: \"h-8 gap-1.5 rounded-md px-3 has-[>svg]:px-2.5\",\n md: \"h-9 px-4 py-2 has-[>svg]:px-3\",\n lg: \"h-10 rounded-md px-6 has-[>svg]:px-4\",\n icon: \"size-9\",\n iconSm: \"size-8\",\n \"icon-xs\": \"size-6 rounded-md [&_svg:not([class*='size-'])]:size-3\",\n \"icon-sm\": \"size-8\",\n \"icon-lg\": \"size-10\",\n navAction: \"h-9 px-3\",\n menuAction: \"h-8 px-3 text-sm\",\n blockAction: \"h-10 px-4\",\n none: \"\",\n },\n },\n defaultVariants: {\n variant: \"default\",\n size: \"default\",\n },\n },\n);\n\n/** Extra props accepted by both Button and LinkButton beyond standard HTML/CVA props */\ntype ButtonExtendedProps = {\n /** Accessible label; maps to aria-label */\n label?: string;\n active?: boolean;\n focused?: boolean;\n isMenu?: boolean;\n truncate?: boolean;\n loading?: boolean;\n isPending?: boolean;\n loadingClassName?: string;\n};\n\nfunction Button({\n className,\n variant = \"default\",\n size = \"default\",\n asChild = false,\n label,\n active,\n focused,\n isMenu,\n truncate,\n loading,\n isPending,\n loadingClassName: _loadingClassName,\n ...props\n}: React.ComponentProps<\"button\"> &\n VariantProps<typeof buttonVariants> & {\n asChild?: boolean;\n } & ButtonExtendedProps) {\n const Comp = asChild ? Slot.Root : \"button\";\n\n return (\n <Comp\n data-slot=\"button\"\n data-variant={variant}\n data-size={size}\n aria-label={label}\n data-active={active || undefined}\n data-focused={focused || undefined}\n data-is-menu={isMenu || undefined}\n data-loading={loading || undefined}\n data-pending={isPending || undefined}\n className={cn(\n buttonVariants({variant, size, className}),\n truncate && \"truncate\",\n )}\n {...props}\n />\n );\n}\n\n/** Button rendered as an anchor (`<a>`) element */\nfunction LinkButton({\n className,\n variant = \"default\",\n size = \"default\",\n href,\n target,\n label,\n active,\n focused,\n isMenu,\n truncate,\n loading,\n isPending,\n loadingClassName: _loadingClassName,\n onClick,\n children,\n ...props\n}: Omit<React.ComponentProps<\"a\">, \"onClick\"> &\n VariantProps<typeof buttonVariants> &\n ButtonExtendedProps & {\n href?: string;\n target?: string;\n onClick?: () => void;\n }) {\n return (\n <a\n data-slot=\"button\"\n data-variant={variant}\n data-size={size}\n href={href}\n target={target}\n aria-label={label}\n data-active={active || undefined}\n data-focused={focused || undefined}\n data-is-menu={isMenu || undefined}\n data-loading={loading || undefined}\n data-pending={isPending || undefined}\n className={cn(\n buttonVariants({variant, size, className}),\n truncate && \"truncate\",\n )}\n onClick={\n onClick\n ? (e) => {\n e.preventDefault();\n onClick();\n }\n : undefined\n }\n {...props}\n >\n {children}\n </a>\n );\n}\n\nexport {Button, buttonVariants, LinkButton};\n",
35
- "type": "registry:ui"
36
- },
37
- {
38
- "path": "components/ui/dialog.tsx",
39
- "content": "import * as React from \"react\";\nimport * as DialogPrimitive from \"@radix-ui/react-dialog\";\nimport {X} from \"lucide-react\";\n\nimport {cn} from \"@/lib/utils\";\n\nconst sizeClasses: Record<string, string> = {\n xs: \"max-w-xs\",\n sm: \"max-w-sm\",\n md: \"max-w-md\",\n lg: \"max-w-lg\",\n xl: \"max-w-xl\",\n \"2xl\": \"max-w-2xl\",\n \"3xl\": \"max-w-3xl\",\n \"4xl\": \"max-w-4xl\",\n};\n\ntype DialogProps = React.ComponentProps<typeof DialogPrimitive.Root> & {\n desktopVariant?: \"modal\" | \"drawer\" | \"full\";\n mobileVariant?: \"modal\" | \"drawer\" | \"full\";\n};\n\nfunction Dialog({\n desktopVariant: _desktopVariant,\n mobileVariant: _mobileVariant,\n ...props\n}: DialogProps) {\n return <DialogPrimitive.Root data-slot=\"dialog\" {...props} />;\n}\n\nfunction DialogTrigger({\n ...props\n}: React.ComponentProps<typeof DialogPrimitive.Trigger>) {\n return <DialogPrimitive.Trigger data-slot=\"dialog-trigger\" {...props} />;\n}\n\nfunction DialogPortal({\n ...props\n}: React.ComponentProps<typeof DialogPrimitive.Portal>) {\n return <DialogPrimitive.Portal data-slot=\"dialog-portal\" {...props} />;\n}\n\nfunction DialogClose({\n ...props\n}: React.ComponentProps<typeof DialogPrimitive.Close>) {\n return <DialogPrimitive.Close data-slot=\"dialog-close\" {...props} />;\n}\n\nfunction DialogOverlay({\n className,\n ...props\n}: React.ComponentProps<typeof DialogPrimitive.Overlay>) {\n return (\n <DialogPrimitive.Overlay\n data-slot=\"dialog-overlay\"\n className={cn(\n \"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50\",\n className,\n )}\n {...props}\n />\n );\n}\n\ntype DialogContentProps = React.ComponentProps<\n typeof DialogPrimitive.Content\n> & {\n size?: \"xs\" | \"sm\" | \"md\" | \"lg\" | \"xl\" | \"2xl\" | \"3xl\" | \"4xl\";\n variant?: \"modal\" | \"full\";\n dismissible?: boolean;\n hideClose?: boolean;\n fixed?: boolean;\n};\n\nfunction DialogContent({\n className,\n children,\n size = \"md\",\n variant,\n dismissible = true,\n hideClose = false,\n fixed,\n ...props\n}: DialogContentProps) {\n return (\n <DialogPortal>\n <DialogOverlay />\n <DialogPrimitive.Content\n data-slot=\"dialog-content\"\n onInteractOutside={\n dismissible === false ? (e) => e.preventDefault() : undefined\n }\n onEscapeKeyDown={\n dismissible === false ? (e) => e.preventDefault() : undefined\n }\n className={cn(\n \"bg-card text-card-foreground border border-border data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-1/2 left-1/2 z-50 w-full -translate-x-1/2 -translate-y-1/2 rounded-lg shadow-xl\",\n sizeClasses[size],\n variant === \"full\" && \"max-w-full h-full rounded-none\",\n fixed && \"flex flex-col max-h-[90vh]\",\n className,\n )}\n {...props}\n >\n {!hideClose && (\n <DialogPrimitive.Close className=\"absolute top-4 right-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none\">\n <X className=\"h-4 w-4\" />\n <span className=\"sr-only\">Close</span>\n </DialogPrimitive.Close>\n )}\n {children}\n </DialogPrimitive.Content>\n </DialogPortal>\n );\n}\n\nfunction DialogHeader({className, ...props}: React.ComponentProps<\"div\">) {\n return (\n <div\n data-slot=\"dialog-header\"\n className={cn(\n \"flex shrink-0 flex-col gap-2 px-6 pt-6 text-center sm:text-left\",\n className,\n )}\n {...props}\n />\n );\n}\n\nfunction DialogBody({className, ...props}: React.ComponentProps<\"div\">) {\n return (\n <div\n data-slot=\"dialog-body\"\n className={cn(\"flex-1 overflow-auto px-6 py-4\", className)}\n {...props}\n />\n );\n}\n\nfunction DialogFooter({className, ...props}: React.ComponentProps<\"div\">) {\n return (\n <div\n data-slot=\"dialog-footer\"\n className={cn(\n \"flex shrink-0 flex-col-reverse gap-2 px-6 pb-6 sm:flex-row sm:justify-end\",\n className,\n )}\n {...props}\n />\n );\n}\n\nfunction DialogTitle({\n className,\n ...props\n}: React.ComponentProps<typeof DialogPrimitive.Title>) {\n return (\n <DialogPrimitive.Title\n data-slot=\"dialog-title\"\n className={cn(\"text-lg font-semibold leading-none\", className)}\n {...props}\n />\n );\n}\n\nfunction DialogDescription({\n className,\n ...props\n}: React.ComponentProps<typeof DialogPrimitive.Description>) {\n return (\n <DialogPrimitive.Description\n data-slot=\"dialog-description\"\n className={cn(\"text-muted-foreground text-sm\", className)}\n {...props}\n />\n );\n}\n\nexport {\n Dialog,\n DialogBody,\n DialogClose,\n DialogContent,\n DialogDescription,\n DialogFooter,\n DialogHeader,\n DialogOverlay,\n DialogPortal,\n DialogTitle,\n DialogTrigger,\n};\n",
40
- "type": "registry:ui"
41
- },
42
- {
43
- "path": "components/ui/editor-static-variants.ts",
44
- "content": "import { cva } from \"class-variance-authority\"\n\nimport { cn } from \"@/components/lib/utils\"\n\nexport const editorVariants = cva(\n cn(\n \"group/editor\",\n \"relative w-full cursor-text overflow-x-hidden break-words whitespace-pre-wrap select-text\",\n \"rounded-md ring-offset-background focus-visible:outline-none\",\n \"placeholder:text-muted-foreground/80 **:data-slate-placeholder:top-[auto_!important] **:data-slate-placeholder:text-muted-foreground/80 **:data-slate-placeholder:opacity-100!\",\n \"[&_strong]:font-bold\"\n ),\n {\n defaultVariants: {\n variant: \"none\",\n },\n variants: {\n disabled: {\n true: \"cursor-not-allowed opacity-50\",\n },\n focused: {\n true: \"ring-2 ring-ring ring-offset-2\",\n },\n variant: {\n ai: \"w-full px-0 text-base md:text-sm\",\n aiChat:\n \"max-h-[min(70vh,320px)] w-full max-w-[700px] overflow-y-auto px-5 py-3 text-base md:text-sm\",\n default:\n \"size-full px-16 pt-4 pb-72 text-base sm:px-[max(64px,calc(50%-350px))]\",\n demo: \"size-full px-16 pt-4 pb-72 text-base sm:px-[max(64px,calc(50%-350px))]\",\n fullWidth: \"size-full px-16 pt-4 pb-72 text-base sm:px-24\",\n none: \"\",\n select: \"px-3 py-2 text-base data-readonly:w-fit\",\n },\n },\n }\n)\n",
45
- "type": "registry:ui"
46
- },
47
- {
48
- "path": "components/ui/field.tsx",
49
- "content": "import { useMemo } from \"react\"\nimport { cva, type VariantProps } from \"class-variance-authority\"\n\nimport { cn } from \"@/lib/utils\"\nimport { Label } from \"@/components/ui/label\"\nimport { Separator } from \"@/components/ui/separator\"\n\nfunction FieldSet({ className, ...props }: React.ComponentProps<\"fieldset\">) {\n return (\n <fieldset\n data-slot=\"field-set\"\n className={cn(\n \"flex flex-col gap-6\",\n \"has-[>[data-slot=checkbox-group]]:gap-3 has-[>[data-slot=radio-group]]:gap-3\",\n className\n )}\n {...props}\n />\n )\n}\n\nfunction FieldLegend({\n className,\n variant = \"legend\",\n ...props\n}: React.ComponentProps<\"legend\"> & { variant?: \"legend\" | \"label\" }) {\n return (\n <legend\n data-slot=\"field-legend\"\n data-variant={variant}\n className={cn(\n \"mb-3 font-medium\",\n \"data-[variant=legend]:text-base\",\n \"data-[variant=label]:text-sm\",\n className\n )}\n {...props}\n />\n )\n}\n\nfunction FieldGroup({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n <div\n data-slot=\"field-group\"\n className={cn(\n \"group/field-group @container/field-group flex w-full flex-col gap-7 data-[slot=checkbox-group]:gap-3 [&>[data-slot=field-group]]:gap-4\",\n className\n )}\n {...props}\n />\n )\n}\n\nconst fieldVariants = cva(\n \"group/field flex w-full gap-3 data-[invalid=true]:text-destructive\",\n {\n variants: {\n orientation: {\n vertical: [\"flex-col [&>*]:w-full [&>.sr-only]:w-auto\"],\n horizontal: [\n \"flex-row items-center\",\n \"[&>[data-slot=field-label]]:flex-auto\",\n \"has-[>[data-slot=field-content]]:items-start has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px\",\n ],\n responsive: [\n \"flex-col @md/field-group:flex-row @md/field-group:items-center [&>*]:w-full @md/field-group:[&>*]:w-auto [&>.sr-only]:w-auto\",\n \"@md/field-group:[&>[data-slot=field-label]]:flex-auto\",\n \"@md/field-group:has-[>[data-slot=field-content]]:items-start @md/field-group:has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px\",\n ],\n },\n },\n defaultVariants: {\n orientation: \"vertical\",\n },\n }\n)\n\nfunction Field({\n className,\n orientation = \"vertical\",\n ...props\n}: React.ComponentProps<\"div\"> & VariantProps<typeof fieldVariants>) {\n return (\n <div\n role=\"group\"\n data-slot=\"field\"\n data-orientation={orientation}\n className={cn(fieldVariants({ orientation }), className)}\n {...props}\n />\n )\n}\n\nfunction FieldContent({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n <div\n data-slot=\"field-content\"\n className={cn(\n \"group/field-content flex flex-1 flex-col gap-1.5 leading-snug\",\n className\n )}\n {...props}\n />\n )\n}\n\nfunction FieldLabel({\n className,\n ...props\n}: React.ComponentProps<typeof Label>) {\n return (\n <Label\n data-slot=\"field-label\"\n className={cn(\n \"group/field-label peer/field-label flex w-fit gap-2 leading-snug group-data-[disabled=true]/field:opacity-50\",\n \"has-[>[data-slot=field]]:w-full has-[>[data-slot=field]]:flex-col has-[>[data-slot=field]]:rounded-md has-[>[data-slot=field]]:border [&>*]:data-[slot=field]:p-4\",\n \"has-data-[state=checked]:border-primary has-data-[state=checked]:bg-primary/5 dark:has-data-[state=checked]:bg-primary/10\",\n className\n )}\n {...props}\n />\n )\n}\n\nfunction FieldTitle({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n <div\n data-slot=\"field-label\"\n className={cn(\n \"flex w-fit items-center gap-2 text-sm leading-snug font-medium group-data-[disabled=true]/field:opacity-50\",\n className\n )}\n {...props}\n />\n )\n}\n\nfunction FieldDescription({ className, ...props }: React.ComponentProps<\"p\">) {\n return (\n <p\n data-slot=\"field-description\"\n className={cn(\n \"text-sm leading-normal font-normal text-muted-foreground group-has-[[data-orientation=horizontal]]/field:text-balance\",\n \"last:mt-0 nth-last-2:-mt-1 [[data-variant=legend]+&]:-mt-1.5\",\n \"[&>a]:underline [&>a]:underline-offset-4 [&>a:hover]:text-primary\",\n className\n )}\n {...props}\n />\n )\n}\n\nfunction FieldSeparator({\n children,\n className,\n ...props\n}: React.ComponentProps<\"div\"> & {\n children?: React.ReactNode\n}) {\n return (\n <div\n data-slot=\"field-separator\"\n data-content={!!children}\n className={cn(\n \"relative -my-2 h-5 text-sm group-data-[variant=outline]/field-group:-mb-2\",\n className\n )}\n {...props}\n >\n <Separator className=\"absolute inset-0 top-1/2\" />\n {children && (\n <span\n className=\"relative mx-auto block w-fit bg-background px-2 text-muted-foreground\"\n data-slot=\"field-separator-content\"\n >\n {children}\n </span>\n )}\n </div>\n )\n}\n\nfunction FieldError({\n className,\n children,\n errors,\n ...props\n}: React.ComponentProps<\"div\"> & {\n errors?: Array<{ message?: string } | undefined>\n}) {\n const content = useMemo(() => {\n if (children) {\n return children\n }\n\n if (!errors?.length) {\n return null\n }\n\n const uniqueErrors = [\n ...new Map(errors.map((error) => [error?.message, error])).values(),\n ]\n\n if (uniqueErrors?.length == 1) {\n return uniqueErrors[0]?.message\n }\n\n return (\n <ul className=\"ml-4 flex list-disc flex-col gap-1\">\n {uniqueErrors.map(\n (error, index) =>\n error?.message && <li key={index}>{error.message}</li>\n )}\n </ul>\n )\n }, [children, errors])\n\n if (!content) {\n return null\n }\n\n return (\n <div\n role=\"alert\"\n data-slot=\"field-error\"\n className={cn(\"text-sm font-normal text-destructive\", className)}\n {...props}\n >\n {content}\n </div>\n )\n}\n\nexport {\n Field,\n FieldLabel,\n FieldDescription,\n FieldError,\n FieldGroup,\n FieldLegend,\n FieldSeparator,\n FieldSet,\n FieldContent,\n FieldTitle,\n}\n",
50
- "type": "registry:ui"
51
- },
52
- {
53
- "path": "components/ui/spinner.tsx",
54
- "content": "import {cn} from \"@/lib/utils\";\n\ntype SpinnerProps = {\n className?: string;\n size?: \"default\" | \"sm\" | \"lg\" | \"icon\";\n};\n\nconst sizeClasses: Record<string, string> = {\n default: \"size-6\",\n sm: \"size-4\",\n lg: \"size-8\",\n icon: \"size-9\",\n};\n\nfunction Spinner({className, size = \"default\"}: SpinnerProps) {\n return (\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n fill=\"none\"\n viewBox=\"0 0 24 24\"\n role=\"status\"\n aria-label=\"Loading\"\n className={cn(\"animate-spin\", sizeClasses[size], className)}\n >\n <circle\n className=\"opacity-25\"\n cx=\"12\"\n cy=\"12\"\n r=\"10\"\n stroke=\"currentColor\"\n strokeWidth=\"4\"\n />\n <path\n className=\"opacity-75\"\n fill=\"currentColor\"\n d=\"M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z\"\n />\n </svg>\n );\n}\n\nexport {Spinner};\n",
55
- "type": "registry:ui"
56
- },
57
- {
58
- "path": "components/ui/stepper.tsx",
59
- "content": "/**\n * Stepper UI primitive — shadcn/Tailwind-based, inspired by MUI Joy UI's Stepper.\n *\n * Architecture\n * ───────────\n * • Tailwind classes handle structural / layout / dimension concerns that are\n * difficult to override with plain CSS (flex layout, width/height, spacing).\n * • BEM class names (`pi-stepper`, `pi-stepper__*`, `pi-stepper__*--state`)\n * are applied to every element so consumers can override visual styles\n * (colours, borders, transitions) in `stepper.css` or any downstream sheet.\n * • Default visual styles live in `src/cards/shadcn/stepper/stepper.css`\n * (imported by the Pihanga card component).\n */\nimport * as React from \"react\";\nimport {Check} from \"lucide-react\";\nimport {cn} from \"@/lib/utils\";\n\n// ─── Types ────────────────────────────────────────────────────────────────────\n\nexport type StepState = \"completed\" | \"active\" | \"upcoming\";\n\nexport interface StepData {\n id: string;\n title: string;\n description?: string;\n optional?: boolean;\n /**\n * Optional pre-rendered React node to replace the default title/description\n * label. Produced by the Pihanga card component when `labelCard` is set on\n * a `StepItem`; rendered via `<Card cardName={step.labelCard} />` there.\n */\n labelNode?: React.ReactNode;\n}\n\nexport interface StepperUIProps {\n steps: StepData[];\n /** 0-based index of the currently active step */\n activeStep: number;\n orientation?: \"horizontal\" | \"vertical\";\n size?: \"sm\" | \"md\" | \"lg\";\n className?: string;\n /** If provided, steps become clickable */\n onStepClick?: (stepIndex: number, stepId: string) => void;\n /** Optional content rendered below the label in vertical-mode active steps */\n renderStepContent?: (stepIndex: number, step: StepData) => React.ReactNode;\n}\n\n// ─── Helpers ──────────────────────────────────────────────────────────────────\n\nfunction getStepState(stepIndex: number, activeStep: number): StepState {\n if (stepIndex < activeStep) return \"completed\";\n if (stepIndex === activeStep) return \"active\";\n return \"upcoming\";\n}\n\n// Tailwind dimension classes for the step-indicator button.\n// Kept here so layout always works even without the CSS file loaded.\nconst INDICATOR_SIZE = {\n sm: {btn: \"h-6 w-6 text-xs\", icon: \"h-3 w-3\"},\n md: {btn: \"h-8 w-8 text-sm\", icon: \"h-4 w-4\"},\n lg: {btn: \"h-10 w-10 text-base\", icon: \"h-5 w-5\"},\n} as const;\n\n// Tailwind label font classes — fallback when CSS file is absent.\nconst LABEL_SIZE = {\n sm: \"text-xs\",\n md: \"text-sm\",\n lg: \"text-base\",\n} as const;\n\n// ─── Indicator button ─────────────────────────────────────────────────────────\n\nfunction IndicatorButton({\n state,\n stepIndex,\n size,\n onClick,\n}: {\n state: StepState;\n stepIndex: number;\n size: \"sm\" | \"md\" | \"lg\";\n onClick?: () => void;\n}) {\n const s = INDICATOR_SIZE[size];\n return (\n <button\n type=\"button\"\n onClick={onClick}\n disabled={!onClick}\n aria-current={state === \"active\" ? \"step\" : undefined}\n className={cn(\n // ── BEM — visual override target ──\n \"pi-stepper__indicator\",\n `pi-stepper__indicator--${state}`,\n onClick\n ? \"pi-stepper__indicator--clickable\"\n : \"pi-stepper__indicator--static\",\n // ── Tailwind — structural defaults (always applied) ──\n \"flex shrink-0 items-center justify-center rounded-full font-semibold\",\n s.btn,\n // Default visual appearance via Tailwind (overridden by stepper.css rules)\n state === \"completed\" && \"bg-primary text-primary-foreground\",\n state === \"active\" &&\n \"bg-primary text-primary-foreground ring-2 ring-primary ring-offset-2\",\n state === \"upcoming\" &&\n \"border-2 border-muted-foreground/40 bg-background text-muted-foreground\",\n onClick && \"cursor-pointer hover:opacity-80\",\n !onClick && \"cursor-default\",\n )}\n >\n {state === \"completed\" ? (\n <Check\n className={cn(\"pi-stepper__indicator-icon\", s.icon)}\n strokeWidth={2.5}\n />\n ) : (\n <span className=\"pi-stepper__indicator-label\">{stepIndex + 1}</span>\n )}\n </button>\n );\n}\n\n// ─── Label ────────────────────────────────────────────────────────────────────\n\nfunction StepLabel({\n step,\n state,\n size,\n vertical = false,\n}: {\n step: StepData;\n state: StepState;\n size: \"sm\" | \"md\" | \"lg\";\n vertical?: boolean;\n}) {\n // Custom card label takes precedence over the default text label.\n if (step.labelNode != null) {\n return (\n <div\n className={cn(\n \"pi-stepper__label-card\",\n `pi-stepper__label-card--${state}`,\n vertical && \"pi-stepper__label-card--vertical\",\n // Tailwind defaults\n vertical ? \"ml-4\" : \"mt-2 text-center\",\n )}\n >\n {step.labelNode}\n </div>\n );\n }\n\n return (\n <div\n className={cn(\n \"pi-stepper__label\",\n `pi-stepper__label--${state}`,\n vertical && \"pi-stepper__label--vertical\",\n // Tailwind defaults\n vertical ? \"ml-4\" : \"mt-2 text-center\",\n !vertical && \"max-w-[96px]\",\n )}\n >\n <p\n className={cn(\n \"pi-stepper__label-title\",\n `pi-stepper__label-title--${state}`,\n // Tailwind defaults\n LABEL_SIZE[size],\n \"font-medium leading-tight\",\n state === \"upcoming\" ? \"text-muted-foreground\" : \"text-foreground\",\n vertical && \"leading-none\",\n )}\n >\n {step.title}\n {vertical && step.optional && (\n <span className=\"pi-stepper__label-optional ml-1 text-xs font-normal text-muted-foreground\">\n (Optional)\n </span>\n )}\n </p>\n\n {!vertical && step.optional && (\n <p className=\"pi-stepper__label-optional text-xs font-normal text-muted-foreground\">\n Optional\n </p>\n )}\n\n {step.description && (\n <p className=\"pi-stepper__label-description mt-0.5 text-xs text-muted-foreground\">\n {step.description}\n </p>\n )}\n </div>\n );\n}\n\n// ─── Horizontal layout ────────────────────────────────────────────────────────\n\nfunction HorizontalStepper({\n steps,\n activeStep,\n size = \"md\",\n className,\n onStepClick,\n}: StepperUIProps) {\n return (\n <ol\n aria-label=\"Steps\"\n className={cn(\n \"pi-stepper pi-stepper--horizontal\",\n `pi-stepper--${size}`,\n \"flex w-full items-start\",\n className,\n )}\n >\n {steps.map((step, index) => {\n const state = getStepState(index, activeStep);\n const isFirst = index === 0;\n const isLast = index === steps.length - 1;\n\n const leftCompleted =\n !isFirst && getStepState(index - 1, activeStep) === \"completed\";\n const rightCompleted = !isLast && state === \"completed\";\n\n return (\n <li\n key={step.id}\n className={cn(\n \"pi-stepper__step\",\n `pi-stepper__step--${state}`,\n isFirst && \"pi-stepper__step--first\",\n isLast && \"pi-stepper__step--last\",\n \"flex flex-1 flex-col items-center\",\n )}\n >\n {/* ── [left-half] [indicator] [right-half] centred row ── */}\n <div className=\"flex w-full items-center\">\n {isFirst ? (\n <div className=\"flex-1\" aria-hidden=\"true\" />\n ) : (\n <div\n className={cn(\n \"pi-stepper__connector pi-stepper__connector--left\",\n leftCompleted\n ? \"pi-stepper__connector--completed\"\n : \"pi-stepper__connector--upcoming\",\n // Tailwind defaults\n \"h-0.5 flex-1 transition-colors duration-300\",\n leftCompleted ? \"bg-primary\" : \"bg-border\",\n )}\n />\n )}\n\n <IndicatorButton\n state={state}\n stepIndex={index}\n size={size}\n onClick={\n onStepClick ? () => onStepClick(index, step.id) : undefined\n }\n />\n\n {isLast ? (\n <div className=\"flex-1\" aria-hidden=\"true\" />\n ) : (\n <div\n className={cn(\n \"pi-stepper__connector pi-stepper__connector--right\",\n rightCompleted\n ? \"pi-stepper__connector--completed\"\n : \"pi-stepper__connector--upcoming\",\n // Tailwind defaults\n \"h-0.5 flex-1 transition-colors duration-300\",\n rightCompleted ? \"bg-primary\" : \"bg-border\",\n )}\n />\n )}\n </div>\n\n <StepLabel step={step} state={state} size={size} vertical={false} />\n </li>\n );\n })}\n </ol>\n );\n}\n\n// ─── Vertical layout ──────────────────────────────────────────────────────────\n\nfunction VerticalStepper({\n steps,\n activeStep,\n size = \"md\",\n className,\n onStepClick,\n renderStepContent,\n}: StepperUIProps) {\n return (\n <ol\n className={cn(\n \"pi-stepper pi-stepper--vertical\",\n `pi-stepper--${size}`,\n \"flex flex-col\",\n className,\n )}\n >\n {steps.map((step, index) => {\n const state = getStepState(index, activeStep);\n const isFirst = index === 0;\n const isLast = index === steps.length - 1;\n const content =\n state === \"active\" && renderStepContent\n ? renderStepContent(index, step)\n : null;\n\n return (\n <li\n key={step.id}\n className={cn(\n \"pi-stepper__step\",\n `pi-stepper__step--${state}`,\n isFirst && \"pi-stepper__step--first\",\n isLast && \"pi-stepper__step--last\",\n \"flex\",\n )}\n >\n {/* Left col: indicator + connector */}\n <div className=\"flex flex-col items-center\">\n <IndicatorButton\n state={state}\n stepIndex={index}\n size={size}\n onClick={\n onStepClick ? () => onStepClick(index, step.id) : undefined\n }\n />\n {!isLast && (\n <div\n className={cn(\n \"pi-stepper__connector pi-stepper__connector--vertical\",\n state === \"completed\"\n ? \"pi-stepper__connector--completed\"\n : \"pi-stepper__connector--upcoming\",\n // Tailwind defaults\n \"mb-1 mt-1 w-0.5 flex-1 transition-colors duration-300\",\n state === \"completed\" ? \"bg-primary\" : \"bg-border\",\n )}\n style={{minHeight: \"24px\"}}\n />\n )}\n </div>\n\n {/* Right col: label + content */}\n <div className={cn(\"flex-1\", !isLast && \"pb-6\")}>\n <StepLabel\n step={step}\n state={state}\n size={size}\n vertical={true}\n />\n {content && (\n <div className=\"pi-stepper__step-content ml-4 mt-3\">\n {content}\n </div>\n )}\n </div>\n </li>\n );\n })}\n </ol>\n );\n}\n\n// ─── Public export ────────────────────────────────────────────────────────────\n\nexport function StepperUI(props: StepperUIProps) {\n return props.orientation === \"vertical\" ? (\n <VerticalStepper {...props} />\n ) : (\n <HorizontalStepper {...props} />\n );\n}\n",
60
- "type": "registry:ui"
61
- },
62
- {
63
- "path": "components/ui/toolbar.tsx",
64
- "content": "\"use client\";\n\nimport * as React from \"react\";\n\nimport * as ToolbarPrimitive from \"@radix-ui/react-toolbar\";\nimport * as TooltipPrimitive from \"@radix-ui/react-tooltip\";\nimport {type VariantProps, cva} from \"class-variance-authority\";\nimport {ChevronDown} from \"lucide-react\";\n\nimport {\n DropdownMenuLabel,\n DropdownMenuRadioGroup,\n DropdownMenuSeparator,\n} from \"@/components/ui/dropdown-menu\";\nimport {Separator} from \"@/components/ui/separator\";\nimport {Tooltip, TooltipTrigger} from \"@/components/ui/tooltip\";\nimport {cn} from \"@/components/lib/utils\";\n\nexport function Toolbar({\n className,\n ...props\n}: React.ComponentProps<typeof ToolbarPrimitive.Root>) {\n return (\n <ToolbarPrimitive.Root\n className={cn(\"relative flex items-center select-none\", className)}\n {...props}\n />\n );\n}\n\nexport function ToolbarToggleGroup({\n className,\n ...props\n}: React.ComponentProps<typeof ToolbarPrimitive.ToolbarToggleGroup>) {\n return (\n <ToolbarPrimitive.ToolbarToggleGroup\n className={cn(\"flex items-center\", className)}\n {...props}\n />\n );\n}\n\nexport function ToolbarLink({\n className,\n ...props\n}: React.ComponentProps<typeof ToolbarPrimitive.Link>) {\n return (\n <ToolbarPrimitive.Link\n className={cn(\"font-medium underline underline-offset-4\", className)}\n {...props}\n />\n );\n}\n\nexport function ToolbarSeparator({\n className,\n ...props\n}: React.ComponentProps<typeof ToolbarPrimitive.Separator>) {\n return (\n <ToolbarPrimitive.Separator\n className={cn(\"mx-2 my-1 w-px shrink-0 bg-border\", className)}\n {...props}\n />\n );\n}\n\n// From toggleVariants\nconst toolbarButtonVariants = cva(\n \"inline-flex cursor-pointer items-center justify-center gap-2 rounded-md text-sm font-medium whitespace-nowrap transition-[color,box-shadow] outline-none hover:bg-muted hover:text-muted-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 aria-checked:bg-accent aria-checked:text-accent-foreground aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4\",\n {\n defaultVariants: {\n size: \"default\",\n variant: \"default\",\n },\n variants: {\n size: {\n default: \"h-9 min-w-9 px-2\",\n lg: \"h-10 min-w-10 px-2.5\",\n sm: \"h-8 min-w-8 px-1.5\",\n },\n variant: {\n default: \"bg-transparent\",\n outline:\n \"border border-input bg-transparent shadow-xs hover:bg-accent hover:text-accent-foreground\",\n },\n },\n }\n);\n\nconst dropdownArrowVariants = cva(\n cn(\n \"inline-flex items-center justify-center rounded-r-md text-sm font-medium text-foreground transition-colors disabled:pointer-events-none disabled:opacity-50\"\n ),\n {\n defaultVariants: {\n size: \"sm\",\n variant: \"default\",\n },\n variants: {\n size: {\n default: \"h-9 w-6\",\n lg: \"h-10 w-8\",\n sm: \"h-8 w-4\",\n },\n variant: {\n default:\n \"bg-transparent hover:bg-muted hover:text-muted-foreground aria-checked:bg-accent aria-checked:text-accent-foreground\",\n outline:\n \"border border-l-0 border-input bg-transparent hover:bg-accent hover:text-accent-foreground\",\n },\n },\n }\n);\n\ntype ToolbarButtonProps = {\n isDropdown?: boolean;\n pressed?: boolean;\n} & Omit<\n React.ComponentPropsWithoutRef<typeof ToolbarToggleItem>,\n \"asChild\" | \"value\"\n> &\n VariantProps<typeof toolbarButtonVariants>;\n\nexport const ToolbarButton = withTooltip(function ToolbarButton({\n children,\n className,\n isDropdown,\n pressed,\n size = \"sm\",\n variant,\n ...props\n}: ToolbarButtonProps) {\n return typeof pressed === \"boolean\" ? (\n <ToolbarToggleGroup disabled={props.disabled} value=\"single\" type=\"single\">\n <ToolbarToggleItem\n className={cn(\n toolbarButtonVariants({\n size,\n variant,\n }),\n isDropdown && \"justify-between gap-1 pr-1\",\n className\n )}\n value={pressed ? \"single\" : \"\"}\n {...props}\n >\n {isDropdown ? (\n <>\n <div className=\"flex flex-1 items-center gap-2 whitespace-nowrap\">\n {children}\n </div>\n <div>\n <ChevronDown\n className=\"size-3.5 text-muted-foreground\"\n data-icon\n />\n </div>\n </>\n ) : (\n children\n )}\n </ToolbarToggleItem>\n </ToolbarToggleGroup>\n ) : (\n <ToolbarPrimitive.Button\n className={cn(\n toolbarButtonVariants({\n size,\n variant,\n }),\n isDropdown && \"pr-1\",\n className\n )}\n {...props}\n >\n {children}\n </ToolbarPrimitive.Button>\n );\n});\n\nexport function ToolbarSplitButton({\n className,\n ...props\n}: React.ComponentPropsWithoutRef<typeof ToolbarButton>) {\n return (\n <ToolbarButton\n className={cn(\"group flex gap-0 px-0 hover:bg-transparent\", className)}\n {...props}\n />\n );\n}\n\ntype ToolbarSplitButtonPrimaryProps = Omit<\n React.ComponentPropsWithoutRef<typeof ToolbarToggleItem>,\n \"value\"\n> &\n VariantProps<typeof toolbarButtonVariants>;\n\nexport function ToolbarSplitButtonPrimary({\n children,\n className,\n size = \"sm\",\n variant,\n ...props\n}: ToolbarSplitButtonPrimaryProps) {\n return (\n <span\n className={cn(\n toolbarButtonVariants({\n size,\n variant,\n }),\n \"rounded-r-none\",\n \"group-data-[pressed=true]:bg-accent group-data-[pressed=true]:text-accent-foreground\",\n className\n )}\n {...props}\n >\n {children}\n </span>\n );\n}\n\nexport function ToolbarSplitButtonSecondary({\n className,\n size,\n variant,\n ...props\n}: React.ComponentPropsWithoutRef<\"span\"> &\n VariantProps<typeof dropdownArrowVariants>) {\n return (\n <span\n className={cn(\n dropdownArrowVariants({\n size,\n variant,\n }),\n \"group-data-[pressed=true]:bg-accent group-data-[pressed=true]:text-accent-foreground\",\n className\n )}\n onClick={(e) => e.stopPropagation()}\n role=\"button\"\n {...props}\n >\n <ChevronDown className=\"size-3.5 text-muted-foreground\" data-icon />\n </span>\n );\n}\n\nexport function ToolbarToggleItem({\n className,\n size = \"sm\",\n variant,\n ...props\n}: React.ComponentProps<typeof ToolbarPrimitive.ToggleItem> &\n VariantProps<typeof toolbarButtonVariants>) {\n return (\n <ToolbarPrimitive.ToggleItem\n className={cn(toolbarButtonVariants({size, variant}), className)}\n {...props}\n />\n );\n}\n\nexport function ToolbarGroup({\n children,\n className,\n}: React.ComponentProps<\"div\">) {\n return (\n <div\n className={cn(\n \"group/toolbar-group\",\n \"relative hidden has-[button]:flex\",\n className\n )}\n >\n <div className=\"flex items-center\">{children}</div>\n\n <div className=\"mx-1.5 py-0.5 group-last/toolbar-group:hidden!\">\n <Separator orientation=\"vertical\" />\n </div>\n </div>\n );\n}\n\ntype TooltipProps<T extends React.ElementType> = {\n tooltip?: React.ReactNode;\n tooltipContentProps?: Omit<\n React.ComponentPropsWithoutRef<typeof TooltipContent>,\n \"children\"\n >;\n tooltipProps?: Omit<\n React.ComponentPropsWithoutRef<typeof Tooltip>,\n \"children\"\n >;\n tooltipTriggerProps?: React.ComponentPropsWithoutRef<typeof TooltipTrigger>;\n} & React.ComponentProps<T>;\n\nfunction withTooltip<T extends React.ElementType>(Component: T) {\n return function ExtendComponent({\n tooltip,\n tooltipContentProps,\n tooltipProps,\n tooltipTriggerProps,\n ...props\n }: TooltipProps<T>) {\n const [mounted, setMounted] = React.useState(false);\n\n React.useEffect(() => {\n setMounted(true);\n }, []);\n\n const component = <Component {...(props as React.ComponentProps<T>)} />;\n\n if (tooltip && mounted) {\n return (\n <Tooltip {...tooltipProps}>\n <TooltipTrigger asChild {...tooltipTriggerProps}>\n {component}\n </TooltipTrigger>\n\n <TooltipContent {...tooltipContentProps}>{tooltip}</TooltipContent>\n </Tooltip>\n );\n }\n\n return component;\n };\n}\n\nfunction TooltipContent({\n children,\n className,\n // CHANGE\n sideOffset = 4,\n ...props\n}: React.ComponentProps<typeof TooltipPrimitive.Content>) {\n return (\n <TooltipPrimitive.Portal>\n <TooltipPrimitive.Content\n className={cn(\n \"z-50 w-fit origin-(--radix-tooltip-content-transform-origin) rounded-md bg-primary px-3 py-1.5 text-xs text-balance text-primary-foreground\",\n className\n )}\n data-slot=\"tooltip-content\"\n sideOffset={sideOffset}\n {...props}\n >\n {children}\n {/* CHANGE */}\n {/* <TooltipPrimitive.Arrow className=\"z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px] bg-primary fill-primary\" /> */}\n </TooltipPrimitive.Content>\n </TooltipPrimitive.Portal>\n );\n}\n\nexport function ToolbarMenuGroup({\n children,\n className,\n label,\n ...props\n}: React.ComponentProps<typeof DropdownMenuRadioGroup> & {label?: string}) {\n return (\n <>\n <DropdownMenuSeparator\n className={cn(\n \"hidden\",\n \"mb-0 shrink-0 peer-has-[[role=menuitem]]/menu-group:block peer-has-[[role=menuitemradio]]/menu-group:block peer-has-[[role=option]]/menu-group:block\"\n )}\n />\n\n <DropdownMenuRadioGroup\n {...props}\n className={cn(\n \"hidden\",\n \"peer/menu-group group/menu-group my-1.5 has-[[role=menuitem]]:block has-[[role=menuitemradio]]:block has-[[role=option]]:block\",\n className\n )}\n >\n {label && (\n <DropdownMenuLabel className=\"text-xs font-semibold text-muted-foreground select-none\">\n {label}\n </DropdownMenuLabel>\n )}\n {children}\n </DropdownMenuRadioGroup>\n </>\n );\n}\n",
65
- "type": "registry:ui"
66
- }
67
- ]
68
- }
package/r/registry.json DELETED
@@ -1,171 +0,0 @@
1
- {
2
- "$schema": "https://ui.shadcn.com/schema/registry.json",
3
- "name": "pihanga-shadcn",
4
- "homepage": "https://ivcap-works.github.io/pihanga-shadcn",
5
- "items": [
6
- {
7
- "name": "pihanga-base",
8
- "type": "registry:component"
9
- },
10
- {
11
- "name": "pihanga-lib-utils",
12
- "type": "registry:lib"
13
- },
14
- {
15
- "name": "pihanga-cards-icons",
16
- "type": "registry:component"
17
- },
18
- {
19
- "name": "pihanga-cards-types",
20
- "type": "registry:component"
21
- },
22
- {
23
- "name": "pihanga-theme-provider",
24
- "type": "registry:component"
25
- },
26
- {
27
- "name": "pihanga-hook-use-is-touch-device",
28
- "type": "registry:hook"
29
- },
30
- {
31
- "name": "pihanga-ui-extras",
32
- "type": "registry:ui"
33
- },
34
- {
35
- "name": "badge",
36
- "type": "registry:component"
37
- },
38
- {
39
- "name": "box",
40
- "type": "registry:component"
41
- },
42
- {
43
- "name": "button",
44
- "type": "registry:component"
45
- },
46
- {
47
- "name": "checkbox",
48
- "type": "registry:component"
49
- },
50
- {
51
- "name": "conditional",
52
- "type": "registry:component"
53
- },
54
- {
55
- "name": "dataTable",
56
- "type": "registry:component"
57
- },
58
- {
59
- "name": "dialog",
60
- "type": "registry:component"
61
- },
62
- {
63
- "name": "dropDownMenu",
64
- "type": "registry:component"
65
- },
66
- {
67
- "name": "field",
68
- "type": "registry:component"
69
- },
70
- {
71
- "name": "flexGrid",
72
- "type": "registry:component"
73
- },
74
- {
75
- "name": "form",
76
- "type": "registry:component"
77
- },
78
- {
79
- "name": "framework",
80
- "type": "registry:component"
81
- },
82
- {
83
- "name": "graphin",
84
- "type": "registry:component"
85
- },
86
- {
87
- "name": "input",
88
- "type": "registry:component"
89
- },
90
- {
91
- "name": "jsonViewer",
92
- "type": "registry:component"
93
- },
94
- {
95
- "name": "list",
96
- "type": "registry:component"
97
- },
98
- {
99
- "name": "loadingOverlay",
100
- "type": "registry:component"
101
- },
102
- {
103
- "name": "loadingSkeleton",
104
- "type": "registry:component"
105
- },
106
- {
107
- "name": "markdownViewer",
108
- "type": "registry:component"
109
- },
110
- {
111
- "name": "menu",
112
- "type": "registry:component"
113
- },
114
- {
115
- "name": "modeToggle",
116
- "type": "registry:component"
117
- },
118
- {
119
- "name": "navbarSearch",
120
- "type": "registry:component"
121
- },
122
- {
123
- "name": "pageWithNavbar",
124
- "type": "registry:component"
125
- },
126
- {
127
- "name": "pasteTarget",
128
- "type": "registry:component"
129
- },
130
- {
131
- "name": "resizable",
132
- "type": "registry:component"
133
- },
134
- {
135
- "name": "select",
136
- "type": "registry:component"
137
- },
138
- {
139
- "name": "stack",
140
- "type": "registry:component"
141
- },
142
- {
143
- "name": "stepper",
144
- "type": "registry:component"
145
- },
146
- {
147
- "name": "switch",
148
- "type": "registry:component"
149
- },
150
- {
151
- "name": "tabs",
152
- "type": "registry:component"
153
- },
154
- {
155
- "name": "textField",
156
- "type": "registry:component"
157
- },
158
- {
159
- "name": "toast",
160
- "type": "registry:component"
161
- },
162
- {
163
- "name": "toggleGroup",
164
- "type": "registry:component"
165
- },
166
- {
167
- "name": "typography",
168
- "type": "registry:component"
169
- }
170
- ]
171
- }