@loworbitstudio/visor 1.19.0 → 1.21.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/dist/CHANGELOG.json +1 -1
- package/dist/registry.json +59 -4
- package/dist/visor-manifest.json +60 -5
- package/package.json +1 -1
package/dist/CHANGELOG.json
CHANGED
package/dist/registry.json
CHANGED
|
@@ -141,12 +141,12 @@
|
|
|
141
141
|
{
|
|
142
142
|
"path": "components/ui/button/button.tsx",
|
|
143
143
|
"type": "registry:ui",
|
|
144
|
-
"content": "import * as React from \"react\"\nimport { Slot } from \"@radix-ui/react-slot\"\nimport { cva, type VariantProps } from \"class-variance-authority\"\nimport { cn } from \"../../../lib/utils\"\nimport styles from \"./button.module.css\"\nimport {\n Tooltip,\n TooltipContent,\n TooltipTrigger,\n} from \"../tooltip/tooltip\"\n\nconst buttonVariants = cva(styles.base, {\n variants: {\n variant: {\n default: styles.variantDefault,\n secondary: styles.variantSecondary,\n outline: styles.variantOutline,\n ghost: styles.variantGhost,\n destructive: styles.variantDestructive,\n },\n size: {\n sm: styles.sizeSm,\n md: styles.sizeMd,\n lg: styles.sizeLg,\n },\n },\n defaultVariants: {\n variant: \"default\",\n size: \"md\",\n },\n})\n\nexport interface ButtonProps\n extends React.ButtonHTMLAttributes<HTMLButtonElement>,\n VariantProps<typeof buttonVariants> {\n asChild?: boolean\n /**\n * When true, the button renders in an inert gated state — visually dimmed,\n * cursor not-allowed, and click handlers suppressed. Uses `aria-disabled`\n * instead of the native `disabled` attribute so the button remains\n * keyboard-focusable (required for the tooltip to be accessible).\n *\n * Note: a `<TooltipProvider>` ancestor is required when `gatedReason` is set.\n */\n gated?: boolean\n /**\n * Explanation surfaced in an anchored tooltip when the button is gated.\n * Only rendered when `gated` is also true.\n * Example: \"You can't delete this — you're not an owner\"\n */\n gatedReason?: string\n}\n\nconst Button = React.forwardRef<HTMLButtonElement, ButtonProps>(\n ({ className, variant, size, asChild, gated, gatedReason, onClick, ...props }, ref) => {\n const Comp = asChild ? Slot : \"button\"\n\n const handleClick = React.useCallback(\n (e: React.MouseEvent<HTMLButtonElement>) => {\n if (gated) return\n onClick?.(e)\n },\n [gated, onClick]\n )\n\n const button = (\n <Comp\n className={cn(buttonVariants({ variant, size }), className)}\n ref={ref}\n aria-disabled={gated ? true : undefined}\n data-gated={gated ? \"true\" : undefined}\n data-gated-reason={gated && gatedReason ? gatedReason : undefined}\n onClick={handleClick}\n {...props}\n />\n )\n\n if (gated && gatedReason) {\n return (\n <Tooltip>\n <TooltipTrigger asChild>{button}</TooltipTrigger>\n <TooltipContent>{gatedReason}</TooltipContent>\n </Tooltip>\n )\n }\n\n return button\n }\n)\nButton.displayName = \"Button\"\n\nexport { Button, buttonVariants }\n"
|
|
144
|
+
"content": "import * as React from \"react\"\nimport { Slot } from \"@radix-ui/react-slot\"\nimport { cva, type VariantProps } from \"class-variance-authority\"\nimport { cn } from \"../../../lib/utils\"\nimport styles from \"./button.module.css\"\nimport {\n Tooltip,\n TooltipContent,\n TooltipTrigger,\n} from \"../tooltip/tooltip\"\n\nconst buttonVariants = cva(styles.base, {\n variants: {\n variant: {\n default: styles.variantDefault,\n secondary: styles.variantSecondary,\n outline: styles.variantOutline,\n ghost: styles.variantGhost,\n destructive: styles.variantDestructive,\n },\n size: {\n sm: styles.sizeSm,\n md: styles.sizeMd,\n lg: styles.sizeLg,\n dlg: styles.sizeDlg,\n },\n },\n defaultVariants: {\n variant: \"default\",\n size: \"md\",\n },\n})\n\nexport interface ButtonProps\n extends React.ButtonHTMLAttributes<HTMLButtonElement>,\n VariantProps<typeof buttonVariants> {\n asChild?: boolean\n /**\n * When true, the button renders in an inert gated state — visually dimmed,\n * cursor not-allowed, and click handlers suppressed. Uses `aria-disabled`\n * instead of the native `disabled` attribute so the button remains\n * keyboard-focusable (required for the tooltip to be accessible).\n *\n * Note: a `<TooltipProvider>` ancestor is required when `gatedReason` is set.\n */\n gated?: boolean\n /**\n * Explanation surfaced in an anchored tooltip when the button is gated.\n * Only rendered when `gated` is also true.\n * Example: \"You can't delete this — you're not an owner\"\n */\n gatedReason?: string\n}\n\nconst Button = React.forwardRef<HTMLButtonElement, ButtonProps>(\n ({ className, variant, size, asChild, gated, gatedReason, onClick, ...props }, ref) => {\n const Comp = asChild ? Slot : \"button\"\n\n const handleClick = React.useCallback(\n (e: React.MouseEvent<HTMLButtonElement>) => {\n if (gated) return\n onClick?.(e)\n },\n [gated, onClick]\n )\n\n const button = (\n <Comp\n className={cn(buttonVariants({ variant, size }), className)}\n ref={ref}\n aria-disabled={gated ? true : undefined}\n data-gated={gated ? \"true\" : undefined}\n data-gated-reason={gated && gatedReason ? gatedReason : undefined}\n onClick={handleClick}\n {...props}\n />\n )\n\n if (gated && gatedReason) {\n return (\n <Tooltip>\n <TooltipTrigger asChild>{button}</TooltipTrigger>\n <TooltipContent>{gatedReason}</TooltipContent>\n </Tooltip>\n )\n }\n\n return button\n }\n)\nButton.displayName = \"Button\"\n\nexport { Button, buttonVariants }\n"
|
|
145
145
|
},
|
|
146
146
|
{
|
|
147
147
|
"path": "components/ui/button/button.module.css",
|
|
148
148
|
"type": "registry:ui",
|
|
149
|
-
"content": "/* Button base styles */\n.base {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n gap: var(--spacing-2, 0.5rem);\n white-space: nowrap;\n border-radius: var(--radius-md, 0.375rem);\n font-size: var(--font-size-sm, 0.875rem);\n font-weight: var(--font-weight-medium, 500);\n line-height: 1;\n cursor: pointer;\n border: 1px solid transparent;\n transition: background-color var(--motion-duration-150, 150ms) var(--motion-easing-default, ease-in-out), color var(--motion-duration-150, 150ms) var(--motion-easing-default, ease-in-out), border-color var(--motion-duration-150, 150ms) var(--motion-easing-default, ease-in-out), opacity var(--motion-duration-150, 150ms) var(--motion-easing-default, ease-in-out), transform var(--motion-duration-150, 150ms) var(--motion-easing-default, ease-in-out);\n text-decoration: none;\n outline: none;\n}\n\n.base:focus-visible {\n outline: var(--focus-ring-width, 2px) solid var(--border-focus, currentColor);\n outline-offset: var(--focus-ring-offset, 2px);\n}\n\n.base:active:not(:disabled) {\n transform: translateY(1px);\n transition: none;\n}\n\n.base:disabled {\n pointer-events: none;\n opacity: 0.5;\n}\n\n/* Variants */\n.variantDefault {\n background-color: var(--interactive-primary-bg, var(--primary, #111827));\n color: var(--interactive-primary-text, #f9fafb);\n}\n\n.variantSecondary {\n background-color: var(--interactive-secondary-bg, #f3f4f6);\n color: var(--interactive-secondary-text, #111827);\n}\n\n/* Editorial density: secondary on the card tier, ghost truly transparent.\n * Direct values — the editorial design language owns control surfaces, so the\n * theme's --interactive-secondary-bg / --interactive-ghost-bg are deliberately\n * bypassed (matches the blessed overlay, which out-cascaded the palette). */\n:global([data-density=\"editorial\"]) .variantSecondary {\n background-color: var(--surface-card, #ffffff);\n}\n\n:global([data-density=\"editorial\"]) .variantGhost {\n background-color: transparent;\n}\n\n.variantOutline {\n background-color: transparent;\n color: var(--text-primary, #111827);\n border-color: var(--border-strong, #e5e7eb);\n}\n\n.variantGhost {\n background-color: var(--interactive-ghost-bg, transparent);\n color: var(--text-primary, #111827);\n}\n\n.variantDestructive {\n background-color: var(--interactive-destructive-bg, #ef4444);\n color: var(--interactive-destructive-text, #fef2f2);\n}\n\n@media (hover: hover) {\n .variantDefault:hover {\n background-color: var(--interactive-primary-bg-hover, #1f2937);\n }\n\n .variantSecondary:hover {\n background-color: var(--interactive-secondary-bg-hover, #e5e7eb);\n }\n\n .variantOutline:hover {\n background-color: var(--surface-muted, #f3f4f6);\n }\n\n .variantGhost:hover {\n background-color: var(--interactive-ghost-bg-hover, var(--surface-muted, #f3f4f6));\n }\n\n .variantDestructive:hover {\n background-color: var(--interactive-destructive-bg-hover, #dc2626);\n }\n}\n\n/* Open / active state — ghost variant only. OPT-IN, strictly additive.\n A ghost Button reads as \"held\" while its popover/menu is open so the\n trigger no longer needs an inline style. Three opt-in triggers, all\n resolving to the same held look:\n - data-state=\"open\" — forwarded automatically by Radix when the\n Button is the trigger via <Trigger asChild> (Popover/DropdownMenu).\n - data-active=\"true\" — manual non-Radix triggers (mirrors the\n sidebar `data-active` convention).\n - .isActive — className opt-in (mirrors sidebar/section-nav).\n Default ghost rendering is untouched: none of these match unless a\n consumer explicitly sets them. Matches the prototype-overlay held look:\n background var(--surface-subtle), color var(--text-primary). */\n.variantGhost[data-state=\"open\"],\n.variantGhost[data-active=\"true\"],\n.variantGhost.isActive {\n background-color: var(--surface-subtle, var(--surface-muted, #f3f4f6));\n color: var(--text-primary, #111827);\n}\n\n/* Gated state — orthogonal to variant, works with all variants */\n.base[data-gated=\"true\"] {\n opacity: var(--opacity-40, 0.4);\n cursor: not-allowed;\n border-color: var(--border-default, #e5e7eb);\n}\n\n/* Editorial density: gated reads like plain disabled (the blessed admin\n builds render gated actions at the 0.5 disabled opacity). */\n:global([data-density=\"editorial\"]) .base[data-gated=\"true\"] {\n opacity: 0.5;\n}\n\n/* Suppress active transform when gated */\n.base[data-gated=\"true\"]:active {\n transform: none;\n transition: opacity var(--motion-duration-150, 150ms) var(--motion-easing-default, ease-in-out);\n}\n\n/* Sizes\n Default (no data-density attr) rendering is byte-identical to the prior\n canonical values. Under data-density=\"editorial\" (set on any ancestor,\n typically the app root), the editorial density axis re-drives specific\n metrics to the blessed admin values. */\n.sizeSm {\n height: 2rem;\n padding: 0 var(--spacing-3, 0.75rem);\n font-size: var(--font-size-xs, 0.75rem);\n}\n\n.sizeMd {\n height: 2.5rem;\n padding: 0 var(--spacing-4, 1rem);\n line-height: 1;\n}\n\n:global([data-density=\"editorial\"]) .sizeSm {\n font-size: 0.8125rem;\n}\n\n:global([data-density=\"editorial\"]) .sizeMd {\n height: 2.125rem;\n line-height: normal;\n}\n\n.sizeLg {\n height: 3rem;\n padding: 0 var(--spacing-6, 1.5rem);\n font-size: var(--font-size-base, 1rem);\n}\n"
|
|
149
|
+
"content": "/* Button base styles */\n.base {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n gap: var(--spacing-2, 0.5rem);\n white-space: nowrap;\n border-radius: var(--radius-md, 0.375rem);\n font-size: var(--font-size-sm, 0.875rem);\n font-weight: var(--font-weight-medium, 500);\n line-height: 1;\n cursor: pointer;\n border: 1px solid transparent;\n transition: background-color var(--motion-duration-150, 150ms) var(--motion-easing-default, ease-in-out), color var(--motion-duration-150, 150ms) var(--motion-easing-default, ease-in-out), border-color var(--motion-duration-150, 150ms) var(--motion-easing-default, ease-in-out), opacity var(--motion-duration-150, 150ms) var(--motion-easing-default, ease-in-out), transform var(--motion-duration-150, 150ms) var(--motion-easing-default, ease-in-out);\n text-decoration: none;\n outline: none;\n}\n\n.base:focus-visible {\n outline: var(--focus-ring-width, 2px) solid var(--border-focus, currentColor);\n outline-offset: var(--focus-ring-offset, 2px);\n}\n\n.base:active:not(:disabled) {\n transform: translateY(1px);\n transition: none;\n}\n\n.base:disabled {\n pointer-events: none;\n opacity: 0.5;\n}\n\n/* Variants */\n.variantDefault {\n background-color: var(--interactive-primary-bg, var(--primary, #111827));\n color: var(--interactive-primary-text, #f9fafb);\n}\n\n.variantSecondary {\n background-color: var(--interactive-secondary-bg, #f3f4f6);\n color: var(--interactive-secondary-text, #111827);\n}\n\n/* Editorial density: secondary on the card tier, ghost truly transparent.\n * Direct values — the editorial design language owns control surfaces, so the\n * theme's --interactive-secondary-bg / --interactive-ghost-bg are deliberately\n * bypassed (matches the blessed overlay, which out-cascaded the palette). */\n:global([data-density=\"editorial\"]) .variantSecondary {\n background-color: var(--surface-card, #ffffff);\n}\n\n:global([data-density=\"editorial\"]) .variantGhost {\n background-color: transparent;\n}\n\n.variantOutline {\n background-color: transparent;\n color: var(--text-primary, #111827);\n border-color: var(--border-strong, #e5e7eb);\n}\n\n.variantGhost {\n background-color: var(--interactive-ghost-bg, transparent);\n color: var(--text-primary, #111827);\n}\n\n.variantDestructive {\n background-color: var(--interactive-destructive-bg, #ef4444);\n color: var(--interactive-destructive-text, #fef2f2);\n}\n\n@media (hover: hover) {\n .variantDefault:hover {\n background-color: var(--interactive-primary-bg-hover, #1f2937);\n }\n\n .variantSecondary:hover {\n background-color: var(--interactive-secondary-bg-hover, #e5e7eb);\n }\n\n .variantOutline:hover {\n background-color: var(--surface-muted, #f3f4f6);\n }\n\n .variantGhost:hover {\n background-color: var(--interactive-ghost-bg-hover, var(--surface-muted, #f3f4f6));\n }\n\n .variantDestructive:hover {\n background-color: var(--interactive-destructive-bg-hover, #dc2626);\n }\n}\n\n/* Open / active state — ghost variant only. OPT-IN, strictly additive.\n A ghost Button reads as \"held\" while its popover/menu is open so the\n trigger no longer needs an inline style. Three opt-in triggers, all\n resolving to the same held look:\n - data-state=\"open\" — forwarded automatically by Radix when the\n Button is the trigger via <Trigger asChild> (Popover/DropdownMenu).\n - data-active=\"true\" — manual non-Radix triggers (mirrors the\n sidebar `data-active` convention).\n - .isActive — className opt-in (mirrors sidebar/section-nav).\n Default ghost rendering is untouched: none of these match unless a\n consumer explicitly sets them. Matches the prototype-overlay held look:\n background var(--surface-subtle), color var(--text-primary). */\n.variantGhost[data-state=\"open\"],\n.variantGhost[data-active=\"true\"],\n.variantGhost.isActive {\n background-color: var(--surface-subtle, var(--surface-muted, #f3f4f6));\n color: var(--text-primary, #111827);\n}\n\n/* Gated state — orthogonal to variant, works with all variants */\n.base[data-gated=\"true\"] {\n opacity: var(--opacity-40, 0.4);\n cursor: not-allowed;\n border-color: var(--border-default, #e5e7eb);\n}\n\n/* Editorial density: gated reads like plain disabled (the blessed admin\n builds render gated actions at the 0.5 disabled opacity). */\n:global([data-density=\"editorial\"]) .base[data-gated=\"true\"] {\n opacity: 0.5;\n}\n\n/* Suppress active transform when gated */\n.base[data-gated=\"true\"]:active {\n transform: none;\n transition: opacity var(--motion-duration-150, 150ms) var(--motion-easing-default, ease-in-out);\n}\n\n/* Sizes\n Default (no data-density attr) rendering is byte-identical to the prior\n canonical values. Under data-density=\"editorial\" (set on any ancestor,\n typically the app root), the editorial density axis re-drives specific\n metrics to the blessed admin values. */\n.sizeSm {\n height: 2rem;\n padding: 0 var(--spacing-3, 0.75rem);\n font-size: var(--font-size-xs, 0.75rem);\n}\n\n.sizeMd {\n height: 2.5rem;\n padding: 0 var(--spacing-4, 1rem);\n line-height: 1;\n}\n\n:global([data-density=\"editorial\"]) .sizeSm {\n font-size: 0.8125rem;\n}\n\n:global([data-density=\"editorial\"]) .sizeMd {\n height: 2.125rem;\n line-height: normal;\n}\n\n.sizeLg {\n height: 3rem;\n padding: 0 var(--spacing-6, 1.5rem);\n font-size: var(--font-size-base, 1rem);\n}\n\n/* Dialog-footer button — dlg-btn (Animal dialog substrate, VI-620).\n Compact modal-action metrics measured from the AN prototype (11.5px / 600,\n ~7px 13px padding, 7px radius), normalized onto Visor tokens: font-size-2xs\n (11px), spacing-2/spacing-3 padding (8/12px), radius-lg (8px). Height is\n auto so the compact padding drives the box. Color faces reuse the existing\n variant axis — primary via variant=\"default\", ghost via variant=\"ghost\"\n (refined below to a hairline-outlined, secondary-ink face), danger via\n variant=\"destructive\" (refined below to a subtle-danger face). */\n.sizeDlg {\n height: auto;\n padding: var(--spacing-2, 0.5rem) var(--spacing-3, 0.75rem);\n font-size: var(--font-size-2xs, 0.6875rem);\n font-weight: var(--font-weight-semibold, 600);\n line-height: 1;\n border-radius: var(--radius-lg, 0.5rem);\n}\n\n/* dlg ghost — transparent well with a hairline outline + secondary ink\n (AN dlg-btn ghost face: transparent / --ink-60 / 1px --hair). Two-class\n specificity keeps this scoped to the dlg size only. */\n.sizeDlg.variantGhost {\n color: var(--text-secondary, #6b7280);\n border-color: var(--hairline, var(--border-default, #e5e7eb));\n}\n\n/* dlg danger — subtle-danger face (AN dlg-btn danger face: --red on a faint\n red fill), distinct from the solid `destructive` variant. */\n.sizeDlg.variantDestructive {\n background-color: var(--surface-error-subtle, color-mix(in srgb, var(--destructive, #ef4444) 8%, transparent));\n color: var(--text-error, var(--destructive, #b91c1c));\n}\n\n@media (hover: hover) {\n .sizeDlg.variantDestructive:hover {\n background-color: color-mix(in srgb, var(--destructive, #ef4444) 14%, transparent);\n }\n}\n"
|
|
150
150
|
}
|
|
151
151
|
]
|
|
152
152
|
},
|
|
@@ -3012,12 +3012,12 @@
|
|
|
3012
3012
|
{
|
|
3013
3013
|
"path": "components/ui/status-badge/status-badge.tsx",
|
|
3014
3014
|
"type": "registry:ui",
|
|
3015
|
-
"content": "import * as React from \"react\"\nimport { cn } from \"../../../lib/utils\"\nimport { Badge } from \"../badge/badge\"\nimport type { BadgeProps } from \"../badge/badge\"\nimport styles from \"./status-badge.module.css\"\n\nexport type StatusBadgeStatus =\n | \"healthy\"\n | \"degraded\"\n | \"down\"\n | \"failed\"\n | \"running\"\n | \"pending\"\n | \"queued\"\n | \"idle\"\n | \"complete\"\n | \"live\"\n | \"warn\"\n | \"scheduled\"\n | \"sold\"\n | \"draft\"\n | \"prospect\"\n | \"pitched\"\n | \"contracted\"\n | \"active\"\n | \"paused\"\n | \"completed\"\n | \"archived\"\n\nexport type StatusBadgeTone = \"subtle\" | \"filled\"\n\nexport interface StatusBadgeProps\n extends Omit<React.HTMLAttributes<HTMLSpanElement>, \"children\"> {\n /** Semantic admin status. Drives both color and default label. */\n status: StatusBadgeStatus\n /** Visible text. Defaults to the capitalized status key. */\n label?: React.ReactNode\n /** Which Badge variant family to use. Defaults to \"subtle\". */\n tone?: StatusBadgeTone\n /** Render the leading indicator dot. Defaults to true. */\n indicator?: boolean\n /** Animate the indicator dot with a soft pulse. Defaults to false. */\n pulse?: boolean\n}\n\n/**\n * Default human-readable labels for each status. Exported so consumers can\n * localize or override without reimplementing the map.\n */\nexport const statusBadgeLabels: Record<StatusBadgeStatus, string> = {\n healthy: \"Healthy\",\n degraded: \"Degraded\",\n down: \"Down\",\n failed: \"Failed\",\n running: \"Running\",\n pending: \"Pending\",\n queued: \"Queued\",\n idle: \"Idle\",\n complete: \"Complete\",\n live: \"Live\",\n warn: \"Warn\",\n scheduled: \"Scheduled\",\n sold: \"Sold\",\n draft: \"Draft\",\n prospect: \"Prospect\",\n pitched: \"Pitched\",\n contracted: \"Contracted\",\n active: \"Active\",\n paused: \"Paused\",\n completed: \"Completed\",\n archived: \"Archived\",\n}\n\ntype BadgeVariant = NonNullable<BadgeProps[\"variant\"]>\n\n/**\n * Color group keyed on the semantic status. Used to pick the indicator dot\n * class and the underlying Badge variant.\n */\ntype StatusColorGroup =\n | \"success\"\n | \"warning\"\n | \"destructive\"\n | \"info\"\n | \"neutral\"\n\nconst STATUS_COLOR_GROUP: Record<StatusBadgeStatus, StatusColorGroup> = {\n healthy: \"success\",\n complete: \"success\",\n degraded: \"warning\",\n pending: \"warning\",\n down: \"destructive\",\n failed: \"destructive\",\n running: \"info\",\n queued: \"neutral\",\n idle: \"neutral\",\n // Admin-ui event tones — map to existing semantic groups.\n // live: active/in-progress positive event → success accent\n // warn: needs attention but not failing → warning\n // scheduled: upcoming/committed, distinct from draft's muted grey → info\n // sold: positive completed outcome → success\n // draft: unpublished/muted → neutral\n live: \"success\",\n warn: \"warning\",\n scheduled: \"info\",\n sold: \"success\",\n draft: \"neutral\",\n // CRM / pipeline stages (VI-492) — bind to existing semantic groups.\n // No new tokens: stages that share a meaning share a color.\n // prospect: new informational lead → info\n // pitched: awaiting response, needs attention → warning\n // contracted / active / completed: positive, in-good-standing → success\n // paused: temporarily on hold, needs attention → warning\n // archived: closed/muted, grouped with draft → neutral\n prospect: \"info\",\n pitched: \"warning\",\n contracted: \"success\",\n active: \"success\",\n paused: \"warning\",\n completed: \"success\",\n archived: \"neutral\",\n}\n\nconst SUBTLE_VARIANT: Record<StatusColorGroup, BadgeVariant> = {\n success: \"success\",\n warning: \"warning\",\n destructive: \"destructive\",\n info: \"info\",\n neutral: \"neutral\",\n}\n\nconst FILLED_VARIANT: Record<StatusColorGroup, BadgeVariant> = {\n success: \"filled-success\",\n warning: \"filled-warning\",\n destructive: \"filled-destructive\",\n info: \"filled-info\",\n neutral: \"filled-neutral\",\n}\n\nconst INDICATOR_CLASS: Record<StatusColorGroup, string> = {\n success: styles.indicatorSuccess,\n warning: styles.indicatorWarning,\n destructive: styles.indicatorDestructive,\n info: styles.indicatorInfo,\n neutral: styles.indicatorNeutral,\n}\n\nconst StatusBadge = React.forwardRef<HTMLSpanElement, StatusBadgeProps>(\n (\n {\n className,\n status,\n label,\n tone = \"subtle\",\n indicator = true,\n pulse = false,\n ...props\n },\n ref\n ) => {\n const group = STATUS_COLOR_GROUP[status]\n const variant: BadgeVariant =\n tone === \"filled\" ? FILLED_VARIANT[group] : SUBTLE_VARIANT[group]\n const visibleLabel = label ?? statusBadgeLabels[status]\n\n return (\n <Badge\n ref={ref}\n variant={variant}\n data-slot=\"status-badge\"\n data-status={status}\n data-tone={tone}\n className={cn(className)}\n {...props}\n >\n {indicator ? (\n <span\n data-slot=\"status-badge-indicator\"\n aria-hidden=\"true\"\n className={cn(\n styles.indicator,\n INDICATOR_CLASS[group],\n pulse && styles.pulse\n )}\n />\n ) : null}\n <span className={styles.srOnly}>Status: </span>\n <span
|
|
3015
|
+
"content": "import * as React from \"react\"\nimport { cn } from \"../../../lib/utils\"\nimport { Badge } from \"../badge/badge\"\nimport type { BadgeProps } from \"../badge/badge\"\nimport styles from \"./status-badge.module.css\"\n\nexport type StatusBadgeStatus =\n | \"healthy\"\n | \"degraded\"\n | \"down\"\n | \"failed\"\n | \"running\"\n | \"pending\"\n | \"queued\"\n | \"idle\"\n | \"complete\"\n | \"live\"\n | \"warn\"\n | \"scheduled\"\n | \"sold\"\n | \"draft\"\n | \"prospect\"\n | \"pitched\"\n | \"contracted\"\n | \"active\"\n | \"paused\"\n | \"completed\"\n | \"archived\"\n\nexport type StatusBadgeTone = \"subtle\" | \"filled\"\n\nexport interface StatusBadgeProps\n extends Omit<React.HTMLAttributes<HTMLSpanElement>, \"children\"> {\n /** Semantic admin status. Drives both color and default label. */\n status: StatusBadgeStatus\n /** Visible text. Defaults to the capitalized status key. */\n label?: React.ReactNode\n /** Which Badge variant family to use. Defaults to \"subtle\". */\n tone?: StatusBadgeTone\n /** Render the leading indicator dot. Defaults to true. */\n indicator?: boolean\n /** Animate the indicator dot with a soft pulse. Defaults to false. */\n pulse?: boolean\n /**\n * Render the label in the monospace token face (`--font-mono`, 11px) — the\n * dot + mono-token treatment from the Animal dialog substrate (VI-620), for\n * ID / code / token-style status readouts. Defaults to false.\n */\n mono?: boolean\n}\n\n/**\n * Default human-readable labels for each status. Exported so consumers can\n * localize or override without reimplementing the map.\n */\nexport const statusBadgeLabels: Record<StatusBadgeStatus, string> = {\n healthy: \"Healthy\",\n degraded: \"Degraded\",\n down: \"Down\",\n failed: \"Failed\",\n running: \"Running\",\n pending: \"Pending\",\n queued: \"Queued\",\n idle: \"Idle\",\n complete: \"Complete\",\n live: \"Live\",\n warn: \"Warn\",\n scheduled: \"Scheduled\",\n sold: \"Sold\",\n draft: \"Draft\",\n prospect: \"Prospect\",\n pitched: \"Pitched\",\n contracted: \"Contracted\",\n active: \"Active\",\n paused: \"Paused\",\n completed: \"Completed\",\n archived: \"Archived\",\n}\n\ntype BadgeVariant = NonNullable<BadgeProps[\"variant\"]>\n\n/**\n * Color group keyed on the semantic status. Used to pick the indicator dot\n * class and the underlying Badge variant.\n */\ntype StatusColorGroup =\n | \"success\"\n | \"warning\"\n | \"destructive\"\n | \"info\"\n | \"neutral\"\n\nconst STATUS_COLOR_GROUP: Record<StatusBadgeStatus, StatusColorGroup> = {\n healthy: \"success\",\n complete: \"success\",\n degraded: \"warning\",\n pending: \"warning\",\n down: \"destructive\",\n failed: \"destructive\",\n running: \"info\",\n queued: \"neutral\",\n idle: \"neutral\",\n // Admin-ui event tones — map to existing semantic groups.\n // live: active/in-progress positive event → success accent\n // warn: needs attention but not failing → warning\n // scheduled: upcoming/committed, distinct from draft's muted grey → info\n // sold: positive completed outcome → success\n // draft: unpublished/muted → neutral\n live: \"success\",\n warn: \"warning\",\n scheduled: \"info\",\n sold: \"success\",\n draft: \"neutral\",\n // CRM / pipeline stages (VI-492) — bind to existing semantic groups.\n // No new tokens: stages that share a meaning share a color.\n // prospect: new informational lead → info\n // pitched: awaiting response, needs attention → warning\n // contracted / active / completed: positive, in-good-standing → success\n // paused: temporarily on hold, needs attention → warning\n // archived: closed/muted, grouped with draft → neutral\n prospect: \"info\",\n pitched: \"warning\",\n contracted: \"success\",\n active: \"success\",\n paused: \"warning\",\n completed: \"success\",\n archived: \"neutral\",\n}\n\nconst SUBTLE_VARIANT: Record<StatusColorGroup, BadgeVariant> = {\n success: \"success\",\n warning: \"warning\",\n destructive: \"destructive\",\n info: \"info\",\n neutral: \"neutral\",\n}\n\nconst FILLED_VARIANT: Record<StatusColorGroup, BadgeVariant> = {\n success: \"filled-success\",\n warning: \"filled-warning\",\n destructive: \"filled-destructive\",\n info: \"filled-info\",\n neutral: \"filled-neutral\",\n}\n\nconst INDICATOR_CLASS: Record<StatusColorGroup, string> = {\n success: styles.indicatorSuccess,\n warning: styles.indicatorWarning,\n destructive: styles.indicatorDestructive,\n info: styles.indicatorInfo,\n neutral: styles.indicatorNeutral,\n}\n\nconst StatusBadge = React.forwardRef<HTMLSpanElement, StatusBadgeProps>(\n (\n {\n className,\n status,\n label,\n tone = \"subtle\",\n indicator = true,\n pulse = false,\n mono = false,\n ...props\n },\n ref\n ) => {\n const group = STATUS_COLOR_GROUP[status]\n const variant: BadgeVariant =\n tone === \"filled\" ? FILLED_VARIANT[group] : SUBTLE_VARIANT[group]\n const visibleLabel = label ?? statusBadgeLabels[status]\n\n return (\n <Badge\n ref={ref}\n variant={variant}\n data-slot=\"status-badge\"\n data-status={status}\n data-tone={tone}\n className={cn(className)}\n {...props}\n >\n {indicator ? (\n <span\n data-slot=\"status-badge-indicator\"\n aria-hidden=\"true\"\n className={cn(\n styles.indicator,\n INDICATOR_CLASS[group],\n pulse && styles.pulse\n )}\n />\n ) : null}\n <span className={styles.srOnly}>Status: </span>\n <span\n data-slot=\"status-badge-label\"\n data-mono={mono ? \"true\" : undefined}\n className={cn(mono && styles.labelMono)}\n >\n {visibleLabel}\n </span>\n </Badge>\n )\n }\n)\nStatusBadge.displayName = \"StatusBadge\"\n\nexport { StatusBadge }\n"
|
|
3016
3016
|
},
|
|
3017
3017
|
{
|
|
3018
3018
|
"path": "components/ui/status-badge/status-badge.module.css",
|
|
3019
3019
|
"type": "registry:ui",
|
|
3020
|
-
"content": "/*\n * StatusBadge — semantic wrapper over Badge.\n *\n * These styles only cover the additions on top of Badge: the leading\n * indicator dot, its per-status color, the optional pulse animation, and\n * the visually-hidden \"Status:\" prefix. All visual chrome (padding, radius,\n * typography, background) is inherited from Badge.\n */\n\n/* Indicator dot — filled circle, 0.5rem, colored per status */\n.indicator {\n display: inline-block;\n width: 0.5rem;\n height: 0.5rem;\n flex-shrink: 0;\n border-radius: var(--radius-full, 9999px);\n background-color: currentColor;\n}\n\n/* Per-color-group fills. Use brand surface default tokens so the dot\n * reads as a saturated status color independent of the Badge subtle bg. */\n.indicatorSuccess {\n background-color: var(--surface-success-default, transparent);\n}\n\n.indicatorWarning {\n background-color: var(--surface-warning-default, transparent);\n}\n\n.indicatorDestructive {\n background-color: var(--surface-error-default, transparent);\n}\n\n.indicatorInfo {\n background-color: var(--surface-info-default, transparent);\n}\n\n.indicatorNeutral {\n background-color: var(--text-tertiary, currentColor);\n}\n\n/* In filled tone, the dot sits on top of a saturated brand fill —\n * paint it with the same inverse token the Badge uses for its text\n * so it's visible in every theme. */\n[data-tone=\"filled\"] .indicatorSuccess,\n[data-tone=\"filled\"] .indicatorWarning,\n[data-tone=\"filled\"] .indicatorDestructive,\n[data-tone=\"filled\"] .indicatorInfo {\n background-color: var(--text-inverse, currentColor);\n}\n\n/* Neutral's filled fill (filled-neutral) is a fixed dark gray in both modes,\n * so its dot stays white rather than following --text-inverse (which flips\n * dark in dark mode). */\n[data-tone=\"filled\"] .indicatorNeutral {\n background-color: var(--color-white, #ffffff);\n}\n\n/* Pulse animation — soft opacity + scale.\n * Keyframes are defined locally to keep the component self-contained. */\n.pulse {\n /* Doubled 800ms token gives ~1.6s per cycle — slow enough to read as a\n * heartbeat without drawing focus away from content. */\n animation: status-badge-pulse\n calc(var(--motion-duration-800, 800ms) * 2)\n var(--motion-easing-standard, ease-in-out) infinite;\n transform-origin: center;\n will-change: opacity, transform;\n}\n\n@keyframes status-badge-pulse {\n 0%,\n 100% {\n opacity: 1;\n transform: scale(1);\n }\n 50% {\n opacity: 0.55;\n transform: scale(0.85);\n }\n}\n\n/* Respect reduced motion */\n@media (prefers-reduced-motion: reduce) {\n .pulse {\n animation: none;\n }\n}\n\n/* Visually hidden but announced to screen readers */\n.srOnly {\n position: absolute;\n width: 1px;\n height: 1px;\n padding: 0;\n margin: -1px;\n overflow: hidden;\n clip: rect(0, 0, 0, 0);\n white-space: nowrap;\n border: 0;\n}\n"
|
|
3020
|
+
"content": "/*\n * StatusBadge — semantic wrapper over Badge.\n *\n * These styles only cover the additions on top of Badge: the leading\n * indicator dot, its per-status color, the optional pulse animation, and\n * the visually-hidden \"Status:\" prefix. All visual chrome (padding, radius,\n * typography, background) is inherited from Badge.\n */\n\n/* Indicator dot — filled circle, 0.5rem, colored per status */\n.indicator {\n display: inline-block;\n width: 0.5rem;\n height: 0.5rem;\n flex-shrink: 0;\n border-radius: var(--radius-full, 9999px);\n background-color: currentColor;\n}\n\n/* Per-color-group fills. Use brand surface default tokens so the dot\n * reads as a saturated status color independent of the Badge subtle bg. */\n.indicatorSuccess {\n background-color: var(--surface-success-default, transparent);\n}\n\n.indicatorWarning {\n background-color: var(--surface-warning-default, transparent);\n}\n\n.indicatorDestructive {\n background-color: var(--surface-error-default, transparent);\n}\n\n.indicatorInfo {\n background-color: var(--surface-info-default, transparent);\n}\n\n.indicatorNeutral {\n background-color: var(--text-tertiary, currentColor);\n}\n\n/* In filled tone, the dot sits on top of a saturated brand fill —\n * paint it with the same inverse token the Badge uses for its text\n * so it's visible in every theme. */\n[data-tone=\"filled\"] .indicatorSuccess,\n[data-tone=\"filled\"] .indicatorWarning,\n[data-tone=\"filled\"] .indicatorDestructive,\n[data-tone=\"filled\"] .indicatorInfo {\n background-color: var(--text-inverse, currentColor);\n}\n\n/* Neutral's filled fill (filled-neutral) is a fixed dark gray in both modes,\n * so its dot stays white rather than following --text-inverse (which flips\n * dark in dark mode). */\n[data-tone=\"filled\"] .indicatorNeutral {\n background-color: var(--color-white, #ffffff);\n}\n\n/* Pulse animation — soft opacity + scale.\n * Keyframes are defined locally to keep the component self-contained. */\n.pulse {\n /* Doubled 800ms token gives ~1.6s per cycle — slow enough to read as a\n * heartbeat without drawing focus away from content. */\n animation: status-badge-pulse\n calc(var(--motion-duration-800, 800ms) * 2)\n var(--motion-easing-standard, ease-in-out) infinite;\n transform-origin: center;\n will-change: opacity, transform;\n}\n\n@keyframes status-badge-pulse {\n 0%,\n 100% {\n opacity: 1;\n transform: scale(1);\n }\n 50% {\n opacity: 0.55;\n transform: scale(0.85);\n }\n}\n\n/* Respect reduced motion */\n@media (prefers-reduced-motion: reduce) {\n .pulse {\n animation: none;\n }\n}\n\n/* Mono label variant (Animal dialog substrate, VI-620) — monospace token\n * label at the 11px chrome step, for ID / code / token-style status readouts.\n * Overrides only the label typography; dot, color, and chrome are unchanged. */\n.labelMono {\n font-family: var(--font-mono, \"SF Mono\", \"Fira Code\", \"Fira Mono\", monospace);\n font-size: var(--text-11, 0.6875rem);\n letter-spacing: 0.02em;\n}\n\n/* Visually hidden but announced to screen readers */\n.srOnly {\n position: absolute;\n width: 1px;\n height: 1px;\n padding: 0;\n margin: -1px;\n overflow: hidden;\n clip: rect(0, 0, 0, 0);\n white-space: nowrap;\n border: 0;\n}\n"
|
|
3021
3021
|
}
|
|
3022
3022
|
]
|
|
3023
3023
|
},
|
|
@@ -4098,6 +4098,61 @@
|
|
|
4098
4098
|
}
|
|
4099
4099
|
]
|
|
4100
4100
|
},
|
|
4101
|
+
{
|
|
4102
|
+
"name": "dialog-form",
|
|
4103
|
+
"type": "registry:block",
|
|
4104
|
+
"description": "Compact admin modal shell (Animal dialog substrate) composing the Dialog atom into a backdrop + centered panel + header + footer. Titled header with a size axis (13px/16px), a body stack for DialogField blocks, and a right-aligned dlg-btn footer row. Token-driven — the theme swaps surface, hairline, radius, and title scale.",
|
|
4105
|
+
"category": "overlay",
|
|
4106
|
+
"dependencies": [
|
|
4107
|
+
"@radix-ui/react-dialog",
|
|
4108
|
+
"@phosphor-icons/react",
|
|
4109
|
+
"@loworbitstudio/visor-core"
|
|
4110
|
+
],
|
|
4111
|
+
"registryDependencies": [
|
|
4112
|
+
"utils",
|
|
4113
|
+
"dialog",
|
|
4114
|
+
"button"
|
|
4115
|
+
],
|
|
4116
|
+
"files": [
|
|
4117
|
+
{
|
|
4118
|
+
"path": "blocks/dialog-form/dialog-form.tsx",
|
|
4119
|
+
"type": "registry:block",
|
|
4120
|
+
"content": "\"use client\"\n\nimport * as React from \"react\"\nimport { cva, type VariantProps } from \"class-variance-authority\"\nimport { cn } from \"../../lib/utils\"\nimport {\n Dialog,\n DialogTrigger,\n DialogClose,\n DialogContent,\n DialogTitle,\n DialogDescription,\n} from \"../../components/ui/dialog/dialog\"\nimport styles from \"./dialog-form.module.css\"\n\n/**\n * DialogForm — the Animal dialog substrate shell (VI-620).\n *\n * A mid-tier block that composes the `dialog` atom into the compact admin\n * modal shell the app's 10 hand-rolled modals converge onto: backdrop +\n * centered panel + header + footer. Everything is token-driven; the active\n * theme swaps the surface, hairline, radius, and title scale.\n *\n * The root pieces (open state, trigger, close) are re-exported straight from\n * the atom so a modal imports its whole substrate from one place.\n */\nconst DialogForm = Dialog\nconst DialogFormTrigger = DialogTrigger\nconst DialogFormClose = DialogClose\nconst DialogFormDescription = DialogDescription\n\n/* ─── Content — the compact panel ───────────────────────────────────── */\n\n/* Panel axes: `width` sets the max-width (token-backed --dialog-form-width-*,\n * default 30rem) and `border` toggles the hairline seam (default keeps it;\n * \"none\" nulls --dialog-form-panel-border for a borderless panel). Both default\n * to the VI-620 rendering, so existing call sites are unchanged. */\nconst panelVariants = cva(styles.panel, {\n variants: {\n width: {\n sm: styles.widthSm,\n md: styles.widthMd,\n lg: styles.widthLg,\n },\n border: {\n default: \"\",\n none: styles.panelBorderless,\n },\n },\n defaultVariants: {\n width: \"md\",\n border: \"default\",\n },\n})\n\nexport interface DialogFormContentProps\n extends React.ComponentProps<typeof DialogContent>,\n VariantProps<typeof panelVariants> {}\n\nconst DialogFormContent = React.forwardRef<\n React.ComponentRef<typeof DialogContent>,\n DialogFormContentProps\n>(({ className, width, border, ...props }, ref) => (\n <DialogContent\n ref={ref}\n className={cn(panelVariants({ width, border }), className)}\n {...props}\n />\n))\nDialogFormContent.displayName = \"DialogFormContent\"\n\n/* ─── Header ─────────────────────────────────────────────────────────── */\n\nfunction DialogFormHeader({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n <div\n data-slot=\"dialog-form-header\"\n className={cn(styles.header, className)}\n {...props}\n />\n )\n}\nDialogFormHeader.displayName = \"DialogFormHeader\"\n\n/* ─── Title (size axis: sm 13px default, md 16px) ────────────────────── */\n\nconst titleVariants = cva(styles.title, {\n variants: {\n size: {\n sm: styles.titleSm,\n md: styles.titleMd,\n },\n },\n defaultVariants: {\n size: \"sm\",\n },\n})\n\nexport interface DialogFormTitleProps\n extends React.ComponentProps<typeof DialogTitle>,\n VariantProps<typeof titleVariants> {}\n\nconst DialogFormTitle = React.forwardRef<\n React.ComponentRef<typeof DialogTitle>,\n DialogFormTitleProps\n>(({ className, size, ...props }, ref) => (\n <DialogTitle\n ref={ref}\n className={cn(titleVariants({ size }), className)}\n {...props}\n />\n))\nDialogFormTitle.displayName = \"DialogFormTitle\"\n\n/* ─── Body — vertical stack of DialogField blocks ────────────────────── */\n\nfunction DialogFormBody({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n <div\n data-slot=\"dialog-form-body\"\n className={cn(styles.body, className)}\n {...props}\n />\n )\n}\nDialogFormBody.displayName = \"DialogFormBody\"\n\n/* ─── Footer — the right-aligned dlg-btn action row ──────────────────── */\n\nfunction DialogFormFooter({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n <div\n data-slot=\"dialog-form-footer\"\n className={cn(styles.footer, className)}\n {...props}\n />\n )\n}\nDialogFormFooter.displayName = \"DialogFormFooter\"\n\nexport {\n DialogForm,\n DialogFormTrigger,\n DialogFormClose,\n DialogFormContent,\n DialogFormHeader,\n DialogFormTitle,\n DialogFormBody,\n DialogFormFooter,\n DialogFormDescription,\n titleVariants,\n panelVariants,\n}\n"
|
|
4121
|
+
},
|
|
4122
|
+
{
|
|
4123
|
+
"path": "blocks/dialog-form/dialog-form.module.css",
|
|
4124
|
+
"type": "registry:block",
|
|
4125
|
+
"content": "/* DialogForm block — Animal dialog substrate shell (VI-620).\n *\n * Restyles the `dialog` atom's DialogContent into the compact admin modal\n * panel, then adds the header / title / body / footer chrome the 10 hand-rolled\n * Animal modals converge onto. Every value traces to a Visor token so the\n * active theme (Animal, or any other) swaps the whole shell — no bespoke CSS.\n */\n\n/* Compact panel — overrides the atom's default 32rem / spacing-6 content. The\n * max-width is the `width` axis (--widthSm/Md/Lg below); the hairline border is a\n * token seam (--dialog-form-panel-border) a theme or the `border=\"none\"` variant\n * can null out — AN-237 removed it (the hairline reads as chrome the design\n * doesn't want) without editing this copy. */\n.panel {\n border-radius: var(--radius-xl, 0.75rem);\n padding: var(--spacing-5, 1.25rem);\n background: var(--surface-card, #ffffff);\n border: var(\n --dialog-form-panel-border,\n var(--stroke-width-thin, 1px) solid\n var(--hairline, var(--border-default, #e5e7eb))\n );\n box-shadow: var(--shadow-lg);\n}\n\n/* Width axis — max-width only. Token-backed so a theme can retune each step;\n * `md` (default) preserves the VI-620 30rem panel. */\n.widthSm {\n max-width: var(--dialog-form-width-sm, 24rem);\n}\n\n.widthMd {\n max-width: var(--dialog-form-width-md, 30rem);\n}\n\n.widthLg {\n max-width: var(--dialog-form-width-lg, 40rem);\n}\n\n/* Borderless variant — nulls the panel-border seam (border=\"none\"). Consumers\n * can equivalently set --dialog-form-panel-border: none in a theme. */\n.panelBorderless {\n --dialog-form-panel-border: none;\n}\n\n/* Header — title + optional description, tight stack above the body. */\n.header {\n display: flex;\n flex-direction: column;\n gap: var(--spacing-1, 0.25rem);\n margin-bottom: var(--spacing-4, 1rem);\n}\n\n/* Title — dlg-title. Bold, primary ink. Size axis reconciles the two AN\n * sources: the 13.5px surface scale (default `sm`) and reuse.html's 15px\n * shared/confirm dialogs (`md`). Normalized onto the --text-N chrome scale. */\n.title {\n font-weight: var(--font-weight-bold, 700);\n color: var(--text-primary, #111827);\n margin: 0;\n line-height: 1.3;\n}\n\n.titleSm {\n font-size: var(--text-13, 0.8125rem);\n}\n\n.titleMd {\n font-size: var(--text-16, 1rem);\n}\n\n/* Body — vertical stack of DialogField blocks. */\n.body {\n display: flex;\n flex-direction: column;\n gap: var(--spacing-4, 1rem);\n}\n\n/* Footer — right-aligned dlg-btn action row. */\n.footer {\n display: flex;\n justify-content: flex-end;\n align-items: center;\n gap: var(--spacing-2, 0.5rem);\n margin-top: var(--spacing-5, 1.25rem);\n}\n"
|
|
4126
|
+
}
|
|
4127
|
+
]
|
|
4128
|
+
},
|
|
4129
|
+
{
|
|
4130
|
+
"name": "dialog-field",
|
|
4131
|
+
"type": "registry:block",
|
|
4132
|
+
"description": "Compact dialog field (dlg-field, Animal dialog substrate): a flex-column field with a 5px label→control gap, an 11px/600 primary-ink label, and a recessed control well (surface deeper than the card in both modes + hairline border) hosting leading-icon and trailing control/caret slots. Composes the Label atom; token-driven.",
|
|
4133
|
+
"category": "form",
|
|
4134
|
+
"dependencies": [
|
|
4135
|
+
"@radix-ui/react-label",
|
|
4136
|
+
"class-variance-authority",
|
|
4137
|
+
"@loworbitstudio/visor-core"
|
|
4138
|
+
],
|
|
4139
|
+
"registryDependencies": [
|
|
4140
|
+
"utils",
|
|
4141
|
+
"label"
|
|
4142
|
+
],
|
|
4143
|
+
"files": [
|
|
4144
|
+
{
|
|
4145
|
+
"path": "blocks/dialog-field/dialog-field.tsx",
|
|
4146
|
+
"type": "registry:block",
|
|
4147
|
+
"content": "\"use client\"\n\nimport * as React from \"react\"\nimport { cva, type VariantProps } from \"class-variance-authority\"\nimport { cn } from \"../../lib/utils\"\nimport { Label } from \"../../components/ui/label/label\"\nimport styles from \"./dialog-field.module.css\"\n\n/**\n * DialogField — the Animal dialog substrate field block (dlg-field, VI-620).\n *\n * A flex-column field (label over control) with the compact dialog treatment:\n * 5px label→control gap, an 11px/600 primary-ink label, and the standard Visor\n * medium field (md size, themed form-control surface, borderless) that hosts a\n * leading icon slot, the control itself, and a trailing control/caret slot.\n * Composes the `label` atom for the label. Every value traces to a Visor token\n * so the active theme swaps the whole treatment.\n */\n\n/* ─── Field root — flex column, 5px gap ──────────────────────────────── */\n\nconst DialogField = React.forwardRef<\n HTMLDivElement,\n React.HTMLAttributes<HTMLDivElement>\n>(({ className, ...props }, ref) => (\n <div\n data-slot=\"dialog-field\"\n className={cn(styles.field, className)}\n ref={ref}\n {...props}\n />\n))\nDialogField.displayName = \"DialogField\"\n\n/* ─── Label — 11px / 600 / primary ink (dlg-label) ───────────────────── */\n\nexport type DialogFieldLabelProps = React.ComponentPropsWithoutRef<typeof Label>\n\nconst DialogFieldLabel = React.forwardRef<\n React.ElementRef<typeof Label>,\n DialogFieldLabelProps\n>(({ className, ...props }, ref) => (\n <Label\n data-slot=\"dialog-field-label\"\n className={cn(styles.label, className)}\n ref={ref}\n {...props}\n />\n))\nDialogFieldLabel.displayName = \"DialogFieldLabel\"\n\n/* ─── Control — the recessed well with icon + trailing slots ─────────── */\n\n/* Size axis drives the well padding only (label typography unchanged), matching\n * Visor's input-only `size` semantics. Default `md` preserves the VI-620 medium\n * well; `sm` is the compact Animal modal-form well. */\nconst controlVariants = cva(styles.control, {\n variants: {\n size: {\n sm: styles.controlSm,\n md: styles.controlMd,\n },\n },\n defaultVariants: {\n size: \"md\",\n },\n})\n\nexport interface DialogFieldControlProps\n extends React.HTMLAttributes<HTMLDivElement>,\n VariantProps<typeof controlVariants> {\n /** Leading icon rendered before the control (aria-hidden decoration). */\n icon?: React.ReactNode\n /** Trailing control / caret slot rendered after the control. */\n trailing?: React.ReactNode\n}\n\nconst DialogFieldControl = React.forwardRef<\n HTMLDivElement,\n DialogFieldControlProps\n>(({ className, size, icon, trailing, children, ...props }, ref) => (\n <div\n data-slot=\"dialog-field-control\"\n className={cn(controlVariants({ size }), className)}\n ref={ref}\n {...props}\n >\n {icon ? (\n <span className={styles.icon} aria-hidden=\"true\">\n {icon}\n </span>\n ) : null}\n <span className={styles.controlInner}>{children}</span>\n {trailing ? <span className={styles.trailing}>{trailing}</span> : null}\n </div>\n))\nDialogFieldControl.displayName = \"DialogFieldControl\"\n\nexport { DialogField, DialogFieldLabel, DialogFieldControl, controlVariants }\n"
|
|
4148
|
+
},
|
|
4149
|
+
{
|
|
4150
|
+
"path": "blocks/dialog-field/dialog-field.module.css",
|
|
4151
|
+
"type": "registry:block",
|
|
4152
|
+
"content": "/* DialogField block — Animal dialog substrate field (dlg-field, VI-620).\n *\n * flex column, 5px gap · 11px/600 primary-ink label · the standard Visor medium\n * field (md size, themed form-control surface, borderless) hosting a leading\n * icon, the control, and a trailing control/caret. Every value traces to a Visor\n * token; the active theme swaps the whole treatment (no bespoke CSS).\n */\n\n/* Field — flex column. 5px label→control gap (dlg-field spec): the 4px grid\n * has no 5px step, so derive it from --spacing-1 (the same calc idiom the\n * field atom uses for its 6px icon gap). */\n.field {\n display: flex;\n flex-direction: column;\n gap: calc(var(--spacing-1, 0.25rem) + var(--spacing-1, 0.25rem) / 4);\n}\n\n/* Label — dlg-label: 11px / 600 / --ink (primary). */\n.label {\n font-size: var(--text-11, 0.6875rem);\n font-weight: var(--font-weight-semibold, 600);\n color: var(--text-primary, #111827);\n line-height: 1.4;\n}\n\n/* Control — the standard Visor field well on the shared form-control surface role\n * (--field-control-bg), borderless. The surface color is theme-driven — a dialog\n * context may lift it with --field-control-bg (the documented in-dialog hook on\n * the input atom); we do not hand-roll it here. Borderless per the dlg-field\n * direction; focus shows a tokenized ring. The well padding is the `size` axis\n * (--controlSm / --controlMd below) so a compact Animal modal field and the\n * default medium field share one well — matching Visor's input-only `size`\n * semantics (padding varies; the label typography does not). */\n.control {\n display: flex;\n align-items: center;\n gap: var(--spacing-2, 0.5rem);\n border-radius: var(--input-radius-md, var(--radius-sm, 0.5rem));\n /* Borderless field → the surface must contrast with the card on its own. A\n * fixed surface token (--surface-subtle) can't guarantee that: some themes\n * pin it almost on the card (e.g. space/light). Instead derive it from the\n * card mixed a few percent toward --text-primary — which is legible on the\n * card by definition, so the field reads as a subtle-but-visible step in\n * every theme and both modes. Overridable per theme via --field-control-bg. */\n background: var(\n --field-control-bg,\n color-mix(in srgb, var(--surface-card, #ffffff), var(--text-primary, #111827) 7%)\n );\n color: var(--text-primary, #111827);\n transition: box-shadow var(--motion-duration-150, 150ms)\n var(--motion-easing-default, ease-in-out);\n}\n\n.control:focus-within {\n box-shadow: 0 0 0 var(--focus-ring-width, 2px)\n color-mix(in srgb, var(--border-focus, var(--primary, #111827)) 15%, transparent);\n}\n\n/* Size axis — well/control padding only (label typography unchanged). Mirrors the\n * input atom's --input-padding-{sm,md} seams so the field well tracks the input\n * size scale. `md` (default) preserves the VI-620 medium well; `sm` is the\n * compact Animal modal-form well (opted into per-field via size=\"sm\"). */\n.controlSm {\n padding: var(--input-padding-sm, var(--spacing-1, 0.25rem) var(--spacing-3, 0.75rem));\n}\n\n.controlMd {\n padding: var(--input-padding-md, var(--spacing-3_5, 0.875rem) var(--spacing-4, 1rem));\n}\n\n/* Leading icon slot. */\n.icon {\n display: inline-flex;\n align-items: center;\n flex-shrink: 0;\n color: var(--text-tertiary, #9ca3af);\n}\n\n/* The control itself sits on the well surface — transparent, borderless, so a\n * plain <input>/<select>/<textarea> dropped in reads as part of the well. */\n.controlInner {\n flex: 1;\n min-width: 0;\n display: flex;\n}\n\n.controlInner :global(input),\n.controlInner :global(select),\n.controlInner :global(textarea) {\n flex: 1;\n min-width: 0;\n border: 0;\n padding: 0;\n background: transparent;\n outline: none;\n color: var(--text-primary, #111827);\n}\n\n/* Trailing control / caret slot. */\n.trailing {\n display: inline-flex;\n align-items: center;\n flex-shrink: 0;\n color: var(--text-tertiary, #9ca3af);\n}\n"
|
|
4153
|
+
}
|
|
4154
|
+
]
|
|
4155
|
+
},
|
|
4101
4156
|
{
|
|
4102
4157
|
"name": "admin-tabbed-editor",
|
|
4103
4158
|
"type": "registry:block",
|
package/dist/visor-manifest.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": "0.4.0",
|
|
3
|
-
"generated_at": "2026-07-
|
|
3
|
+
"generated_at": "2026-07-21T20:04:25.715Z",
|
|
4
4
|
"components": {
|
|
5
5
|
"accessibility-specimen": {
|
|
6
6
|
"category": "specimen",
|
|
@@ -1004,7 +1004,8 @@
|
|
|
1004
1004
|
"size": [
|
|
1005
1005
|
"sm",
|
|
1006
1006
|
"md",
|
|
1007
|
-
"lg"
|
|
1007
|
+
"lg",
|
|
1008
|
+
"dlg"
|
|
1008
1009
|
]
|
|
1009
1010
|
},
|
|
1010
1011
|
"props": [
|
|
@@ -1015,8 +1016,9 @@
|
|
|
1015
1016
|
},
|
|
1016
1017
|
{
|
|
1017
1018
|
"name": "size",
|
|
1018
|
-
"type": "'sm' | 'md' | 'lg'",
|
|
1019
|
-
"default": "md"
|
|
1019
|
+
"type": "'sm' | 'md' | 'lg' | 'dlg'",
|
|
1020
|
+
"default": "md",
|
|
1021
|
+
"description": "Standard sizes plus `dlg` — the compact dialog-footer button (dlg-btn) from the Animal dialog substrate (VI-620): 11px/600 text, tokenized ~8/12px padding, radius-lg. Its three intents reuse the variant axis — primary via variant=\"default\", ghost via variant=\"ghost\" (renders a hairline-outlined, secondary-ink face at dlg size), danger via variant=\"destructive\" (renders a subtle-danger face at dlg size).\n"
|
|
1020
1022
|
},
|
|
1021
1023
|
{
|
|
1022
1024
|
"name": "asChild",
|
|
@@ -1050,12 +1052,16 @@
|
|
|
1050
1052
|
"--border-default",
|
|
1051
1053
|
"--border-focus",
|
|
1052
1054
|
"--border-strong",
|
|
1055
|
+
"--destructive",
|
|
1053
1056
|
"--focus-ring-offset",
|
|
1054
1057
|
"--focus-ring-width",
|
|
1058
|
+
"--font-size-2xs",
|
|
1055
1059
|
"--font-size-base",
|
|
1056
1060
|
"--font-size-sm",
|
|
1057
1061
|
"--font-size-xs",
|
|
1058
1062
|
"--font-weight-medium",
|
|
1063
|
+
"--font-weight-semibold",
|
|
1064
|
+
"--hairline",
|
|
1059
1065
|
"--interactive-destructive-bg",
|
|
1060
1066
|
"--interactive-destructive-bg-hover",
|
|
1061
1067
|
"--interactive-destructive-text",
|
|
@@ -1071,15 +1077,19 @@
|
|
|
1071
1077
|
"--motion-easing-default",
|
|
1072
1078
|
"--opacity-40",
|
|
1073
1079
|
"--primary",
|
|
1080
|
+
"--radius-lg",
|
|
1074
1081
|
"--radius-md",
|
|
1075
1082
|
"--spacing-2",
|
|
1076
1083
|
"--spacing-3",
|
|
1077
1084
|
"--spacing-4",
|
|
1078
1085
|
"--spacing-6",
|
|
1079
1086
|
"--surface-card",
|
|
1087
|
+
"--surface-error-subtle",
|
|
1080
1088
|
"--surface-muted",
|
|
1081
1089
|
"--surface-subtle",
|
|
1082
|
-
"--text-
|
|
1090
|
+
"--text-error",
|
|
1091
|
+
"--text-primary",
|
|
1092
|
+
"--text-secondary"
|
|
1083
1093
|
],
|
|
1084
1094
|
"example": "<Button variant=\"default\" size=\"md\">Save changes</Button>\n\n<Button variant=\"destructive\">Delete account</Button>\n\n<Button variant=\"ghost\" size=\"sm\">Cancel</Button>\n\n{/* Gated — no reason (tooltip suppressed) */}\n<Button gated>Delete organization</Button>\n\n{/* Gated with reason — tooltip shown on hover/focus */}\n<TooltipProvider>\n <Button gated gatedReason=\"You can't delete this — you're not an owner\">\n Delete organization\n </Button>\n</TooltipProvider>\n"
|
|
1085
1095
|
},
|
|
@@ -8737,6 +8747,12 @@
|
|
|
8737
8747
|
"type": "boolean",
|
|
8738
8748
|
"default": false,
|
|
8739
8749
|
"description": "Animate the indicator dot with a soft pulse. Respects prefers-reduced-motion."
|
|
8750
|
+
},
|
|
8751
|
+
{
|
|
8752
|
+
"name": "mono",
|
|
8753
|
+
"type": "boolean",
|
|
8754
|
+
"default": false,
|
|
8755
|
+
"description": "Render the label in the monospace token face (--font-mono, 11px) — the dot + mono-token treatment from the Animal dialog substrate (VI-620), for ID / code / token-style status readouts.\n"
|
|
8740
8756
|
}
|
|
8741
8757
|
],
|
|
8742
8758
|
"dependencies": [
|
|
@@ -8744,6 +8760,7 @@
|
|
|
8744
8760
|
],
|
|
8745
8761
|
"tokens_used": [
|
|
8746
8762
|
"--color-white",
|
|
8763
|
+
"--font-mono",
|
|
8747
8764
|
"--motion-duration-800",
|
|
8748
8765
|
"--motion-easing-standard",
|
|
8749
8766
|
"--radius-full",
|
|
@@ -8751,6 +8768,7 @@
|
|
|
8751
8768
|
"--surface-info-default",
|
|
8752
8769
|
"--surface-success-default",
|
|
8753
8770
|
"--surface-warning-default",
|
|
8771
|
+
"--text-11",
|
|
8754
8772
|
"--text-inverse",
|
|
8755
8773
|
"--text-tertiary"
|
|
8756
8774
|
],
|
|
@@ -11437,6 +11455,43 @@
|
|
|
11437
11455
|
"Production user-facing pages"
|
|
11438
11456
|
]
|
|
11439
11457
|
},
|
|
11458
|
+
"dialog-field": {
|
|
11459
|
+
"category": "form",
|
|
11460
|
+
"description": "Compact dialog field block (dlg-field, the Animal dialog substrate VI-620): a flex-column field with a 5px label→control gap, an 11px/600 primary-ink label, and a recessed control \"well\" (a surface deeper than the card in both modes + a hairline border) hosting a leading icon slot, the control, and a trailing control/caret slot. Composes the Label atom; token-driven so the active theme swaps the treatment.\n",
|
|
11461
|
+
"components_used": [
|
|
11462
|
+
"label",
|
|
11463
|
+
"input"
|
|
11464
|
+
],
|
|
11465
|
+
"when_to_use": [
|
|
11466
|
+
"Form fields inside a DialogForm / admin modal",
|
|
11467
|
+
"Fields that need a leading icon or a trailing caret / stepper / unit control",
|
|
11468
|
+
"Any compact form control that would otherwise hand-roll a recessed input well"
|
|
11469
|
+
],
|
|
11470
|
+
"when_not_to_use": [
|
|
11471
|
+
"Standard page forms (use the Field component)",
|
|
11472
|
+
"Standalone labels (use Label directly)",
|
|
11473
|
+
"Non-form content grouping (use a div or Card)"
|
|
11474
|
+
]
|
|
11475
|
+
},
|
|
11476
|
+
"dialog-form": {
|
|
11477
|
+
"category": "overlay",
|
|
11478
|
+
"description": "Compact admin modal shell (the Animal dialog substrate, VI-620) composing the Dialog atom into a backdrop + centered panel + header + footer. Exposes a titled header with a size axis (13px default / 16px), a body stack for DialogField blocks, and a right-aligned dlg-btn footer row. Token-driven — the active theme swaps the surface, hairline, radius, and title scale.\n",
|
|
11479
|
+
"components_used": [
|
|
11480
|
+
"dialog",
|
|
11481
|
+
"button"
|
|
11482
|
+
],
|
|
11483
|
+
"when_to_use": [
|
|
11484
|
+
"Create / edit modals in an admin app (invite user, edit venue, add artist)",
|
|
11485
|
+
"Confirmation and destructive-action prompts that need a titled shell + actions",
|
|
11486
|
+
"Any pop-up form that would otherwise hand-roll a backdrop + panel + footer"
|
|
11487
|
+
],
|
|
11488
|
+
"when_not_to_use": [
|
|
11489
|
+
"⌘K command palette (use command-dialog)",
|
|
11490
|
+
"Right-side detail editing (use admin-detail-drawer / Sheet)",
|
|
11491
|
+
"Non-blocking notifications (use Toast)",
|
|
11492
|
+
"A plain full-page form (use a page, not a modal)"
|
|
11493
|
+
]
|
|
11494
|
+
},
|
|
11440
11495
|
"export-menu": {
|
|
11441
11496
|
"category": "admin",
|
|
11442
11497
|
"description": "Export button + popover composing a format-picker radio group, optional scope checkboxes, and an async-aware Cancel/Export footer. Drop-in for any admin list's export affordance.",
|