@devalok/shilp-sutra 0.50.0 → 0.52.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/AGENTS.md +1 -1
- package/MIGRATION.md +12 -0
- package/dist/tokens/semantic.css +20 -1
- package/dist/ui/button-group.js +2 -0
- package/dist/ui/button-group.js.map +1 -1
- package/dist/ui/button-processing.d.ts.map +1 -1
- package/dist/ui/button-processing.js +1 -0
- package/dist/ui/button-processing.js.map +1 -1
- package/dist/ui/button.d.ts +3 -3
- package/dist/ui/button.d.ts.map +1 -1
- package/dist/ui/button.js +26 -0
- package/dist/ui/button.js.map +1 -1
- package/dist/ui/card.d.ts +5 -4
- package/dist/ui/card.d.ts.map +1 -1
- package/dist/ui/card.js +1 -1
- package/dist/ui/card.js.map +1 -1
- package/dist/ui/color-input.d.ts.map +1 -1
- package/dist/ui/color-input.js +82 -82
- package/dist/ui/color-input.js.map +1 -1
- package/dist/ui/icon.d.ts +2 -0
- package/dist/ui/icon.d.ts.map +1 -1
- package/dist/ui/icon.js +31 -26
- package/dist/ui/icon.js.map +1 -1
- package/dist/ui/search-input.d.ts.map +1 -1
- package/dist/ui/search-input.js +1 -0
- package/dist/ui/search-input.js.map +1 -1
- package/dist/ui/segmented-control.d.ts +28 -6
- package/dist/ui/segmented-control.d.ts.map +1 -1
- package/dist/ui/segmented-control.js +61 -43
- package/dist/ui/segmented-control.js.map +1 -1
- package/dist/ui/split-button.d.ts.map +1 -1
- package/dist/ui/split-button.js +7 -0
- package/dist/ui/split-button.js.map +1 -1
- package/dist/ui/stat-card.d.ts +3 -3
- package/dist/ui/stat-card.d.ts.map +1 -1
- package/dist/ui/stat-card.js.map +1 -1
- package/docs/components/ui/button.md +2 -2
- package/docs/components/ui/segmented-control.md +31 -11
- package/docs/components/ui/stat-card.md +1 -1
- package/docs/recipes/install-remix.md +3 -3
- package/docs/recipes/install-vite.md +3 -3
- package/docs/recipes/server-components.md +2 -2
- package/llms.txt +1 -1
- package/make-kit/components/card.md +4 -4
- package/make-kit/foundations/color.md +20 -0
- package/make-kit/foundations/dark-mode.md +6 -9
- package/make-kit/foundations/icons.md +3 -3
- package/make-kit/setup.md +4 -4
- package/mcp-manifest.json +62 -11
- package/package.json +1 -1
- package/scripts/welcome.mjs +59 -1
- package/skill/SKILL.md +1 -1
- package/skill/references/components.md +1 -1
- package/skill/references/server-components.md +2 -2
- package/skill/references/setup-remix.md +3 -3
- package/skill/references/setup-vite.md +3 -3
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"segmented-control.js","names":[],"sources":["../../src/ui/segmented-control.tsx"],"sourcesContent":["'use client'\n\nimport { LayoutGroup,motion } from 'framer-motion'\nimport * as React from 'react'\n\nimport { IconProvider } from './icon-context'\nimport type { IconInput } from './lib/icon-input'\nimport { normalizeIcon } from './lib/normalize-icon'\nimport { cn } from './lib/utils'\n\n/* ── Types ─────────────────────────────────────────────────── */\n\nexport type SegmentedControlSize = 'sm' | 'md' | 'lg'\nexport type SegmentedControlVariant = 'default' | 'solid'\n\nexport interface SegmentedControlOption {\n id: string\n text: string\n /** Optional icon rendered before the text label. Accepts any `IconInput`. */\n icon?: IconInput\n}\n\nexport interface SegmentedControlProps extends Omit<React.HTMLAttributes<HTMLDivElement>, 'onSelect'> {\n size?: SegmentedControlSize\n variant?: SegmentedControlVariant\n options: SegmentedControlOption[]\n selectedId: string\n onSelect: (id: string) => void\n disabled?: boolean\n}\n\n/* ── Size config ───────────────────────────────────────────── */\n\nconst sizeConfig = {\n sm: { button: 'h-7 px-ds-04 text-body-sm', icon: 'h-3.5 w-3.5' },\n md: { button: 'h-8 px-ds-05 text-body-md', icon: 'h-4 w-4' },\n lg: { button: 'h-10 px-ds-06 text-body-md', icon: 'h-4 w-4' },\n} as const\n\n/* ── Pill styles per variant ───────────────────────────────── */\n\nconst pillStyles = {\n default: 'bg-surface-overlay shadow-raised',\n solid: 'bg-accent-9',\n} as const\n\nconst selectedTextStyles = {\n default: 'text-surface-fg',\n solid: 'text-accent-fg',\n} as const\n\n/* ── Spring config (snappy, minimal overshoot) ─────────────── */\n/* Intentionally softer than springs.snappy (500/30/0.5) for pill slide feel */\n\nconst pillSpring = { type: 'spring' as const, stiffness: 400, damping: 30 }\n\n/* ── SegmentedControl ──────────────────────────────────────── */\n\nconst SegmentedControl = React.forwardRef<HTMLDivElement, SegmentedControlProps>(\n function SegmentedControl(\n {\n size = 'md',\n variant = 'default',\n options,\n selectedId,\n onSelect,\n disabled = false,\n className,\n ...props\n },\n ref,\n ) {\n const instanceId = React.useId()\n const tablistRef = React.useRef<HTMLDivElement | null>(null)\n\n // Compose refs\n const mergedRef = React.useCallback(\n (node: HTMLDivElement | null) => {\n tablistRef.current = node\n if (typeof ref === 'function') ref(node)\n else if (ref) (ref as React.MutableRefObject<HTMLDivElement | null>).current = node\n },\n [ref],\n )\n\n // Keyboard navigation\n const handleKeyDown = (e: React.KeyboardEvent<HTMLDivElement>) => {\n if (disabled) return\n\n const currentIndex = options.findIndex((o) => o.id === selectedId)\n let nextIndex = currentIndex\n\n switch (e.key) {\n case 'ArrowLeft':\n e.preventDefault()\n nextIndex = currentIndex > 0 ? currentIndex - 1 : options.length - 1\n break\n case 'ArrowRight':\n e.preventDefault()\n nextIndex = currentIndex < options.length - 1 ? currentIndex + 1 : 0\n break\n case 'Home':\n e.preventDefault()\n nextIndex = 0\n break\n case 'End':\n e.preventDefault()\n nextIndex = options.length - 1\n break\n default:\n return\n }\n\n onSelect(options[nextIndex].id)\n requestAnimationFrame(() => {\n const buttons = tablistRef.current?.querySelectorAll<HTMLButtonElement>('[role=\"radio\"]')\n buttons?.[nextIndex]?.focus()\n })\n }\n\n const { button: buttonSize, icon: iconSize } = sizeConfig[size]\n\n return (\n <div\n ref={mergedRef}\n role=\"radiogroup\"\n tabIndex={-1}\n aria-label={props['aria-label'] ?? 'Segmented control'}\n onKeyDown={handleKeyDown}\n className={cn(\n 'inline-flex p-[3px] rounded-pill',\n 'bg-surface-raised-hover border border-surface-border-subtle shadow-inset',\n disabled && 'opacity-action-disabled pointer-events-none',\n className,\n )}\n {...props}\n >\n <LayoutGroup id={instanceId}>\n {options.map((option) => {\n const isSelected = option.id === selectedId\n\n return (\n <button\n key={option.id}\n type=\"button\"\n role=\"radio\"\n aria-checked={isSelected}\n tabIndex={isSelected ? 0 : -1}\n disabled={disabled}\n onClick={() => onSelect(option.id)}\n className={cn(\n 'relative inline-flex items-center justify-center gap-ds-02 rounded-pill',\n 'font-medium transition-colors duration-fast-02 ease-productive-standard',\n 'outline-hidden focus-visible:ring-2 focus-visible:ring-accent-9 focus-visible:ring-offset-2',\n buttonSize,\n isSelected\n ? selectedTextStyles[variant]\n : 'text-surface-fg-muted hover:text-surface-fg',\n )}\n >\n {/* Sliding pill indicator */}\n {isSelected && (\n <motion.span\n layoutId=\"segment-pill\"\n className={cn(\n 'absolute inset-0 rounded-pill pointer-events-none',\n pillStyles[variant],\n )}\n transition={pillSpring}\n />\n )}\n\n {/* Content (above pill via z-index) */}\n {option.icon && (\n <span className={cn('relative z-[1] shrink-0 inline-flex items-center justify-center', iconSize)}>\n <IconProvider size={size === 'lg' ? 'sm' : 'xs'}>\n {normalizeIcon(option.icon)}\n </IconProvider>\n </span>\n )}\n <span className=\"relative z-[1]\">{option.text}</span>\n </button>\n )\n })}\n </LayoutGroup>\n </div>\n )\n },\n)\nSegmentedControl.displayName = 'SegmentedControl'\n\n/* ── Exports ───────────────────────────────────────────────── */\n\nexport { SegmentedControl }\n"],"mappings":";;;;;;;;AAiCA,IAAM,IAAa;CACjB,IAAI;EAAE,QAAQ;EAA6B,MAAM;CAAc;CAC/D,IAAI;EAAE,QAAQ;EAA6B,MAAM;CAAU;CAC3D,IAAI;EAAE,QAAQ;EAA8B,MAAM;CAAU;AAC9D,GAIM,IAAa;CACjB,SAAS;CACT,OAAO;AACT,GAEM,IAAqB;CACzB,SAAS;CACT,OAAO;AACT,GAKM,IAAa;CAAE,MAAM;CAAmB,WAAW;CAAK,SAAS;AAAG,GAIpE,IAAmB,EAAM,WAC7B,SACE,EACE,UAAO,MACP,aAAU,WACV,YACA,eACA,aACA,cAAW,IACX,cACA,GAAG,KAEL,GACA;CACA,IAAM,IAAa,EAAM,MAAM,GACzB,IAAa,EAAM,OAA8B,IAAI,GAGrD,IAAY,EAAM,aACrB,MAAgC;EAE/B,AADA,EAAW,UAAU,GACjB,OAAO,KAAQ,aAAY,EAAI,CAAI,IAC9B,MAAK,EAAuD,UAAU;CACjF,GACA,CAAC,CAAG,CACN,GAGM,KAAiB,MAA2C;EAChE,IAAI,GAAU;EAEd,IAAM,IAAe,EAAQ,WAAW,MAAM,EAAE,OAAO,CAAU,GAC7D,IAAY;EAEhB,QAAQ,EAAE,KAAV;GACE,KAAK;IAEH,AADA,EAAE,eAAe,GACjB,IAAY,IAAe,IAAI,IAAe,IAAI,EAAQ,SAAS;IACnE;GACF,KAAK;IAEH,AADA,EAAE,eAAe,GACjB,IAAY,IAAe,EAAQ,SAAS,IAAI,IAAe,IAAI;IACnE;GACF,KAAK;IAEH,AADA,EAAE,eAAe,GACjB,IAAY;IACZ;GACF,KAAK;IAEH,AADA,EAAE,eAAe,GACjB,IAAY,EAAQ,SAAS;IAC7B;GACF,SACE;EACJ;EAGA,AADA,EAAS,EAAQ,EAAU,CAAC,EAAE,GAC9B,4BAA4B;GAE1B,CADgB,EAAW,SAAS,iBAAoC,kBAAgB,EAAA,GAC9E,EAAU,EAAE,MAAM;EAC9B,CAAC;CACH,GAEM,EAAE,QAAQ,GAAY,MAAM,MAAa,EAAW;CAE1D,OACE,kBAAC,OAAD;EACE,KAAK;EACL,MAAK;EACL,UAAU;EACV,cAAY,EAAM,iBAAiB;EACnC,WAAW;EACX,WAAW,EACT,oCACA,4EACA,KAAY,+CACZ,CACF;EACA,GAAI;YAEJ,kBAAC,GAAD;GAAa,IAAI;aACd,EAAQ,KAAK,MAAW;IACvB,IAAM,IAAa,EAAO,OAAO;IAEjC,OACE,kBAAC,UAAD;KAEE,MAAK;KACL,MAAK;KACL,gBAAc;KACd,UAAU,IAAa,IAAI;KACjB;KACV,eAAe,EAAS,EAAO,EAAE;KACjC,WAAW,EACT,2EACA,2EACA,+FACA,GACA,IACI,EAAmB,KACnB,6CACN;eAhBF;MAmBG,KACC,kBAAC,EAAO,MAAR;OACE,UAAS;OACT,WAAW,EACT,qDACA,EAAW,EACb;OACA,YAAY;MACb,CAAA;MAIF,EAAO,QACN,kBAAC,QAAD;OAAM,WAAW,EAAG,mEAAmE,CAAQ;iBAC7F,kBAAC,GAAD;QAAc,MAAM,MAAS,OAAO,OAAO;kBACxC,EAAc,EAAO,IAAI;OACd,CAAA;MACV,CAAA;MAER,kBAAC,QAAD;OAAM,WAAU;iBAAkB,EAAO;MAAW,CAAA;KAC9C;OAtCD,EAAO,EAsCN;GAEZ,CAAC;EACU,CAAA;CACV,CAAA;AAET,CACF;AACA,EAAiB,cAAc"}
|
|
1
|
+
{"version":3,"file":"segmented-control.js","names":[],"sources":["../../src/ui/segmented-control.tsx"],"sourcesContent":["'use client'\n\nimport { LayoutGroup, motion, useReducedMotion } from 'framer-motion'\nimport * as React from 'react'\n\nimport { IconProvider } from './icon-context'\nimport type { IconInput } from './lib/icon-input'\nimport { normalizeIcon } from './lib/normalize-icon'\nimport { cn } from './lib/utils'\n\n/* ── Types ─────────────────────────────────────────────────── */\n\nexport type SegmentedControlSize = 'sm' | 'md' | 'lg'\nexport type SegmentedControlVariant = 'soft' | 'solid'\n\n/** @deprecated `variant=\"default\"` was renamed to `\"soft\"`. Still accepted at\n * runtime (maps to `\"soft\"`); update call sites — the alias is removed in a\n * future major. */\ntype DeprecatedVariant = 'default'\n\nexport interface SegmentedControlOption {\n id: string\n /** Visible label. Accepts a `ReactNode` so an option can carry a count badge\n * or custom node, not just a string. Optional — omit for an icon-only\n * segment, in which case set `ariaLabel`. */\n text?: React.ReactNode\n /** Optional icon rendered before the text label. Accepts any `IconInput`. */\n icon?: IconInput\n /** Accessible name for the segment. Required for icon-only segments (no\n * `text`); otherwise the visible text provides the name. */\n ariaLabel?: string\n}\n\nexport interface SegmentedControlProps extends Omit<React.HTMLAttributes<HTMLDivElement>, 'onSelect' | 'defaultValue'> {\n size?: SegmentedControlSize\n variant?: SegmentedControlVariant | DeprecatedVariant\n options: SegmentedControlOption[]\n /** Selected option id (controlled). Aligns with Tabs/ToggleGroup. */\n value?: string\n /** Initial selected id for uncontrolled mode. Ignored when `value` is set;\n * defaults to the first option. */\n defaultValue?: string\n /** Fires with the newly-selected id (both modes). */\n onValueChange?: (id: string) => void\n disabled?: boolean\n /** Fill the container: segments split the available width equally instead of\n * hugging their content. Use for full-width toggles and view switchers. */\n fullWidth?: boolean\n\n /** @deprecated Use `value`. Still accepted; kept for back-compat. */\n selectedId?: string\n /** @deprecated Use `onValueChange`. Still accepted; kept for back-compat. */\n onSelect?: (id: string) => void\n}\n\n/* ── RTL detection ─────────────────────────────────────────────\n * Reads the nearest `dir` attribute (testable, the common case), falling\n * back to the computed style for CSS-set direction. In RTL the horizontal\n * arrow keys invert so Arrow keys always track reading order. */\n\nfunction isRtl(el: HTMLElement | null): boolean {\n if (!el) return false\n const dirEl = el.closest('[dir]') as HTMLElement | null\n if (dirEl) return (dirEl.getAttribute('dir') ?? '').toLowerCase() === 'rtl'\n try {\n return getComputedStyle(el).direction === 'rtl'\n } catch {\n return false\n }\n}\n\n/* ── Size config ───────────────────────────────────────────── */\n\nconst sizeConfig = {\n sm: { button: 'h-7 px-ds-04 text-body-sm', icon: 'h-3.5 w-3.5' },\n md: { button: 'h-8 px-ds-05 text-body-md', icon: 'h-4 w-4' },\n lg: { button: 'h-10 px-ds-06 text-body-md', icon: 'h-4 w-4' },\n} as const\n\n/* ── Thumb (sliding indicator) styles per variant ──────────────\n * The track carries no border/inset (see tokens/semantic.css\n * --color-segment-track), so the thumb defines its own edge with a\n * single ring-less soft shadow (--shadow-segment). */\n\nconst thumbStyles = {\n soft: 'bg-segment-thumb shadow-segment',\n solid: 'bg-accent-9 shadow-segment',\n} as const\n\nconst selectedTextStyles = {\n soft: 'text-surface-fg',\n solid: 'text-accent-fg',\n} as const\n\n/* ── SegmentedControl ──────────────────────────────────────── */\n\nconst SegmentedControl = React.forwardRef<HTMLDivElement, SegmentedControlProps>(\n function SegmentedControl(\n {\n size = 'md',\n variant = 'soft',\n options,\n value,\n defaultValue,\n onValueChange,\n selectedId,\n onSelect,\n disabled = false,\n fullWidth = false,\n className,\n ...props\n },\n ref,\n ) {\n const instanceId = React.useId()\n const tablistRef = React.useRef<HTMLDivElement | null>(null)\n const reduceMotion = useReducedMotion()\n\n // Back-compat: 'default' was renamed to 'soft'. Map silently.\n const resolvedVariant: SegmentedControlVariant = variant === 'default' ? 'soft' : variant\n\n // Controlled/uncontrolled resolution. `value` is canonical; `selectedId` is\n // the deprecated alias. Controlled when either is provided; otherwise the\n // hook owns state, seeded from `defaultValue` (or the first option).\n const controlledValue = value ?? selectedId\n const isControlled = controlledValue !== undefined\n const [uncontrolled, setUncontrolled] = React.useState(defaultValue ?? options[0]?.id)\n const selected = isControlled ? controlledValue : uncontrolled\n\n const emit = React.useCallback(\n (id: string) => {\n if (!isControlled) setUncontrolled(id)\n ;(onValueChange ?? onSelect)?.(id)\n },\n [isControlled, onValueChange, onSelect],\n )\n\n // Thumb glide. This is a frequent, functional toggle, so the motion is\n // crisp and bounce-free (Apple spring notation: settles ~300ms, no\n // overshoot) rather than playful. Snaps instantly under reduced motion —\n // the slide is the only movement here.\n const thumbTransition = reduceMotion\n ? { duration: 0 }\n : { type: 'spring' as const, duration: 0.3, bounce: 0 }\n\n // Compose refs\n const mergedRef = React.useCallback(\n (node: HTMLDivElement | null) => {\n tablistRef.current = node\n if (typeof ref === 'function') ref(node)\n else if (ref) (ref as React.MutableRefObject<HTMLDivElement | null>).current = node\n },\n [ref],\n )\n\n // Keyboard navigation\n const handleKeyDown = (e: React.KeyboardEvent<HTMLDivElement>) => {\n if (disabled) return\n\n const currentIndex = options.findIndex((o) => o.id === selected)\n let nextIndex = currentIndex\n\n const prev = currentIndex > 0 ? currentIndex - 1 : options.length - 1\n const next = currentIndex < options.length - 1 ? currentIndex + 1 : 0\n const rtl = isRtl(tablistRef.current)\n\n switch (e.key) {\n case 'ArrowLeft':\n e.preventDefault()\n nextIndex = rtl ? next : prev\n break\n case 'ArrowRight':\n e.preventDefault()\n nextIndex = rtl ? prev : next\n break\n case 'Home':\n e.preventDefault()\n nextIndex = 0\n break\n case 'End':\n e.preventDefault()\n nextIndex = options.length - 1\n break\n default:\n return\n }\n\n emit(options[nextIndex].id)\n requestAnimationFrame(() => {\n const buttons = tablistRef.current?.querySelectorAll<HTMLButtonElement>('[role=\"radio\"]')\n buttons?.[nextIndex]?.focus()\n })\n }\n\n const { button: buttonSize, icon: iconSize } = sizeConfig[size]\n\n return (\n <div\n ref={mergedRef}\n role=\"radiogroup\"\n tabIndex={-1}\n aria-label={props['aria-label'] ?? 'Segmented control'}\n onKeyDown={handleKeyDown}\n className={cn(\n 'p-ds-01 rounded-surface bg-segment-track',\n fullWidth ? 'flex w-full' : 'inline-flex w-fit',\n disabled && 'opacity-action-disabled pointer-events-none',\n className,\n )}\n {...props}\n >\n <LayoutGroup id={instanceId}>\n {options.map((option) => {\n const isSelected = option.id === selected\n\n return (\n <button\n key={option.id}\n type=\"button\"\n role=\"radio\"\n aria-checked={isSelected}\n aria-label={option.ariaLabel}\n tabIndex={isSelected ? 0 : -1}\n disabled={disabled}\n onClick={() => emit(option.id)}\n className={cn(\n 'relative inline-flex items-center justify-center gap-ds-02 rounded-control',\n 'font-medium outline-hidden',\n // `touch-target` adds a 44px min hit area via ::before without\n // changing the visual (dense) height.\n 'touch-target',\n // Exact properties (not `all`); press-scale gives instant\n // tactile feedback, gated to motion-safe so reduced-motion\n // users keep only the color change.\n 'transition-[color,transform] duration-fast-02 ease-productive-standard',\n 'motion-safe:active:scale-[0.97]',\n 'focus-visible:ring-2 focus-visible:ring-accent-9 focus-visible:ring-offset-2 focus-visible:ring-offset-transparent',\n buttonSize,\n fullWidth && 'flex-1 min-w-16',\n isSelected\n ? selectedTextStyles[resolvedVariant]\n : 'text-surface-fg-muted hover:text-surface-fg',\n )}\n >\n {/* Sliding thumb indicator. Rendered before the content so the\n positioned content siblings paint above it (no z-index). */}\n {isSelected && (\n <motion.span\n layoutId=\"segment-thumb\"\n initial={false}\n className={cn(\n 'absolute inset-0 rounded-control pointer-events-none',\n thumbStyles[resolvedVariant],\n )}\n transition={thumbTransition}\n />\n )}\n\n {/* Content — `relative` so it paints over the thumb */}\n {option.icon && (\n <span className={cn('relative shrink-0 inline-flex items-center justify-center', iconSize)}>\n <IconProvider size={size === 'lg' ? 'sm' : 'xs'}>\n {normalizeIcon(option.icon)}\n </IconProvider>\n </span>\n )}\n {option.text != null && <span className=\"relative\">{option.text}</span>}\n </button>\n )\n })}\n </LayoutGroup>\n </div>\n )\n },\n)\nSegmentedControl.displayName = 'SegmentedControl'\n\n/* ── Exports ───────────────────────────────────────────────── */\n\nexport { SegmentedControl }\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"split-button.d.ts","sourceRoot":"","sources":["../../src/ui/split-button.tsx"],"names":[],"mappings":"AAEA,OAAO,EAAE,KAAK,SAAS,EAAE,MAAM,kBAAkB,CAAA;AACjD,OAAO,EAAE,KAAK,YAAY,EAAE,MAAM,0BAA0B,CAAA;AAC5D,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAE9B,OAAO,EAAE,cAAc,EAAE,MAAM,UAAU,CAAA;AAMzC,KAAK,kBAAkB,GAAG,OAAO,GAAG,MAAM,GAAG,SAAS,CAAA;AACtD,KAAK,gBAAgB,GAAG,WAAW,CAAC,YAAY,CAAC,OAAO,cAAc,CAAC,CAAC,OAAO,CAAC,CAAC,CAAA;AACjF,KAAK,eAAe,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,GAAG,SAAS,GAAG,SAAS,GAAG,SAAS,CAAA;AAE7E,MAAM,WAAW,gBAAgB;IAC/B,0CAA0C;IAC1C,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAA;IACzB,yCAAyC;IACzC,OAAO,EAAE,CAAC,CAAC,EAAE,KAAK,CAAC,UAAU,CAAC,iBAAiB,CAAC,KAAK,IAAI,CAAA;IACzD,2DAA2D;IAC3D,eAAe,CAAC,EAAE,KAAK,CAAC,SAAS,CAAA;IACjC,6BAA6B;IAC7B,IAAI,CAAC,EAAE,OAAO,CAAA;IACd,+DAA+D;IAC/D,YAAY,CAAC,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAA;IACtC,uBAAuB;IACvB,OAAO,CAAC,EAAE,kBAAkB,CAAA;IAC5B,wBAAwB;IACxB,KAAK,CAAC,EAAE,gBAAgB,CAAA;IACxB,oBAAoB;IACpB,IAAI,CAAC,EAAE,eAAe,CAAA;IACtB,2BAA2B;IAC3B,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,yCAAyC;IACzC,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,mEAAmE;IACnE,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,sEAAsE;IACtE,YAAY,CAAC,EAAE,KAAK,CAAC,SAAS,CAAA;IAC9B,gEAAgE;IAChE,WAAW,CAAC,EAAE,MAAM,GAAG,OAAO,CAAA;IAC9B,qEAAqE;IACrE,SAAS,CAAC,EAAE,SAAS,CAAA;IACrB,2DAA2D;IAC3D,YAAY,CAAC,EAAE,MAAM,GAAG,MAAM,CAAA;IAC9B,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;
|
|
1
|
+
{"version":3,"file":"split-button.d.ts","sourceRoot":"","sources":["../../src/ui/split-button.tsx"],"names":[],"mappings":"AAEA,OAAO,EAAE,KAAK,SAAS,EAAE,MAAM,kBAAkB,CAAA;AACjD,OAAO,EAAE,KAAK,YAAY,EAAE,MAAM,0BAA0B,CAAA;AAC5D,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAE9B,OAAO,EAAE,cAAc,EAAE,MAAM,UAAU,CAAA;AAMzC,KAAK,kBAAkB,GAAG,OAAO,GAAG,MAAM,GAAG,SAAS,CAAA;AACtD,KAAK,gBAAgB,GAAG,WAAW,CAAC,YAAY,CAAC,OAAO,cAAc,CAAC,CAAC,OAAO,CAAC,CAAC,CAAA;AACjF,KAAK,eAAe,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,GAAG,SAAS,GAAG,SAAS,GAAG,SAAS,CAAA;AAE7E,MAAM,WAAW,gBAAgB;IAC/B,0CAA0C;IAC1C,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAA;IACzB,yCAAyC;IACzC,OAAO,EAAE,CAAC,CAAC,EAAE,KAAK,CAAC,UAAU,CAAC,iBAAiB,CAAC,KAAK,IAAI,CAAA;IACzD,2DAA2D;IAC3D,eAAe,CAAC,EAAE,KAAK,CAAC,SAAS,CAAA;IACjC,6BAA6B;IAC7B,IAAI,CAAC,EAAE,OAAO,CAAA;IACd,+DAA+D;IAC/D,YAAY,CAAC,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAA;IACtC,uBAAuB;IACvB,OAAO,CAAC,EAAE,kBAAkB,CAAA;IAC5B,wBAAwB;IACxB,KAAK,CAAC,EAAE,gBAAgB,CAAA;IACxB,oBAAoB;IACpB,IAAI,CAAC,EAAE,eAAe,CAAA;IACtB,2BAA2B;IAC3B,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,yCAAyC;IACzC,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,mEAAmE;IACnE,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,sEAAsE;IACtE,YAAY,CAAC,EAAE,KAAK,CAAC,SAAS,CAAA;IAC9B,gEAAgE;IAChE,WAAW,CAAC,EAAE,MAAM,GAAG,OAAO,CAAA;IAC9B,qEAAqE;IACrE,SAAS,CAAC,EAAE,SAAS,CAAA;IACrB,2DAA2D;IAC3D,YAAY,CAAC,EAAE,MAAM,GAAG,MAAM,CAAA;IAC9B,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AA8FD,QAAA,MAAM,WAAW,yFAuIhB,CAAA;AAID,OAAO,EAAE,WAAW,EAAE,CAAA;AACtB,YAAY,EAAE,gBAAgB,EAAE,eAAe,EAAC,kBAAkB,EAAE,CAAA"}
|
package/dist/ui/split-button.js
CHANGED
|
@@ -10,6 +10,7 @@ var c = {
|
|
|
10
10
|
error: "bg-error-11/20",
|
|
11
11
|
success: "bg-success-11/20",
|
|
12
12
|
warning: "bg-warning-11/20",
|
|
13
|
+
info: "bg-info-11/20",
|
|
13
14
|
neutral: "bg-neutral-8/30"
|
|
14
15
|
},
|
|
15
16
|
soft: {
|
|
@@ -17,6 +18,7 @@ var c = {
|
|
|
17
18
|
error: "bg-error-6",
|
|
18
19
|
success: "bg-success-6",
|
|
19
20
|
warning: "bg-warning-6",
|
|
21
|
+
info: "bg-info-6",
|
|
20
22
|
neutral: "bg-surface-border"
|
|
21
23
|
},
|
|
22
24
|
outline: {
|
|
@@ -24,6 +26,7 @@ var c = {
|
|
|
24
26
|
error: "bg-error-7",
|
|
25
27
|
success: "bg-success-7",
|
|
26
28
|
warning: "bg-warning-7",
|
|
29
|
+
info: "bg-info-7",
|
|
27
30
|
neutral: "bg-surface-border-strong"
|
|
28
31
|
}
|
|
29
32
|
}, l = {
|
|
@@ -69,6 +72,7 @@ function m(e, t) {
|
|
|
69
72
|
error: "bg-error-9 text-error-fg hover:bg-error-10 active:bg-error-11",
|
|
70
73
|
success: "bg-success-9 text-success-fg hover:bg-success-10 active:bg-success-11",
|
|
71
74
|
warning: "bg-warning-9 text-warning-fg hover:bg-warning-10 active:bg-warning-11",
|
|
75
|
+
info: "bg-info-9 text-info-fg hover:bg-info-10 active:bg-info-11",
|
|
72
76
|
neutral: "bg-neutral-5 text-surface-fg hover:bg-neutral-7 active:bg-neutral-8"
|
|
73
77
|
},
|
|
74
78
|
soft: {
|
|
@@ -76,6 +80,7 @@ function m(e, t) {
|
|
|
76
80
|
error: "bg-error-3 text-error-11 hover:bg-error-4 active:bg-error-5",
|
|
77
81
|
success: "bg-success-3 text-success-11 hover:bg-success-4 active:bg-success-5",
|
|
78
82
|
warning: "bg-warning-3 text-warning-11 hover:bg-warning-4 active:bg-warning-5",
|
|
83
|
+
info: "bg-info-3 text-info-11 hover:bg-info-4 active:bg-info-5",
|
|
79
84
|
neutral: "bg-surface-raised-hover text-surface-fg-muted hover:bg-surface-raised-active active:bg-neutral-5"
|
|
80
85
|
},
|
|
81
86
|
outline: {
|
|
@@ -83,6 +88,7 @@ function m(e, t) {
|
|
|
83
88
|
error: "bg-transparent text-error-11 hover:bg-error-3 active:bg-error-4",
|
|
84
89
|
success: "bg-transparent text-success-11 hover:bg-success-3 active:bg-success-4",
|
|
85
90
|
warning: "bg-transparent text-warning-11 hover:bg-warning-3 active:bg-warning-4",
|
|
91
|
+
info: "bg-transparent text-info-11 hover:bg-info-3 active:bg-info-4",
|
|
86
92
|
neutral: "bg-transparent text-surface-fg hover:bg-surface-raised-hover active:bg-surface-raised-active"
|
|
87
93
|
}
|
|
88
94
|
};
|
|
@@ -94,6 +100,7 @@ function h(e) {
|
|
|
94
100
|
error: "border-error-7",
|
|
95
101
|
success: "border-success-7",
|
|
96
102
|
warning: "border-warning-7",
|
|
103
|
+
info: "border-info-7",
|
|
97
104
|
neutral: "border-surface-border-strong"
|
|
98
105
|
};
|
|
99
106
|
return t[e] ?? t.accent;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"split-button.js","names":[],"sources":["../../src/ui/split-button.tsx"],"sourcesContent":["'use client'\n\nimport { type Placement } from '@floating-ui/dom'\nimport { type VariantProps } from 'class-variance-authority'\nimport * as React from 'react'\n\nimport { buttonVariants } from './button'\nimport { cn } from './lib/utils'\nimport { Popover, PopoverContent, PopoverTrigger } from './popover'\n\n// ── Types ───────────────────────────────────────────────────────\n\ntype SplitButtonVariant = 'solid' | 'soft' | 'outline'\ntype SplitButtonColor = NonNullable<VariantProps<typeof buttonVariants>['color']>\ntype SplitButtonSize = 'xs' | 'sm' | 'md' | 'icon-xs' | 'icon-sm' | 'icon-md'\n\nexport interface SplitButtonProps {\n /** Primary action content (left side). */\n children: React.ReactNode\n /** Primary click handler (left side). */\n onClick: (e: React.MouseEvent<HTMLButtonElement>) => void\n /** Content rendered inside the floating dropdown panel. */\n dropdownContent?: React.ReactNode\n /** Controlled open state. */\n open?: boolean\n /** Called when open state changes. Uncontrolled if omitted. */\n onOpenChange?: (open: boolean) => void\n /** @default 'solid' */\n variant?: SplitButtonVariant\n /** @default 'accent' */\n color?: SplitButtonColor\n /** @default 'md' */\n size?: SplitButtonSize\n /** Disable both halves. */\n disabled?: boolean\n /** aria-label for the primary button. */\n 'aria-label'?: string\n /** aria-label for the dropdown trigger. @default 'More options' */\n dropdownLabel?: string\n /** Custom icon for the dropdown trigger. Defaults to chevron-down. */\n dropdownIcon?: React.ReactNode\n /** Which side the dropdown trigger sits on. @default 'right' */\n triggerSide?: 'left' | 'right'\n /** Preferred placement for the dropdown panel. @default 'top-end' */\n placement?: Placement\n /** Width of the trigger (chevron) half. @default 'auto' */\n triggerWidth?: number | string\n className?: string\n}\n\n// ── Styling maps ────────────────────────────────────────────────\n\nconst dividerColor: Record<SplitButtonVariant, Record<string, string>> = {\n solid: {\n accent: 'bg-accent-11/20', error: 'bg-error-11/20', success: 'bg-success-11/20',\n warning: 'bg-warning-11/20', neutral: 'bg-neutral-8/30',\n },\n soft: {\n accent: 'bg-accent-6', error: 'bg-error-6', success: 'bg-success-6',\n warning: 'bg-warning-6', neutral: 'bg-surface-border',\n },\n outline: {\n accent: 'bg-accent-7', error: 'bg-error-7', success: 'bg-success-7',\n warning: 'bg-warning-7', neutral: 'bg-surface-border-strong',\n },\n}\n\nconst heightClass: Record<SplitButtonSize, string> = {\n xs: 'h-ds-xs-plus', sm: 'h-ds-sm', md: 'h-ds-md',\n 'icon-xs': 'h-ds-xs-plus', 'icon-sm': 'h-ds-sm', 'icon-md': 'h-ds-md',\n}\n\nconst textClass: Record<SplitButtonSize, string> = {\n xs: 'text-body-sm', sm: 'text-body-sm', md: 'text-body-md',\n 'icon-xs': 'text-body-sm', 'icon-sm': 'text-body-sm', 'icon-md': 'text-body-md',\n}\n\nconst primaryPadding: Record<SplitButtonSize, string> = {\n xs: 'px-ds-03 gap-1', sm: 'px-ds-04 gap-1.5', md: 'px-ds-05 gap-2',\n 'icon-xs': 'px-ds-02', 'icon-sm': 'px-ds-03', 'icon-md': 'px-ds-04',\n}\n\nconst triggerPadding: Record<SplitButtonSize, string> = {\n xs: 'px-ds-02', sm: 'px-ds-02', md: 'px-ds-03',\n 'icon-xs': 'px-ds-01', 'icon-sm': 'px-ds-02', 'icon-md': 'px-ds-02',\n}\n\nconst radiusClass: Record<SplitButtonSize, string> = {\n xs: 'rounded-control', sm: 'rounded-control', md: 'rounded-control',\n 'icon-xs': 'rounded-control', 'icon-sm': 'rounded-control', 'icon-md': 'rounded-control',\n}\n\nfunction getHalfClasses(variant: SplitButtonVariant, color: string): string {\n const map: Record<SplitButtonVariant, Record<string, string>> = {\n solid: {\n accent: 'bg-accent-9 text-accent-fg hover:bg-accent-10 active:bg-accent-11',\n error: 'bg-error-9 text-error-fg hover:bg-error-10 active:bg-error-11',\n success: 'bg-success-9 text-success-fg hover:bg-success-10 active:bg-success-11',\n warning: 'bg-warning-9 text-warning-fg hover:bg-warning-10 active:bg-warning-11',\n neutral: 'bg-neutral-5 text-surface-fg hover:bg-neutral-7 active:bg-neutral-8',\n },\n soft: {\n accent: 'bg-accent-3 text-accent-11 hover:bg-accent-4 active:bg-accent-5',\n error: 'bg-error-3 text-error-11 hover:bg-error-4 active:bg-error-5',\n success: 'bg-success-3 text-success-11 hover:bg-success-4 active:bg-success-5',\n warning: 'bg-warning-3 text-warning-11 hover:bg-warning-4 active:bg-warning-5',\n neutral: 'bg-surface-raised-hover text-surface-fg-muted hover:bg-surface-raised-active active:bg-neutral-5',\n },\n outline: {\n accent: 'bg-transparent text-accent-11 hover:bg-accent-3 active:bg-accent-4',\n error: 'bg-transparent text-error-11 hover:bg-error-3 active:bg-error-4',\n success: 'bg-transparent text-success-11 hover:bg-success-3 active:bg-success-4',\n warning: 'bg-transparent text-warning-11 hover:bg-warning-3 active:bg-warning-4',\n neutral: 'bg-transparent text-surface-fg hover:bg-surface-raised-hover active:bg-surface-raised-active',\n },\n }\n return map[variant][color] ?? map[variant].accent\n}\n\nfunction getOutlineBorderColor(color: string): string {\n const map: Record<string, string> = {\n accent: 'border-accent-7', error: 'border-error-7', success: 'border-success-7',\n warning: 'border-warning-7', neutral: 'border-surface-border-strong',\n }\n return map[color] ?? map.accent\n}\n\n// ── Chevron SVG ─────────────────────────────────────────────────\n\nfunction ChevronDown({ className }: { className?: string }) {\n return (\n <svg width=\"12\" height=\"12\" viewBox=\"0 0 12 12\" fill=\"none\" className={className} aria-hidden=\"true\">\n <path d=\"M3 4.5L6 7.5L9 4.5\" stroke=\"currentColor\" strokeWidth=\"1.5\" strokeLinecap=\"round\" strokeLinejoin=\"round\" />\n </svg>\n )\n}\n\n// ── Component ───────────────────────────────────────────────────\n\nconst SplitButton = React.forwardRef<HTMLDivElement, SplitButtonProps>(\n function SplitButton(\n {\n children,\n onClick,\n dropdownContent,\n open: openProp,\n onOpenChange,\n variant = 'solid',\n color = 'accent',\n size = 'md',\n disabled = false,\n 'aria-label': ariaLabel,\n dropdownLabel = 'More options',\n dropdownIcon,\n triggerSide = 'right',\n placement: placementProp = 'top-end',\n triggerWidth,\n className,\n },\n ref,\n ) {\n // Map the floating-ui `placement` (e.g. 'top-end') to Radix side + align.\n // Positioning, focus-in/return, outside-click, and Escape all come from the\n // Popover primitive now — no hand-rolled floating-ui / listeners.\n const [side, align] = React.useMemo(() => {\n const [s, a] = placementProp.split('-')\n return [\n s as 'top' | 'right' | 'bottom' | 'left',\n (a === 'start' ? 'start' : a === 'end' ? 'end' : 'center') as\n | 'start'\n | 'center'\n | 'end',\n ]\n }, [placementProp])\n\n const halfClasses = getHalfClasses(variant, color)\n const divider = dividerColor[variant][color] ?? dividerColor[variant].accent\n\n const triggerStyle = triggerWidth != null\n ? { width: typeof triggerWidth === 'number' ? `${triggerWidth}px` : triggerWidth }\n : undefined\n\n // Dropdown trigger (the chevron half). PopoverTrigger wires aria-haspopup /\n // aria-expanded / focus-return for us.\n const dropdownTrigger = (\n <PopoverTrigger asChild>\n <button\n type=\"button\"\n disabled={disabled}\n aria-label={dropdownLabel}\n className={cn(\n 'inline-flex items-center justify-center select-none',\n 'transition-colors duration-fast-01 ease-productive-standard',\n 'focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-accent-9 focus-visible:ring-inset',\n 'disabled:pointer-events-none disabled:opacity-action-disabled',\n halfClasses,\n heightClass[size],\n triggerPadding[size],\n )}\n style={triggerStyle}\n >\n {dropdownIcon ?? <ChevronDown />}\n </button>\n </PopoverTrigger>\n )\n\n return (\n <Popover open={openProp} onOpenChange={onOpenChange}>\n {/* Button group */}\n <div\n ref={ref}\n role=\"group\"\n aria-label={ariaLabel ?? undefined}\n className={cn('relative inline-flex', className)}\n >\n <div\n className={cn(\n 'inline-flex items-stretch overflow-hidden',\n radiusClass[size],\n variant === 'solid' && 'shadow-raised',\n variant === 'outline' && `border ${getOutlineBorderColor(color)}`,\n )}\n >\n {triggerSide === 'left' && (\n <>\n {dropdownTrigger}\n <div className={cn('w-px self-stretch', divider)} />\n </>\n )}\n\n {/* Primary action */}\n <button\n type=\"button\"\n onClick={onClick}\n disabled={disabled}\n aria-label={ariaLabel}\n className={cn(\n 'inline-flex items-center justify-center font-semibold select-none whitespace-nowrap',\n 'transition-[colors,transform] duration-fast-01 ease-productive-standard',\n 'active:scale-[0.97]',\n 'focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-accent-9 focus-visible:ring-inset',\n 'disabled:pointer-events-none disabled:opacity-action-disabled',\n halfClasses,\n heightClass[size],\n textClass[size],\n primaryPadding[size],\n )}\n >\n {children}\n </button>\n\n {triggerSide === 'right' && (\n <>\n <div className={cn('w-px self-stretch', divider)} />\n {dropdownTrigger}\n </>\n )}\n </div>\n </div>\n\n {/* Dropdown panel — Popover handles focus, Escape, outside-click, return */}\n {dropdownContent && (\n <PopoverContent\n side={side}\n align={align}\n sideOffset={8}\n className=\"w-auto min-w-[10rem] p-ds-01\"\n >\n {dropdownContent}\n </PopoverContent>\n )}\n </Popover>\n )\n },\n)\n\nSplitButton.displayName = 'SplitButton'\n\nexport { SplitButton }\nexport type { SplitButtonColor, SplitButtonSize,SplitButtonVariant }\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"split-button.js","names":[],"sources":["../../src/ui/split-button.tsx"],"sourcesContent":["'use client'\n\nimport { type Placement } from '@floating-ui/dom'\nimport { type VariantProps } from 'class-variance-authority'\nimport * as React from 'react'\n\nimport { buttonVariants } from './button'\nimport { cn } from './lib/utils'\nimport { Popover, PopoverContent, PopoverTrigger } from './popover'\n\n// ── Types ───────────────────────────────────────────────────────\n\ntype SplitButtonVariant = 'solid' | 'soft' | 'outline'\ntype SplitButtonColor = NonNullable<VariantProps<typeof buttonVariants>['color']>\ntype SplitButtonSize = 'xs' | 'sm' | 'md' | 'icon-xs' | 'icon-sm' | 'icon-md'\n\nexport interface SplitButtonProps {\n /** Primary action content (left side). */\n children: React.ReactNode\n /** Primary click handler (left side). */\n onClick: (e: React.MouseEvent<HTMLButtonElement>) => void\n /** Content rendered inside the floating dropdown panel. */\n dropdownContent?: React.ReactNode\n /** Controlled open state. */\n open?: boolean\n /** Called when open state changes. Uncontrolled if omitted. */\n onOpenChange?: (open: boolean) => void\n /** @default 'solid' */\n variant?: SplitButtonVariant\n /** @default 'accent' */\n color?: SplitButtonColor\n /** @default 'md' */\n size?: SplitButtonSize\n /** Disable both halves. */\n disabled?: boolean\n /** aria-label for the primary button. */\n 'aria-label'?: string\n /** aria-label for the dropdown trigger. @default 'More options' */\n dropdownLabel?: string\n /** Custom icon for the dropdown trigger. Defaults to chevron-down. */\n dropdownIcon?: React.ReactNode\n /** Which side the dropdown trigger sits on. @default 'right' */\n triggerSide?: 'left' | 'right'\n /** Preferred placement for the dropdown panel. @default 'top-end' */\n placement?: Placement\n /** Width of the trigger (chevron) half. @default 'auto' */\n triggerWidth?: number | string\n className?: string\n}\n\n// ── Styling maps ────────────────────────────────────────────────\n\nconst dividerColor: Record<SplitButtonVariant, Record<string, string>> = {\n solid: {\n accent: 'bg-accent-11/20', error: 'bg-error-11/20', success: 'bg-success-11/20',\n warning: 'bg-warning-11/20', info: 'bg-info-11/20', neutral: 'bg-neutral-8/30',\n },\n soft: {\n accent: 'bg-accent-6', error: 'bg-error-6', success: 'bg-success-6',\n warning: 'bg-warning-6', info: 'bg-info-6', neutral: 'bg-surface-border',\n },\n outline: {\n accent: 'bg-accent-7', error: 'bg-error-7', success: 'bg-success-7',\n warning: 'bg-warning-7', info: 'bg-info-7', neutral: 'bg-surface-border-strong',\n },\n}\n\nconst heightClass: Record<SplitButtonSize, string> = {\n xs: 'h-ds-xs-plus', sm: 'h-ds-sm', md: 'h-ds-md',\n 'icon-xs': 'h-ds-xs-plus', 'icon-sm': 'h-ds-sm', 'icon-md': 'h-ds-md',\n}\n\nconst textClass: Record<SplitButtonSize, string> = {\n xs: 'text-body-sm', sm: 'text-body-sm', md: 'text-body-md',\n 'icon-xs': 'text-body-sm', 'icon-sm': 'text-body-sm', 'icon-md': 'text-body-md',\n}\n\nconst primaryPadding: Record<SplitButtonSize, string> = {\n xs: 'px-ds-03 gap-1', sm: 'px-ds-04 gap-1.5', md: 'px-ds-05 gap-2',\n 'icon-xs': 'px-ds-02', 'icon-sm': 'px-ds-03', 'icon-md': 'px-ds-04',\n}\n\nconst triggerPadding: Record<SplitButtonSize, string> = {\n xs: 'px-ds-02', sm: 'px-ds-02', md: 'px-ds-03',\n 'icon-xs': 'px-ds-01', 'icon-sm': 'px-ds-02', 'icon-md': 'px-ds-02',\n}\n\nconst radiusClass: Record<SplitButtonSize, string> = {\n xs: 'rounded-control', sm: 'rounded-control', md: 'rounded-control',\n 'icon-xs': 'rounded-control', 'icon-sm': 'rounded-control', 'icon-md': 'rounded-control',\n}\n\nfunction getHalfClasses(variant: SplitButtonVariant, color: string): string {\n const map: Record<SplitButtonVariant, Record<string, string>> = {\n solid: {\n accent: 'bg-accent-9 text-accent-fg hover:bg-accent-10 active:bg-accent-11',\n error: 'bg-error-9 text-error-fg hover:bg-error-10 active:bg-error-11',\n success: 'bg-success-9 text-success-fg hover:bg-success-10 active:bg-success-11',\n warning: 'bg-warning-9 text-warning-fg hover:bg-warning-10 active:bg-warning-11',\n info: 'bg-info-9 text-info-fg hover:bg-info-10 active:bg-info-11',\n neutral: 'bg-neutral-5 text-surface-fg hover:bg-neutral-7 active:bg-neutral-8',\n },\n soft: {\n accent: 'bg-accent-3 text-accent-11 hover:bg-accent-4 active:bg-accent-5',\n error: 'bg-error-3 text-error-11 hover:bg-error-4 active:bg-error-5',\n success: 'bg-success-3 text-success-11 hover:bg-success-4 active:bg-success-5',\n warning: 'bg-warning-3 text-warning-11 hover:bg-warning-4 active:bg-warning-5',\n info: 'bg-info-3 text-info-11 hover:bg-info-4 active:bg-info-5',\n neutral: 'bg-surface-raised-hover text-surface-fg-muted hover:bg-surface-raised-active active:bg-neutral-5',\n },\n outline: {\n accent: 'bg-transparent text-accent-11 hover:bg-accent-3 active:bg-accent-4',\n error: 'bg-transparent text-error-11 hover:bg-error-3 active:bg-error-4',\n success: 'bg-transparent text-success-11 hover:bg-success-3 active:bg-success-4',\n warning: 'bg-transparent text-warning-11 hover:bg-warning-3 active:bg-warning-4',\n info: 'bg-transparent text-info-11 hover:bg-info-3 active:bg-info-4',\n neutral: 'bg-transparent text-surface-fg hover:bg-surface-raised-hover active:bg-surface-raised-active',\n },\n }\n return map[variant][color] ?? map[variant].accent\n}\n\nfunction getOutlineBorderColor(color: string): string {\n const map: Record<string, string> = {\n accent: 'border-accent-7', error: 'border-error-7', success: 'border-success-7',\n warning: 'border-warning-7', info: 'border-info-7', neutral: 'border-surface-border-strong',\n }\n return map[color] ?? map.accent\n}\n\n// ── Chevron SVG ─────────────────────────────────────────────────\n\nfunction ChevronDown({ className }: { className?: string }) {\n return (\n <svg width=\"12\" height=\"12\" viewBox=\"0 0 12 12\" fill=\"none\" className={className} aria-hidden=\"true\">\n <path d=\"M3 4.5L6 7.5L9 4.5\" stroke=\"currentColor\" strokeWidth=\"1.5\" strokeLinecap=\"round\" strokeLinejoin=\"round\" />\n </svg>\n )\n}\n\n// ── Component ───────────────────────────────────────────────────\n\nconst SplitButton = React.forwardRef<HTMLDivElement, SplitButtonProps>(\n function SplitButton(\n {\n children,\n onClick,\n dropdownContent,\n open: openProp,\n onOpenChange,\n variant = 'solid',\n color = 'accent',\n size = 'md',\n disabled = false,\n 'aria-label': ariaLabel,\n dropdownLabel = 'More options',\n dropdownIcon,\n triggerSide = 'right',\n placement: placementProp = 'top-end',\n triggerWidth,\n className,\n },\n ref,\n ) {\n // Map the floating-ui `placement` (e.g. 'top-end') to Radix side + align.\n // Positioning, focus-in/return, outside-click, and Escape all come from the\n // Popover primitive now — no hand-rolled floating-ui / listeners.\n const [side, align] = React.useMemo(() => {\n const [s, a] = placementProp.split('-')\n return [\n s as 'top' | 'right' | 'bottom' | 'left',\n (a === 'start' ? 'start' : a === 'end' ? 'end' : 'center') as\n | 'start'\n | 'center'\n | 'end',\n ]\n }, [placementProp])\n\n const halfClasses = getHalfClasses(variant, color)\n const divider = dividerColor[variant][color] ?? dividerColor[variant].accent\n\n const triggerStyle = triggerWidth != null\n ? { width: typeof triggerWidth === 'number' ? `${triggerWidth}px` : triggerWidth }\n : undefined\n\n // Dropdown trigger (the chevron half). PopoverTrigger wires aria-haspopup /\n // aria-expanded / focus-return for us.\n const dropdownTrigger = (\n <PopoverTrigger asChild>\n <button\n type=\"button\"\n disabled={disabled}\n aria-label={dropdownLabel}\n className={cn(\n 'inline-flex items-center justify-center select-none',\n 'transition-colors duration-fast-01 ease-productive-standard',\n 'focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-accent-9 focus-visible:ring-inset',\n 'disabled:pointer-events-none disabled:opacity-action-disabled',\n halfClasses,\n heightClass[size],\n triggerPadding[size],\n )}\n style={triggerStyle}\n >\n {dropdownIcon ?? <ChevronDown />}\n </button>\n </PopoverTrigger>\n )\n\n return (\n <Popover open={openProp} onOpenChange={onOpenChange}>\n {/* Button group */}\n <div\n ref={ref}\n role=\"group\"\n aria-label={ariaLabel ?? undefined}\n className={cn('relative inline-flex', className)}\n >\n <div\n className={cn(\n 'inline-flex items-stretch overflow-hidden',\n radiusClass[size],\n variant === 'solid' && 'shadow-raised',\n variant === 'outline' && `border ${getOutlineBorderColor(color)}`,\n )}\n >\n {triggerSide === 'left' && (\n <>\n {dropdownTrigger}\n <div className={cn('w-px self-stretch', divider)} />\n </>\n )}\n\n {/* Primary action */}\n <button\n type=\"button\"\n onClick={onClick}\n disabled={disabled}\n aria-label={ariaLabel}\n className={cn(\n 'inline-flex items-center justify-center font-semibold select-none whitespace-nowrap',\n 'transition-[colors,transform] duration-fast-01 ease-productive-standard',\n 'active:scale-[0.97]',\n 'focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-accent-9 focus-visible:ring-inset',\n 'disabled:pointer-events-none disabled:opacity-action-disabled',\n halfClasses,\n heightClass[size],\n textClass[size],\n primaryPadding[size],\n )}\n >\n {children}\n </button>\n\n {triggerSide === 'right' && (\n <>\n <div className={cn('w-px self-stretch', divider)} />\n {dropdownTrigger}\n </>\n )}\n </div>\n </div>\n\n {/* Dropdown panel — Popover handles focus, Escape, outside-click, return */}\n {dropdownContent && (\n <PopoverContent\n side={side}\n align={align}\n sideOffset={8}\n className=\"w-auto min-w-[10rem] p-ds-01\"\n >\n {dropdownContent}\n </PopoverContent>\n )}\n </Popover>\n )\n },\n)\n\nSplitButton.displayName = 'SplitButton'\n\nexport { SplitButton }\nexport type { SplitButtonColor, SplitButtonSize,SplitButtonVariant }\n"],"mappings":"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"}
|
package/dist/ui/stat-card.d.ts
CHANGED
|
@@ -7,8 +7,8 @@ import * as React from 'react';
|
|
|
7
7
|
* Props for StatCard — a dashboard metric tile displaying a label, a large numeric value,
|
|
8
8
|
* an optional trend delta (with directional arrow icon), and an optional header icon.
|
|
9
9
|
*
|
|
10
|
-
* **Surface:** delegated to `Card` via `variant` — `default` (
|
|
11
|
-
* `elevated` | `outline` (border, no shadow) | `flat`. StatCard composes `<Card>`, so the surface,
|
|
10
|
+
* **Surface:** delegated to `Card` via `variant` — `default` (tonal hairline, no shadow) |
|
|
11
|
+
* `elevated` (shadow) | `outline` (strong border, no shadow) | `flat`. StatCard composes `<Card>`, so the surface,
|
|
12
12
|
* padding (gap model), and elevation all live in one place.
|
|
13
13
|
*
|
|
14
14
|
* **Accent:** `accentStyle="none"` (default) is neutral; `"icon"` wraps `icon` in an accent chip
|
|
@@ -75,7 +75,7 @@ export interface StatCardProps extends Omit<React.HTMLAttributes<HTMLDivElement>
|
|
|
75
75
|
secondaryLabel?: string;
|
|
76
76
|
/** Progress toward a target (0-100). Renders a thin progress bar below the value. */
|
|
77
77
|
progress?: number;
|
|
78
|
-
/** Surface variant, delegated to Card: `default` (
|
|
78
|
+
/** Surface variant, delegated to Card: `default` (tonal hairline, no shadow) | `elevated` (shadow) | `outline` (strong border, no shadow) | `flat`. @default 'default' */
|
|
79
79
|
variant?: 'default' | 'elevated' | 'outline' | 'flat';
|
|
80
80
|
/**
|
|
81
81
|
* Tile density, delegated to Card's size axis. `sm` tightens padding to 16px and steps
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"stat-card.d.ts","sourceRoot":"","sources":["../../src/ui/stat-card.tsx"],"names":[],"mappings":"AAIA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAE9B,OAAO,EAAiC,KAAK,QAAQ,EAAE,MAAM,QAAQ,CAAA;AAGrE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAA;AAOjD,OAAO,EAAE,KAAK,WAAW,EAAE,KAAK,SAAS,EAAE,KAAK,UAAU,EAAY,MAAM,cAAc,CAAA;AAE1F;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+CG;AACH,MAAM,WAAW,aAAc,SAAQ,IAAI,CAAC,KAAK,CAAC,cAAc,CAAC,cAAc,CAAC,EAAE,OAAO,CAAC;IACxF,oEAAoE;IACpE,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,uDAAuD;IACvD,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,KAAK,EAAE,MAAM,GAAG,MAAM,CAAA;IACtB,oCAAoC;IACpC,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,uCAAuC;IACvC,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,KAAK,CAAC,EAAE;QACN,KAAK,EAAE,MAAM,CAAA;QACb,SAAS,EAAE,IAAI,GAAG,MAAM,GAAG,SAAS,CAAA;KACrC,CAAA;IACD,kJAAkJ;IAClJ,cAAc,CAAC,EAAE,OAAO,GAAG,QAAQ,CAAA;IACnC,IAAI,CAAC,EAAE,SAAS,CAAA;IAChB,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,sEAAsE;IACtE,eAAe,CAAC,EAAE,MAAM,CAAA;IACxB,kEAAkE;IAClE,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,qFAAqF;IACrF,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB
|
|
1
|
+
{"version":3,"file":"stat-card.d.ts","sourceRoot":"","sources":["../../src/ui/stat-card.tsx"],"names":[],"mappings":"AAIA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAE9B,OAAO,EAAiC,KAAK,QAAQ,EAAE,MAAM,QAAQ,CAAA;AAGrE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAA;AAOjD,OAAO,EAAE,KAAK,WAAW,EAAE,KAAK,SAAS,EAAE,KAAK,UAAU,EAAY,MAAM,cAAc,CAAA;AAE1F;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+CG;AACH,MAAM,WAAW,aAAc,SAAQ,IAAI,CAAC,KAAK,CAAC,cAAc,CAAC,cAAc,CAAC,EAAE,OAAO,CAAC;IACxF,oEAAoE;IACpE,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,uDAAuD;IACvD,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,KAAK,EAAE,MAAM,GAAG,MAAM,CAAA;IACtB,oCAAoC;IACpC,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,uCAAuC;IACvC,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,KAAK,CAAC,EAAE;QACN,KAAK,EAAE,MAAM,CAAA;QACb,SAAS,EAAE,IAAI,GAAG,MAAM,GAAG,SAAS,CAAA;KACrC,CAAA;IACD,kJAAkJ;IAClJ,cAAc,CAAC,EAAE,OAAO,GAAG,QAAQ,CAAA;IACnC,IAAI,CAAC,EAAE,SAAS,CAAA;IAChB,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,sEAAsE;IACtE,eAAe,CAAC,EAAE,MAAM,CAAA;IACxB,kEAAkE;IAClE,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,qFAAqF;IACrF,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,0KAA0K;IAC1K,OAAO,CAAC,EAAE,SAAS,GAAG,UAAU,GAAG,SAAS,GAAG,MAAM,CAAA;IACrD;;;;OAIG;IACH,IAAI,CAAC,EAAE,QAAQ,CAAA;IACf,yJAAyJ;IACzJ,WAAW,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,MAAM,CAAA;IACtC,6HAA6H;IAC7H,MAAM,CAAC,EAAE,OAAO,CAAA;IAChB,uGAAuG;IACvG,QAAQ,CAAC,EAAE,MAAM,GAAG,OAAO,CAAA;IAC3B;;;;OAIG;IACH,KAAK,CAAC,EAAE,WAAW,GAAG,SAAS,CAAA;IAC/B,iFAAiF;IACjF,UAAU,CAAC,EAAE,UAAU,CAAA;IACvB,wEAAwE;IACxE,SAAS,CAAC,EAAE,MAAM,EAAE,CAAA;IACpB,+CAA+C;IAC/C,OAAO,CAAC,EAAE,MAAM,IAAI,CAAA;IACpB,yEAAyE;IACzE,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,yEAAyE;IACzE,MAAM,CAAC,EAAE,KAAK,CAAC,SAAS,CAAA;CACzB;AAgGD,QAAA,MAAM,QAAQ,sFAoQb,CAAA;AAID,OAAO,EAAE,QAAQ,EAAE,CAAA"}
|
package/dist/ui/stat-card.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"stat-card.js","names":[],"sources":["../../src/ui/stat-card.tsx"],"sourcesContent":["'use client'\n\nimport { IconMinus, IconTrendingDown, IconTrendingUp } from '@tabler/icons-react'\nimport { motion } from 'framer-motion'\nimport * as React from 'react'\n\nimport { Card, CardContent, CardFooter, type CardSize } from './card'\nimport { Icon } from './icon'\nimport { IconProvider } from './icon-context'\nimport type { IconInput } from './lib/icon-input'\nimport { useLink } from './lib/link-context'\nimport { springs, tweens } from './lib/motion'\nimport { normalizeIcon } from './lib/normalize-icon'\nimport { cn } from './lib/utils'\nimport { Progress, type ProgressColor } from './progress'\nimport { Skeleton } from './skeleton'\nimport { type FlashPreset, type FlashSpec, type FlashSpeed,StatFlash } from './stat-flash'\n\n/**\n * Props for StatCard — a dashboard metric tile displaying a label, a large numeric value,\n * an optional trend delta (with directional arrow icon), and an optional header icon.\n *\n * **Surface:** delegated to `Card` via `variant` — `default` (ring-in-shadow, no border) |\n * `elevated` | `outline` (border, no shadow) | `flat`. StatCard composes `<Card>`, so the surface,\n * padding (gap model), and elevation all live in one place.\n *\n * **Accent:** `accentStyle=\"none\"` (default) is neutral; `\"icon\"` wraps `icon` in an accent chip\n * (`iconFill=\"soft\" | \"solid\"`); `\"tint\"` applies a subtle accent surface wash + accent value.\n * (Replaces the removed `accent` left-rail prop — see CHANGELOG migration.)\n *\n * **Delta direction:** `'up'` renders a green trending-up arrow, `'down'` renders a red\n * trending-down arrow, `'neutral'` renders a grey dash. The `delta.value` is a formatted\n * string (e.g. `\"+8%\"` or `\"−120\"`).\n *\n * **Loading state:** Pass `loading={true}` to render a pulse-skeleton placeholder instead of data.\n *\n * @example\n * // Revenue metric with a positive trend:\n * <StatCard\n * label=\"Monthly Revenue\"\n * value=\"$48,200\"\n * delta={{ value: \"+12%\", direction: \"up\" }}\n * icon={<IconCurrencyDollar />}\n * />\n *\n * @example\n * // Open ticket count with a downward trend (good for support queues):\n * <StatCard\n * label=\"Open Tickets\"\n * value={142}\n * delta={{ value: \"−18\", direction: \"down\" }}\n * />\n *\n * @example\n * // Loading skeleton while data is fetching:\n * <StatCard label=\"Users\" value={0} loading={isFetching} />\n *\n * @example\n * // Simple metric with no trend (stable, neutral):\n * <StatCard\n * label=\"Storage Used\"\n * value=\"4.2 GB\"\n * delta={{ value: \"No change\", direction: \"neutral\" }}\n * />\n * // These are just a few ways — feel free to combine props creatively!\n */\nexport interface StatCardProps extends Omit<React.HTMLAttributes<HTMLDivElement>, 'color'> {\n /** Heading text for the metric. `title` is an alias for `label`. */\n label?: string\n /** Alias for `label` — use whichever feels natural. */\n title?: string\n value: string | number\n /** Prefix before value, e.g. \"$\" */\n prefix?: string\n /** Suffix after value, e.g. \"users\" */\n suffix?: string\n delta?: {\n value: string\n direction: 'up' | 'down' | 'neutral'\n }\n /** Where the delta sits relative to the value: `block` (below — default) or `inline` (on the value's baseline, to its right). @default 'block' */\n deltaPlacement?: 'block' | 'inline'\n icon?: IconInput\n loading?: boolean\n /** Comparison period label shown after delta, e.g. \"vs last month\" */\n comparisonLabel?: string\n /** Secondary metric below main value, e.g. \"of $50,000 target\" */\n secondaryLabel?: string\n /** Progress toward a target (0-100). Renders a thin progress bar below the value. */\n progress?: number\n /** Surface variant, delegated to Card: `default` (ring-in-shadow) | `elevated` | `outline` (border, no shadow) | `flat`. @default 'default' */\n variant?: 'default' | 'elevated' | 'outline' | 'flat'\n /**\n * Tile density, delegated to Card's size axis. `sm` tightens padding to 16px and steps\n * the value down to `text-heading-md` — use it for dense KPI rows and narrow stat grids\n * (e.g. stat-row's 140px tiles). @default 'md'\n */\n size?: CardSize\n /** How the brand accent reads: `none` (neutral — default), `icon` (accent chip around `icon`), or `tint` (subtle accent surface wash + accent value). */\n accentStyle?: 'none' | 'icon' | 'tint'\n /** Animate value/label in on mount (subtle settle; content stays visible either way, never opacity-gated). @default false */\n reveal?: boolean\n /** When `accentStyle=\"icon\"`: `soft` (tinted chip) or `solid` (filled accent chip). @default 'soft' */\n iconFill?: 'soft' | 'solid'\n /**\n * Opt-in entrance flash: the icon chip mounts showing a transient state (e.g. a green up-arrow)\n * then settles to `icon`. A preset (`'up' | 'down' | 'goal' | 'record' | 'alert' | 'live'`) or an\n * explicit `{ tone, icon }`. Requires `icon`; implies the icon chip. Off by default.\n */\n flash?: FlashPreset | FlashSpec\n /** Speed of the entrance flash: `fast` | `normal` | `slow`. @default 'normal' */\n flashSpeed?: FlashSpeed\n /** Sparkline data points. Renders a mini SVG line chart in the card. */\n sparkline?: number[]\n /** Make the card clickable with hover state */\n onClick?: () => void\n /** Href — makes the card a link (uses the LinkContext Link component) */\n href?: string\n /** Footer content rendered below the card body, e.g. \"View details →\" */\n footer?: React.ReactNode\n}\n\nfunction buildSparklinePath(raw: number[], width: number, height: number): string {\n const data = raw.filter((v) => Number.isFinite(v))\n if (data.length < 2) return ''\n const min = Math.min(...data)\n const max = Math.max(...data)\n const range = max - min || 1\n const stepX = width / (data.length - 1)\n return data\n .map((v, i) => {\n const x = i * stepX\n const y = height - ((v - min) / range) * height\n return `${i === 0 ? 'M' : 'L'}${x.toFixed(1)},${y.toFixed(1)}`\n })\n .join(' ')\n}\n\nfunction Sparkline({\n data,\n colorClass,\n}: {\n data: number[]\n colorClass: string\n}) {\n const id = React.useId()\n const pathRef = React.useRef<SVGPathElement>(null)\n const [pathLength, setPathLength] = React.useState(0)\n const svgWidth = 80\n const svgHeight = 32\n const path = buildSparklinePath(data, svgWidth, svgHeight)\n\n React.useEffect(() => {\n if (pathRef.current) {\n setPathLength(pathRef.current.getTotalLength())\n }\n }, [path])\n\n if (!path) return null\n\n // Build area fill path (close to bottom-right then bottom-left)\n const areaPath = `${path} L${svgWidth.toFixed(1)},${svgHeight.toFixed(1)} L0,${svgHeight.toFixed(1)} Z`\n\n return (\n <div className={cn('w-20 h-8', colorClass)} aria-hidden=\"true\">\n <svg\n viewBox={`0 0 ${svgWidth} ${svgHeight}`}\n className=\"w-full h-full overflow-visible\"\n preserveAspectRatio=\"none\"\n >\n <defs>\n <linearGradient id={`sparkline-fill-${id}`} x1=\"0\" y1=\"0\" x2=\"0\" y2=\"1\">\n <stop offset=\"0%\" stopColor=\"currentColor\" stopOpacity=\"0.1\" />\n <stop offset=\"100%\" stopColor=\"currentColor\" stopOpacity=\"0\" />\n </linearGradient>\n </defs>\n <path d={areaPath} fill={`url(#sparkline-fill-${id})`} />\n <path\n ref={pathRef}\n d={path}\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth=\"1.5\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n style={{\n opacity: pathLength === 0 ? 0 : 1,\n strokeDasharray: pathLength,\n strokeDashoffset: pathLength,\n animation: pathLength > 0 ? `sparkline-draw-${id.replace(/:/g, '')} 1s ease-out forwards` : 'none',\n }}\n />\n </svg>\n {pathLength > 0 && (\n <style>{`@keyframes sparkline-draw-${id.replace(/:/g, '')} { to { stroke-dashoffset: 0; } }`}</style>\n )}\n </div>\n )\n}\n\nfunction ProgressBar({ progress, label }: { progress: number; label: string }) {\n const clamped = Math.max(0, Math.min(100, progress))\n // StatCard's own thresholds (90 = success, 70 = warning) — deliberately distinct\n // from Progress's autoColor (85/60), so we pass an explicit color.\n const barColor: ProgressColor =\n clamped >= 90 ? 'success' : clamped >= 70 ? 'warning' : 'accent'\n\n return (\n <Progress.Root value={clamped} size=\"sm\">\n <Progress.Track aria-label={`${label} progress`}>\n <Progress.Indicator color={barColor} />\n </Progress.Track>\n </Progress.Root>\n )\n}\n\nconst StatCard = React.forwardRef<HTMLDivElement, StatCardProps>(\n (\n {\n className,\n label,\n title,\n value,\n prefix,\n suffix,\n delta,\n deltaPlacement = 'block',\n icon,\n loading,\n comparisonLabel,\n secondaryLabel,\n progress,\n variant = 'default',\n size = 'md',\n accentStyle = 'none',\n reveal = false,\n iconFill = 'soft',\n flash,\n flashSpeed,\n sparkline,\n onClick,\n href,\n footer,\n ...props\n },\n ref,\n ) => {\n const Link = useLink()\n const resolvedLabel = title ?? label ?? ''\n const isClickable = !!(onClick || href)\n const computedAriaLabel =\n isClickable && !props['aria-label'] ? `View ${resolvedLabel}` : props['aria-label']\n\n if (loading) {\n return (\n <Card ref={ref} variant={variant} size={size} className={className} aria-busy=\"true\" {...props}>\n <CardContent className=\"flex flex-col gap-ds-03\">\n <Skeleton className=\"h-ds-04 w-24 rounded-control-inner\" />\n <Skeleton className=\"h-ds-sm w-32\" />\n <Skeleton className=\"h-3 w-16 rounded-control-inner\" />\n </CardContent>\n </Card>\n )\n }\n\n const DeltaIcon =\n delta?.direction === 'up'\n ? IconTrendingUp\n : delta?.direction === 'down'\n ? IconTrendingDown\n : IconMinus\n\n const deltaColour =\n delta?.direction === 'up'\n ? 'text-success-11'\n : delta?.direction === 'down'\n ? 'text-error-11'\n : 'text-surface-fg-muted'\n\n const sparklineColor =\n delta?.direction === 'up'\n ? 'text-success-11'\n : delta?.direction === 'down'\n ? 'text-error-11'\n : 'text-accent-11'\n\n const deltaNode = delta ? (\n <motion.div\n className={cn(\n // Rhythm comes from CardContent's flex gap — no placement margins.\n 'flex items-center gap-ds-02 text-body-sm font-medium',\n deltaColour,\n )}\n initial={{ opacity: 0, y: 8 }}\n animate={{ opacity: 1, y: 0 }}\n transition={{ ...springs.smooth, delay: 0.2 }}\n >\n <motion.span\n className=\"inline-flex\"\n initial={{ opacity: 0.5, scale: 1.4 }}\n animate={{ opacity: 1, scale: 1 }}\n transition={springs.smooth}\n >\n <Icon icon={DeltaIcon} size=\"sm\" />\n </motion.span>\n <span>{delta.value}</span>\n {comparisonLabel && (\n <span className=\"text-surface-fg-subtle font-normal\">{comparisonLabel}</span>\n )}\n </motion.div>\n ) : null\n\n // reveal=false → static (content-safe default). reveal=true → subtle settle; opacity NEVER gated.\n const revealProps = reveal\n ? { initial: { y: 8 }, animate: { y: 0 }, transition: springs.smooth }\n : { initial: false as const }\n\n const cardContent = (\n <>\n <div className=\"flex items-center justify-between\">\n <motion.p\n className=\"text-body-md font-medium text-surface-fg-muted\"\n {...revealProps}\n >\n {resolvedLabel}\n </motion.p>\n <div className=\"flex items-center gap-ds-03\">\n {sparkline && sparkline.length >= 2 && (\n <Sparkline data={sparkline} colorClass={sparklineColor} />\n )}\n {icon && flash ? (\n <StatFlash icon={icon} flash={flash} fill={iconFill} speed={flashSpeed} />\n ) : icon ? (\n accentStyle === 'icon' ? (\n <motion.span\n className={cn(\n 'inline-flex items-center justify-center rounded-control p-ds-02',\n iconFill === 'solid'\n ? 'bg-accent-9 text-accent-fg'\n : 'bg-accent-3 text-accent-11',\n )}\n {...revealProps}\n aria-hidden=\"true\"\n >\n <IconProvider size=\"md\">{normalizeIcon(icon, 'md')}</IconProvider>\n </motion.span>\n ) : (\n <motion.span\n className=\"text-surface-fg-muted\"\n {...revealProps}\n aria-hidden=\"true\"\n >\n <IconProvider size=\"lg\">{normalizeIcon(icon, 'lg')}</IconProvider>\n </motion.span>\n )\n ) : null}\n </div>\n </div>\n <div className=\"flex flex-col gap-ds-01\">\n <div className={cn(deltaPlacement === 'inline' && delta && 'flex items-baseline gap-ds-03')}>\n <div className=\"overflow-hidden\">\n <motion.p\n className={cn(\n 'inline-block font-semibold',\n size === 'sm' ? 'text-heading-md' : 'text-heading-lg',\n accentStyle === 'tint' ? 'text-accent-11' : 'text-surface-fg',\n )}\n {...revealProps}\n >\n {prefix && (\n <span className={cn('text-surface-fg-muted', size === 'sm' ? 'text-body-md' : 'text-heading-xs')}>{prefix}</span>\n )}\n <span className=\"tabular-nums\">{value}</span>\n {suffix && (\n <span className={cn('text-surface-fg-muted', size === 'sm' ? 'text-body-md' : 'text-heading-xs')}>{suffix}</span>\n )}\n </motion.p>\n </div>\n {deltaPlacement === 'inline' && deltaNode}\n </div>\n {secondaryLabel && (\n <motion.p\n className=\"text-body-sm text-surface-fg-subtle\"\n {...revealProps}\n >\n {secondaryLabel}\n </motion.p>\n )}\n </div>\n {progress != null && (\n <motion.div\n {...revealProps}\n >\n <ProgressBar progress={progress} label={resolvedLabel} />\n </motion.div>\n )}\n {deltaPlacement === 'block' && deltaNode}\n </>\n )\n\n // accentStyle=\"tint\" → a subtle accent gradient wash over Card's surface.\n const tintClass =\n accentStyle === 'tint' ? 'bg-linear-to-t from-accent-2 to-surface-raised' : undefined\n // Footer sits outside CardContent behind a full-width divider (both are direct\n // children of Card, so the rule spans edge-to-edge — no inset border-t).\n const body = (\n <>\n <CardContent className=\"flex flex-col gap-ds-03\">{cardContent}</CardContent>\n {footer && (\n <>\n <div aria-hidden=\"true\" className=\"h-px w-full bg-surface-border-subtle\" />\n <CardFooter className=\"text-body-sm\">\n <motion.div\n className=\"w-full\"\n initial={false}\n animate={{ opacity: 1 }}\n transition={{ ...tweens.fade, delay: 0.25 }}\n >\n {footer}\n </motion.div>\n </CardFooter>\n </>\n )}\n </>\n )\n\n // href → wrap Card in the framework Link (Card stays the shell; hover-lift inside).\n if (href) {\n return (\n <Link\n ref={ref as React.Ref<HTMLAnchorElement>}\n href={href}\n onClick={onClick}\n className=\"block no-underline\"\n aria-label={computedAriaLabel}\n {...(props as React.AnchorHTMLAttributes<HTMLAnchorElement>)}\n >\n <Card variant={variant} size={size} interactive className={tintClass}>\n {body}\n </Card>\n </Link>\n )\n }\n\n // onClick → interactive Card with button semantics + keyboard activation.\n if (onClick) {\n return (\n <Card\n ref={ref}\n variant={variant}\n size={size}\n interactive\n className={cn(tintClass, className)}\n role=\"button\"\n tabIndex={0}\n aria-label={computedAriaLabel}\n onClick={onClick}\n onKeyDown={(e: React.KeyboardEvent) => {\n if (e.key === 'Enter' || e.key === ' ') {\n e.preventDefault()\n onClick()\n }\n }}\n {...props}\n >\n {body}\n </Card>\n )\n }\n\n return (\n <Card ref={ref} variant={variant} size={size} className={cn(tintClass, className)} {...props}>\n {body}\n </Card>\n )\n },\n)\n\nStatCard.displayName = 'StatCard'\n\nexport { StatCard }\n"],"mappings":";;;;;;;;;;;;;;;;AA0HA,SAAS,EAAmB,GAAe,GAAe,GAAwB;CAChF,IAAM,IAAO,EAAI,QAAQ,MAAM,OAAO,SAAS,CAAC,CAAC;CACjD,IAAI,EAAK,SAAS,GAAG,OAAO;CAC5B,IAAM,IAAM,KAAK,IAAI,GAAG,CAAI,GAEtB,IADM,KAAK,IAAI,GAAG,CACV,IAAM,KAAO,GACrB,IAAQ,KAAS,EAAK,SAAS;CACrC,OAAO,EACJ,KAAK,GAAG,MAAM;EACb,IAAM,IAAI,IAAI,GACR,IAAI,KAAW,IAAI,KAAO,IAAS;EACzC,OAAO,GAAG,MAAM,IAAI,MAAM,MAAM,EAAE,QAAQ,CAAC,EAAE,GAAG,EAAE,QAAQ,CAAC;CAC7D,CAAC,CAAC,CACD,KAAK,GAAG;AACb;AAEA,SAAS,EAAU,EACjB,SACA,iBAIC;CACD,IAAM,IAAK,EAAM,MAAM,GACjB,IAAU,EAAM,OAAuB,IAAI,GAC3C,CAAC,GAAY,KAAiB,EAAM,SAAS,CAAC,GAG9C,IAAO,EAAmB,GAAM,IAAU,EAAS;CAQzD,IANA,EAAM,gBAAgB;EACpB,AAAI,EAAQ,WACV,EAAc,EAAQ,QAAQ,eAAe,CAAC;CAElD,GAAG,CAAC,CAAI,CAAC,GAEL,CAAC,GAAM,OAAO;CAGlB,IAAM,IAAW,GAAG,EAAK,IAAI,IAAS,QAAQ,CAAC,EAAE,GAAG,IAAU,QAAQ,CAAC,EAAE,MAAM,IAAU,QAAQ,CAAC,EAAE;CAEpG,OACE,kBAAC,OAAD;EAAK,WAAW,EAAG,YAAY,CAAU;EAAG,eAAY;YAAxD,CACE,kBAAC,OAAD;GACE,SAAS;GACT,WAAU;GACV,qBAAoB;aAHtB;IAKE,kBAAC,QAAD,EAAA,UACE,kBAAC,kBAAD;KAAgB,IAAI,kBAAkB;KAAM,IAAG;KAAI,IAAG;KAAI,IAAG;KAAI,IAAG;eAApE,CACE,kBAAC,QAAD;MAAM,QAAO;MAAK,WAAU;MAAe,aAAY;KAAO,CAAA,GAC9D,kBAAC,QAAD;MAAM,QAAO;MAAO,WAAU;MAAe,aAAY;KAAK,CAAA,CAChD;OACZ,CAAA;IACN,kBAAC,QAAD;KAAM,GAAG;KAAU,MAAM,uBAAuB,EAAG;IAAK,CAAA;IACxD,kBAAC,QAAD;KACE,KAAK;KACL,GAAG;KACH,MAAK;KACL,QAAO;KACP,aAAY;KACZ,eAAc;KACd,gBAAe;KACf,OAAO;MACL,SAAS,MAAe,IAAI,IAAI;MAChC,iBAAiB;MACjB,kBAAkB;MAClB,WAAW,IAAa,IAAI,kBAAkB,EAAG,QAAQ,MAAM,EAAE,EAAE,yBAAyB;KAC9F;IACD,CAAA;GACE;MACJ,IAAa,KACZ,kBAAC,SAAD,EAAA,UAAQ,6BAA6B,EAAG,QAAQ,MAAM,EAAE,EAAE,mCAA0C,CAAA,CAEnG;;AAET;AAEA,SAAS,EAAY,EAAE,aAAU,YAA8C;CAC7E,IAAM,IAAU,KAAK,IAAI,GAAG,KAAK,IAAI,KAAK,CAAQ,CAAC,GAG7C,IACJ,KAAW,KAAK,YAAY,KAAW,KAAK,YAAY;CAE1D,OACE,kBAAC,EAAS,MAAV;EAAe,OAAO;EAAS,MAAK;YAClC,kBAAC,EAAS,OAAV;GAAgB,cAAY,GAAG,EAAM;aACnC,kBAAC,EAAS,WAAV,EAAoB,OAAO,EAAW,CAAA;EACxB,CAAA;CACH,CAAA;AAEnB;AAEA,IAAM,IAAW,EAAM,YAEnB,EACE,cACA,UACA,UACA,UACA,WACA,WACA,UACA,oBAAiB,SACjB,SACA,aACA,oBACA,mBACA,aACA,aAAU,WACV,UAAO,MACP,iBAAc,QACd,YAAS,IACT,cAAW,QACX,UACA,eACA,cACA,YACA,SACA,WACA,GAAG,KAEL,MACG;CACH,IAAM,IAAO,EAAQ,GACf,IAAgB,KAAS,KAAS,IAElC,KADiB,KAAW,MAEjB,CAAC,EAAM,gBAAgB,QAAQ,MAAkB,EAAM;CAExE,IAAI,IACF,OACE,kBAAC,GAAD;EAAW;EAAc;EAAe;EAAiB;EAAW,aAAU;EAAO,GAAI;YACvF,kBAAC,GAAD;GAAa,WAAU;aAAvB;IACE,kBAAC,GAAD,EAAU,WAAU,qCAAsC,CAAA;IAC1D,kBAAC,GAAD,EAAU,WAAU,eAAgB,CAAA;IACpC,kBAAC,GAAD,EAAU,WAAU,iCAAkC,CAAA;GAC3C;;CACT,CAAA;CAIV,IAAM,IACJ,GAAO,cAAc,OACjB,IACA,GAAO,cAAc,SACnB,KACA,IAEF,IACJ,GAAO,cAAc,OACjB,oBACA,GAAO,cAAc,SACnB,kBACA,yBAEF,IACJ,GAAO,cAAc,OACjB,oBACA,GAAO,cAAc,SACnB,kBACA,kBAEF,IAAY,IAChB,kBAAC,EAAO,KAAR;EACE,WAAW,EAET,wDACA,CACF;EACA,SAAS;GAAE,SAAS;GAAG,GAAG;EAAE;EAC5B,SAAS;GAAE,SAAS;GAAG,GAAG;EAAE;EAC5B,YAAY;GAAE,GAAG,EAAQ;GAAQ,OAAO;EAAI;YAR9C;GAUE,kBAAC,EAAO,MAAR;IACE,WAAU;IACV,SAAS;KAAE,SAAS;KAAK,OAAO;IAAI;IACpC,SAAS;KAAE,SAAS;KAAG,OAAO;IAAE;IAChC,YAAY,EAAQ;cAEpB,kBAAC,GAAD;KAAM,MAAM;KAAW,MAAK;IAAM,CAAA;GACvB,CAAA;GACb,kBAAC,QAAD,EAAA,UAAO,EAAM,MAAY,CAAA;GACxB,KACC,kBAAC,QAAD;IAAM,WAAU;cAAsC;GAAsB,CAAA;EAEpE;MACV,MAGE,IAAc,IAChB;EAAE,SAAS,EAAE,GAAG,EAAE;EAAG,SAAS,EAAE,GAAG,EAAE;EAAG,YAAY,EAAQ;CAAO,IACnE,EAAE,SAAS,GAAe,GAExB,KACJ,kBAAA,GAAA,EAAA,UAAA;EACE,kBAAC,OAAD;GAAK,WAAU;aAAf,CACE,kBAAC,EAAO,GAAR;IACE,WAAU;IACV,GAAI;cAEH;GACO,CAAA,GACV,kBAAC,OAAD;IAAK,WAAU;cAAf,CACG,KAAa,EAAU,UAAU,KAChC,kBAAC,GAAD;KAAW,MAAM;KAAW,YAAY;IAAiB,CAAA,GAE1D,KAAQ,IACP,kBAAC,GAAD;KAAiB;KAAa;KAAO,MAAM;KAAU,OAAO;IAAa,CAAA,IACvE,IACF,MAAgB,SACd,kBAAC,EAAO,MAAR;KACE,WAAW,EACT,mEACA,MAAa,UACT,+BACA,4BACN;KACA,GAAI;KACJ,eAAY;eAEZ,kBAAC,GAAD;MAAc,MAAK;gBAAM,EAAc,GAAM,IAAI;KAAgB,CAAA;IACtD,CAAA,IAEb,kBAAC,EAAO,MAAR;KACE,WAAU;KACV,GAAI;KACJ,eAAY;eAEZ,kBAAC,GAAD;MAAc,MAAK;gBAAM,EAAc,GAAM,IAAI;KAAgB,CAAA;IACtD,CAAA,IAEb,IACD;KACF;;EACL,kBAAC,OAAD;GAAK,WAAU;aAAf,CACE,kBAAC,OAAD;IAAK,WAAW,EAAG,MAAmB,YAAY,KAAS,+BAA+B;cAA1F,CACE,kBAAC,OAAD;KAAK,WAAU;eACb,kBAAC,EAAO,GAAR;MACE,WAAW,EACT,8BACA,MAAS,OAAO,oBAAoB,mBACpC,MAAgB,SAAS,mBAAmB,iBAC9C;MACA,GAAI;gBANN;OAQG,KACC,kBAAC,QAAD;QAAM,WAAW,EAAG,yBAAyB,MAAS,OAAO,iBAAiB,iBAAiB;kBAAI;OAAa,CAAA;OAElH,kBAAC,QAAD;QAAM,WAAU;kBAAgB;OAAY,CAAA;OAC3C,KACC,kBAAC,QAAD;QAAM,WAAW,EAAG,yBAAyB,MAAS,OAAO,iBAAiB,iBAAiB;kBAAI;OAAa,CAAA;MAE1G;;IACP,CAAA,GACJ,MAAmB,YAAY,CAC7B;OACJ,KACC,kBAAC,EAAO,GAAR;IACE,WAAU;IACV,GAAI;cAEH;GACO,CAAA,CAET;;EACJ,KAAY,QACX,kBAAC,EAAO,KAAR;GACE,GAAI;aAEJ,kBAAC,GAAD;IAAuB;IAAU,OAAO;GAAgB,CAAA;EAC9C,CAAA;EAEb,MAAmB,WAAW;CAC/B,EAAA,CAAA,GAIE,IACJ,MAAgB,SAAS,mDAAmD,KAAA,GAGxE,IACJ,kBAAA,GAAA,EAAA,UAAA,CACE,kBAAC,GAAD;EAAa,WAAU;YAA2B;CAAyB,CAAA,GAC1E,KACC,kBAAA,GAAA,EAAA,UAAA,CACE,kBAAC,OAAD;EAAK,eAAY;EAAO,WAAU;CAAwC,CAAA,GAC1E,kBAAC,GAAD;EAAY,WAAU;YACpB,kBAAC,EAAO,KAAR;GACE,WAAU;GACV,SAAS;GACT,SAAS,EAAE,SAAS,EAAE;GACtB,YAAY;IAAE,GAAG,EAAO;IAAM,OAAO;GAAK;aAEzC;EACS,CAAA;CACF,CAAA,CACZ,EAAA,CAAA,CAEJ,EAAA,CAAA;CA+CJ,OA3CI,IAEA,kBAAC,GAAD;EACO;EACC;EACG;EACT,WAAU;EACV,cAAY;EACZ,GAAK;YAEL,kBAAC,GAAD;GAAe;GAAe;GAAM,aAAA;GAAY,WAAW;aACxD;EACG,CAAA;CACF,CAAA,IAKN,IAEA,kBAAC,GAAD;EACO;EACI;EACH;EACN,aAAA;EACA,WAAW,EAAG,GAAW,CAAS;EAClC,MAAK;EACL,UAAU;EACV,cAAY;EACH;EACT,YAAY,MAA2B;GACrC,CAAI,EAAE,QAAQ,WAAW,EAAE,QAAQ,SACjC,EAAE,eAAe,GACjB,EAAQ;EAEZ;EACA,GAAI;YAEH;CACG,CAAA,IAKR,kBAAC,GAAD;EAAW;EAAc;EAAe;EAAM,WAAW,EAAG,GAAW,CAAS;EAAG,GAAI;YACpF;CACG,CAAA;AAEV,CACF;AAEA,EAAS,cAAc"}
|
|
1
|
+
{"version":3,"file":"stat-card.js","names":[],"sources":["../../src/ui/stat-card.tsx"],"sourcesContent":["'use client'\n\nimport { IconMinus, IconTrendingDown, IconTrendingUp } from '@tabler/icons-react'\nimport { motion } from 'framer-motion'\nimport * as React from 'react'\n\nimport { Card, CardContent, CardFooter, type CardSize } from './card'\nimport { Icon } from './icon'\nimport { IconProvider } from './icon-context'\nimport type { IconInput } from './lib/icon-input'\nimport { useLink } from './lib/link-context'\nimport { springs, tweens } from './lib/motion'\nimport { normalizeIcon } from './lib/normalize-icon'\nimport { cn } from './lib/utils'\nimport { Progress, type ProgressColor } from './progress'\nimport { Skeleton } from './skeleton'\nimport { type FlashPreset, type FlashSpec, type FlashSpeed,StatFlash } from './stat-flash'\n\n/**\n * Props for StatCard — a dashboard metric tile displaying a label, a large numeric value,\n * an optional trend delta (with directional arrow icon), and an optional header icon.\n *\n * **Surface:** delegated to `Card` via `variant` — `default` (tonal hairline, no shadow) |\n * `elevated` (shadow) | `outline` (strong border, no shadow) | `flat`. StatCard composes `<Card>`, so the surface,\n * padding (gap model), and elevation all live in one place.\n *\n * **Accent:** `accentStyle=\"none\"` (default) is neutral; `\"icon\"` wraps `icon` in an accent chip\n * (`iconFill=\"soft\" | \"solid\"`); `\"tint\"` applies a subtle accent surface wash + accent value.\n * (Replaces the removed `accent` left-rail prop — see CHANGELOG migration.)\n *\n * **Delta direction:** `'up'` renders a green trending-up arrow, `'down'` renders a red\n * trending-down arrow, `'neutral'` renders a grey dash. The `delta.value` is a formatted\n * string (e.g. `\"+8%\"` or `\"−120\"`).\n *\n * **Loading state:** Pass `loading={true}` to render a pulse-skeleton placeholder instead of data.\n *\n * @example\n * // Revenue metric with a positive trend:\n * <StatCard\n * label=\"Monthly Revenue\"\n * value=\"$48,200\"\n * delta={{ value: \"+12%\", direction: \"up\" }}\n * icon={<IconCurrencyDollar />}\n * />\n *\n * @example\n * // Open ticket count with a downward trend (good for support queues):\n * <StatCard\n * label=\"Open Tickets\"\n * value={142}\n * delta={{ value: \"−18\", direction: \"down\" }}\n * />\n *\n * @example\n * // Loading skeleton while data is fetching:\n * <StatCard label=\"Users\" value={0} loading={isFetching} />\n *\n * @example\n * // Simple metric with no trend (stable, neutral):\n * <StatCard\n * label=\"Storage Used\"\n * value=\"4.2 GB\"\n * delta={{ value: \"No change\", direction: \"neutral\" }}\n * />\n * // These are just a few ways — feel free to combine props creatively!\n */\nexport interface StatCardProps extends Omit<React.HTMLAttributes<HTMLDivElement>, 'color'> {\n /** Heading text for the metric. `title` is an alias for `label`. */\n label?: string\n /** Alias for `label` — use whichever feels natural. */\n title?: string\n value: string | number\n /** Prefix before value, e.g. \"$\" */\n prefix?: string\n /** Suffix after value, e.g. \"users\" */\n suffix?: string\n delta?: {\n value: string\n direction: 'up' | 'down' | 'neutral'\n }\n /** Where the delta sits relative to the value: `block` (below — default) or `inline` (on the value's baseline, to its right). @default 'block' */\n deltaPlacement?: 'block' | 'inline'\n icon?: IconInput\n loading?: boolean\n /** Comparison period label shown after delta, e.g. \"vs last month\" */\n comparisonLabel?: string\n /** Secondary metric below main value, e.g. \"of $50,000 target\" */\n secondaryLabel?: string\n /** Progress toward a target (0-100). Renders a thin progress bar below the value. */\n progress?: number\n /** Surface variant, delegated to Card: `default` (tonal hairline, no shadow) | `elevated` (shadow) | `outline` (strong border, no shadow) | `flat`. @default 'default' */\n variant?: 'default' | 'elevated' | 'outline' | 'flat'\n /**\n * Tile density, delegated to Card's size axis. `sm` tightens padding to 16px and steps\n * the value down to `text-heading-md` — use it for dense KPI rows and narrow stat grids\n * (e.g. stat-row's 140px tiles). @default 'md'\n */\n size?: CardSize\n /** How the brand accent reads: `none` (neutral — default), `icon` (accent chip around `icon`), or `tint` (subtle accent surface wash + accent value). */\n accentStyle?: 'none' | 'icon' | 'tint'\n /** Animate value/label in on mount (subtle settle; content stays visible either way, never opacity-gated). @default false */\n reveal?: boolean\n /** When `accentStyle=\"icon\"`: `soft` (tinted chip) or `solid` (filled accent chip). @default 'soft' */\n iconFill?: 'soft' | 'solid'\n /**\n * Opt-in entrance flash: the icon chip mounts showing a transient state (e.g. a green up-arrow)\n * then settles to `icon`. A preset (`'up' | 'down' | 'goal' | 'record' | 'alert' | 'live'`) or an\n * explicit `{ tone, icon }`. Requires `icon`; implies the icon chip. Off by default.\n */\n flash?: FlashPreset | FlashSpec\n /** Speed of the entrance flash: `fast` | `normal` | `slow`. @default 'normal' */\n flashSpeed?: FlashSpeed\n /** Sparkline data points. Renders a mini SVG line chart in the card. */\n sparkline?: number[]\n /** Make the card clickable with hover state */\n onClick?: () => void\n /** Href — makes the card a link (uses the LinkContext Link component) */\n href?: string\n /** Footer content rendered below the card body, e.g. \"View details →\" */\n footer?: React.ReactNode\n}\n\nfunction buildSparklinePath(raw: number[], width: number, height: number): string {\n const data = raw.filter((v) => Number.isFinite(v))\n if (data.length < 2) return ''\n const min = Math.min(...data)\n const max = Math.max(...data)\n const range = max - min || 1\n const stepX = width / (data.length - 1)\n return data\n .map((v, i) => {\n const x = i * stepX\n const y = height - ((v - min) / range) * height\n return `${i === 0 ? 'M' : 'L'}${x.toFixed(1)},${y.toFixed(1)}`\n })\n .join(' ')\n}\n\nfunction Sparkline({\n data,\n colorClass,\n}: {\n data: number[]\n colorClass: string\n}) {\n const id = React.useId()\n const pathRef = React.useRef<SVGPathElement>(null)\n const [pathLength, setPathLength] = React.useState(0)\n const svgWidth = 80\n const svgHeight = 32\n const path = buildSparklinePath(data, svgWidth, svgHeight)\n\n React.useEffect(() => {\n if (pathRef.current) {\n setPathLength(pathRef.current.getTotalLength())\n }\n }, [path])\n\n if (!path) return null\n\n // Build area fill path (close to bottom-right then bottom-left)\n const areaPath = `${path} L${svgWidth.toFixed(1)},${svgHeight.toFixed(1)} L0,${svgHeight.toFixed(1)} Z`\n\n return (\n <div className={cn('w-20 h-8', colorClass)} aria-hidden=\"true\">\n <svg\n viewBox={`0 0 ${svgWidth} ${svgHeight}`}\n className=\"w-full h-full overflow-visible\"\n preserveAspectRatio=\"none\"\n >\n <defs>\n <linearGradient id={`sparkline-fill-${id}`} x1=\"0\" y1=\"0\" x2=\"0\" y2=\"1\">\n <stop offset=\"0%\" stopColor=\"currentColor\" stopOpacity=\"0.1\" />\n <stop offset=\"100%\" stopColor=\"currentColor\" stopOpacity=\"0\" />\n </linearGradient>\n </defs>\n <path d={areaPath} fill={`url(#sparkline-fill-${id})`} />\n <path\n ref={pathRef}\n d={path}\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth=\"1.5\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n style={{\n opacity: pathLength === 0 ? 0 : 1,\n strokeDasharray: pathLength,\n strokeDashoffset: pathLength,\n animation: pathLength > 0 ? `sparkline-draw-${id.replace(/:/g, '')} 1s ease-out forwards` : 'none',\n }}\n />\n </svg>\n {pathLength > 0 && (\n <style>{`@keyframes sparkline-draw-${id.replace(/:/g, '')} { to { stroke-dashoffset: 0; } }`}</style>\n )}\n </div>\n )\n}\n\nfunction ProgressBar({ progress, label }: { progress: number; label: string }) {\n const clamped = Math.max(0, Math.min(100, progress))\n // StatCard's own thresholds (90 = success, 70 = warning) — deliberately distinct\n // from Progress's autoColor (85/60), so we pass an explicit color.\n const barColor: ProgressColor =\n clamped >= 90 ? 'success' : clamped >= 70 ? 'warning' : 'accent'\n\n return (\n <Progress.Root value={clamped} size=\"sm\">\n <Progress.Track aria-label={`${label} progress`}>\n <Progress.Indicator color={barColor} />\n </Progress.Track>\n </Progress.Root>\n )\n}\n\nconst StatCard = React.forwardRef<HTMLDivElement, StatCardProps>(\n (\n {\n className,\n label,\n title,\n value,\n prefix,\n suffix,\n delta,\n deltaPlacement = 'block',\n icon,\n loading,\n comparisonLabel,\n secondaryLabel,\n progress,\n variant = 'default',\n size = 'md',\n accentStyle = 'none',\n reveal = false,\n iconFill = 'soft',\n flash,\n flashSpeed,\n sparkline,\n onClick,\n href,\n footer,\n ...props\n },\n ref,\n ) => {\n const Link = useLink()\n const resolvedLabel = title ?? label ?? ''\n const isClickable = !!(onClick || href)\n const computedAriaLabel =\n isClickable && !props['aria-label'] ? `View ${resolvedLabel}` : props['aria-label']\n\n if (loading) {\n return (\n <Card ref={ref} variant={variant} size={size} className={className} aria-busy=\"true\" {...props}>\n <CardContent className=\"flex flex-col gap-ds-03\">\n <Skeleton className=\"h-ds-04 w-24 rounded-control-inner\" />\n <Skeleton className=\"h-ds-sm w-32\" />\n <Skeleton className=\"h-3 w-16 rounded-control-inner\" />\n </CardContent>\n </Card>\n )\n }\n\n const DeltaIcon =\n delta?.direction === 'up'\n ? IconTrendingUp\n : delta?.direction === 'down'\n ? IconTrendingDown\n : IconMinus\n\n const deltaColour =\n delta?.direction === 'up'\n ? 'text-success-11'\n : delta?.direction === 'down'\n ? 'text-error-11'\n : 'text-surface-fg-muted'\n\n const sparklineColor =\n delta?.direction === 'up'\n ? 'text-success-11'\n : delta?.direction === 'down'\n ? 'text-error-11'\n : 'text-accent-11'\n\n const deltaNode = delta ? (\n <motion.div\n className={cn(\n // Rhythm comes from CardContent's flex gap — no placement margins.\n 'flex items-center gap-ds-02 text-body-sm font-medium',\n deltaColour,\n )}\n initial={{ opacity: 0, y: 8 }}\n animate={{ opacity: 1, y: 0 }}\n transition={{ ...springs.smooth, delay: 0.2 }}\n >\n <motion.span\n className=\"inline-flex\"\n initial={{ opacity: 0.5, scale: 1.4 }}\n animate={{ opacity: 1, scale: 1 }}\n transition={springs.smooth}\n >\n <Icon icon={DeltaIcon} size=\"sm\" />\n </motion.span>\n <span>{delta.value}</span>\n {comparisonLabel && (\n <span className=\"text-surface-fg-subtle font-normal\">{comparisonLabel}</span>\n )}\n </motion.div>\n ) : null\n\n // reveal=false → static (content-safe default). reveal=true → subtle settle; opacity NEVER gated.\n const revealProps = reveal\n ? { initial: { y: 8 }, animate: { y: 0 }, transition: springs.smooth }\n : { initial: false as const }\n\n const cardContent = (\n <>\n <div className=\"flex items-center justify-between\">\n <motion.p\n className=\"text-body-md font-medium text-surface-fg-muted\"\n {...revealProps}\n >\n {resolvedLabel}\n </motion.p>\n <div className=\"flex items-center gap-ds-03\">\n {sparkline && sparkline.length >= 2 && (\n <Sparkline data={sparkline} colorClass={sparklineColor} />\n )}\n {icon && flash ? (\n <StatFlash icon={icon} flash={flash} fill={iconFill} speed={flashSpeed} />\n ) : icon ? (\n accentStyle === 'icon' ? (\n <motion.span\n className={cn(\n 'inline-flex items-center justify-center rounded-control p-ds-02',\n iconFill === 'solid'\n ? 'bg-accent-9 text-accent-fg'\n : 'bg-accent-3 text-accent-11',\n )}\n {...revealProps}\n aria-hidden=\"true\"\n >\n <IconProvider size=\"md\">{normalizeIcon(icon, 'md')}</IconProvider>\n </motion.span>\n ) : (\n <motion.span\n className=\"text-surface-fg-muted\"\n {...revealProps}\n aria-hidden=\"true\"\n >\n <IconProvider size=\"lg\">{normalizeIcon(icon, 'lg')}</IconProvider>\n </motion.span>\n )\n ) : null}\n </div>\n </div>\n <div className=\"flex flex-col gap-ds-01\">\n <div className={cn(deltaPlacement === 'inline' && delta && 'flex items-baseline gap-ds-03')}>\n <div className=\"overflow-hidden\">\n <motion.p\n className={cn(\n 'inline-block font-semibold',\n size === 'sm' ? 'text-heading-md' : 'text-heading-lg',\n accentStyle === 'tint' ? 'text-accent-11' : 'text-surface-fg',\n )}\n {...revealProps}\n >\n {prefix && (\n <span className={cn('text-surface-fg-muted', size === 'sm' ? 'text-body-md' : 'text-heading-xs')}>{prefix}</span>\n )}\n <span className=\"tabular-nums\">{value}</span>\n {suffix && (\n <span className={cn('text-surface-fg-muted', size === 'sm' ? 'text-body-md' : 'text-heading-xs')}>{suffix}</span>\n )}\n </motion.p>\n </div>\n {deltaPlacement === 'inline' && deltaNode}\n </div>\n {secondaryLabel && (\n <motion.p\n className=\"text-body-sm text-surface-fg-subtle\"\n {...revealProps}\n >\n {secondaryLabel}\n </motion.p>\n )}\n </div>\n {progress != null && (\n <motion.div\n {...revealProps}\n >\n <ProgressBar progress={progress} label={resolvedLabel} />\n </motion.div>\n )}\n {deltaPlacement === 'block' && deltaNode}\n </>\n )\n\n // accentStyle=\"tint\" → a subtle accent gradient wash over Card's surface.\n const tintClass =\n accentStyle === 'tint' ? 'bg-linear-to-t from-accent-2 to-surface-raised' : undefined\n // Footer sits outside CardContent behind a full-width divider (both are direct\n // children of Card, so the rule spans edge-to-edge — no inset border-t).\n const body = (\n <>\n <CardContent className=\"flex flex-col gap-ds-03\">{cardContent}</CardContent>\n {footer && (\n <>\n <div aria-hidden=\"true\" className=\"h-px w-full bg-surface-border-subtle\" />\n <CardFooter className=\"text-body-sm\">\n <motion.div\n className=\"w-full\"\n initial={false}\n animate={{ opacity: 1 }}\n transition={{ ...tweens.fade, delay: 0.25 }}\n >\n {footer}\n </motion.div>\n </CardFooter>\n </>\n )}\n </>\n )\n\n // href → wrap Card in the framework Link (Card stays the shell; hover-lift inside).\n if (href) {\n return (\n <Link\n ref={ref as React.Ref<HTMLAnchorElement>}\n href={href}\n onClick={onClick}\n className=\"block no-underline\"\n aria-label={computedAriaLabel}\n {...(props as React.AnchorHTMLAttributes<HTMLAnchorElement>)}\n >\n <Card variant={variant} size={size} interactive className={tintClass}>\n {body}\n </Card>\n </Link>\n )\n }\n\n // onClick → interactive Card with button semantics + keyboard activation.\n if (onClick) {\n return (\n <Card\n ref={ref}\n variant={variant}\n size={size}\n interactive\n className={cn(tintClass, className)}\n role=\"button\"\n tabIndex={0}\n aria-label={computedAriaLabel}\n onClick={onClick}\n onKeyDown={(e: React.KeyboardEvent) => {\n if (e.key === 'Enter' || e.key === ' ') {\n e.preventDefault()\n onClick()\n }\n }}\n {...props}\n >\n {body}\n </Card>\n )\n }\n\n return (\n <Card ref={ref} variant={variant} size={size} className={cn(tintClass, className)} {...props}>\n {body}\n </Card>\n )\n },\n)\n\nStatCard.displayName = 'StatCard'\n\nexport { StatCard }\n"],"mappings":"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"}
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
## Props
|
|
8
8
|
variant: "solid" | "soft" | "outline" | "ghost" | "link"
|
|
9
|
-
color: "accent" | "error" | "success" | "warning" | "neutral"
|
|
9
|
+
color: "accent" | "error" | "success" | "warning" | "info" | "neutral"
|
|
10
10
|
size: "xs" | "sm" | "md" | "lg" | "compact-xs" | "compact-sm" | "compact-md" | "icon" | "icon-xs" | "icon-sm" | "icon-md" | "icon-lg"
|
|
11
11
|
weight: "semibold" | "normal"
|
|
12
12
|
shape: "default" | "pill"
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
fullWidth: boolean
|
|
18
18
|
asChild: boolean
|
|
19
19
|
processing: boolean | 'ambient' | 'working' | 'urgent' (marching ants SVG border)
|
|
20
|
-
processingColor: 'accent' | 'error' | 'success' | 'warning' | 'neutral' (override animation color)
|
|
20
|
+
processingColor: 'accent' | 'error' | 'success' | 'warning' | 'info' | 'neutral' (override animation color)
|
|
21
21
|
processingDisabled: boolean (disable button during processing, default: true)
|
|
22
22
|
onClickAsync: (e: MouseEvent) => Promise<void> (auto loading->success/error->idle, auto-activates processing)
|
|
23
23
|
asyncFeedbackDuration: number (ms, default 1500)
|
|
@@ -6,32 +6,37 @@
|
|
|
6
6
|
|
|
7
7
|
## Props
|
|
8
8
|
size: "sm" | "md" | "lg"
|
|
9
|
-
variant: "
|
|
9
|
+
variant: "soft" | "solid"
|
|
10
10
|
options: SegmentedControlOption[] (REQUIRED)
|
|
11
|
-
|
|
12
|
-
|
|
11
|
+
value: string // controlled
|
|
12
|
+
defaultValue: string // uncontrolled initial
|
|
13
|
+
onValueChange: (id: string) => void
|
|
13
14
|
disabled: boolean
|
|
15
|
+
fullWidth: boolean
|
|
16
|
+
selectedId: string // @deprecated — use value
|
|
17
|
+
onSelect: (id: string) => void // @deprecated — use onValueChange
|
|
14
18
|
|
|
15
19
|
## Types
|
|
16
|
-
SegmentedControlOption = { id: string, text
|
|
20
|
+
SegmentedControlOption = { id: string, text?: React.ReactNode, icon?: IconInput, ariaLabel?: string }
|
|
17
21
|
SegmentedControlSize = 'sm' | 'md' | 'lg'
|
|
18
|
-
SegmentedControlVariant = '
|
|
22
|
+
SegmentedControlVariant = 'soft' | 'solid'
|
|
19
23
|
|
|
20
24
|
## Defaults
|
|
21
25
|
size: "md"
|
|
22
|
-
variant: "
|
|
26
|
+
variant: "soft"
|
|
27
|
+
fullWidth: false
|
|
23
28
|
|
|
24
29
|
## Example
|
|
25
30
|
```jsx
|
|
26
31
|
<SegmentedControl
|
|
27
32
|
size="md"
|
|
28
|
-
variant="
|
|
33
|
+
variant="soft"
|
|
29
34
|
options={[
|
|
30
35
|
{ id: 'list', text: 'List' },
|
|
31
36
|
{ id: 'grid', text: 'Grid' },
|
|
32
37
|
]}
|
|
33
|
-
|
|
34
|
-
|
|
38
|
+
value={viewMode}
|
|
39
|
+
onValueChange={setViewMode}
|
|
35
40
|
/>
|
|
36
41
|
```
|
|
37
42
|
|
|
@@ -39,15 +44,30 @@
|
|
|
39
44
|
- **Data-driven, not compound** — unlike Tabs/ToggleGroup, SegmentedControl takes an `options` array rather than children. This makes it easier to render from a list but harder to customize per-option styling; use Tabs if you need compound children.
|
|
40
45
|
- **When to use vs Tabs:** SegmentedControl is for mutually-exclusive VIEW-MODE toggles (List/Grid/Kanban) — short labels, no associated content panel. Tabs is for content switching where each tab has a corresponding TabsContent. SegmentedControl renders `role="radiogroup"` with `role="radio"` segments (a panel-less single-select); Tabs renders `role="tablist"`.
|
|
41
46
|
- **Option icons** auto-size based on the `size` prop — don't set explicit icon sizes.
|
|
42
|
-
-
|
|
47
|
+
- **`fullWidth`** switches segments from content-hug (default) to equal-fill: each segment takes an equal share of the container (a 2-item toggle splits 50/50, a 3-item switcher gives each a third). Use for view switchers and toolbar toggles that should fill their column; leave off for compact inline toolbars.
|
|
48
|
+
- **Visual model:** a rounded-rect track (not a full pill) — a translucent recessed groove with a single soft-shadowed sliding thumb. The track has no border/inset shadow; the thumb carries the only edge. Elevation inverts in dark so the groove stays visible.
|
|
49
|
+
- **Controlled or uncontrolled** — pass `value` + `onValueChange` to control it, or `defaultValue` (optional; falls back to the first option) to let it own state. Matches the Tabs/ToggleGroup vocabulary. `selectedId`/`onSelect` are deprecated aliases that still work.
|
|
50
|
+
- **Option labels accept `ReactNode`** — a segment can hold a count badge or custom node, not just a string. `text` is optional: omit it for an **icon-only** segment and set `ariaLabel` so the segment still has an accessible name.
|
|
51
|
+
- **Touch targets** — each segment has a 44px minimum hit area (via `touch-target`) even though the visual height stays dense.
|
|
52
|
+
- **RTL** — Arrow-key navigation tracks reading order: in a right-to-left context `ArrowLeft` moves to the next option and `ArrowRight` to the previous (detected from the nearest `dir` attribute).
|
|
43
53
|
- Built from scratch (no Radix primitive) — standard HTML buttons with `role="radio"` + `aria-checked` and roving tabindex.
|
|
44
54
|
|
|
45
55
|
## Gotchas
|
|
46
|
-
- Controlled
|
|
56
|
+
- Controlled (`value`) or uncontrolled (`defaultValue`) — `selectedId`/`onSelect` are deprecated aliases
|
|
47
57
|
- Uses data-driven API (options prop), not compound children
|
|
48
58
|
- Use Tabs (not SegmentedControl) when you need associated content panels per option
|
|
49
59
|
|
|
50
60
|
## Changes
|
|
61
|
+
### v0.52.0
|
|
62
|
+
- **Changed** Visual rebuild — rounded-rect track (was full pill), translucent recessed track with no border/inset, single ring-less soft-shadow thumb. Dark-mode elevation inverts so the groove stays visible. New tokens: `--color-segment-track`, `--color-segment-thumb`, `--shadow-segment`.
|
|
63
|
+
- **Added** `value` / `defaultValue` / `onValueChange` — canonical controlled+uncontrolled API (aligns with Tabs/ToggleGroup).
|
|
64
|
+
- **Added** `fullWidth` prop — segments split the container equally.
|
|
65
|
+
- **Added** 44px minimum touch targets (`touch-target`), keeping dense visual height.
|
|
66
|
+
- **Added** Crisp bounce-free thumb motion (reduced-motion aware) + `motion-safe` press-scale feedback.
|
|
67
|
+
- **Changed** Option `text` widened from `string` to `ReactNode`, and made optional (omit for icon-only segments).
|
|
68
|
+
- **Added** `ariaLabel` per option for icon-only segments; RTL-aware Arrow-key navigation.
|
|
69
|
+
- **Deprecated** `variant="default"` → `variant="soft"`; `selectedId` → `value`; `onSelect` → `onValueChange`. All old names still accepted as aliases; update call sites.
|
|
70
|
+
|
|
51
71
|
### v0.38.0
|
|
52
72
|
- **Removed** (BREAKING) deprecated `variant="accent"` alias. Use `variant="solid"`.
|
|
53
73
|
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
comparisonLabel: string (shown after delta, e.g. "vs last month")
|
|
18
18
|
secondaryLabel: string (below main value, e.g. "of $50,000 target")
|
|
19
19
|
progress: number (0-100, renders thin progress bar below value)
|
|
20
|
-
variant: "default" | "elevated" | "outline" | "flat" (default =
|
|
20
|
+
variant: "default" | "elevated" | "outline" | "flat" (default = tonal border-card hairline, no shadow; elevated = shadow, no border; outline = strong border, no shadow; flat = filled, no edge) — delegated to Card
|
|
21
21
|
size: "sm" | "md" (default) | "lg" — delegated to Card's size axis; sm tightens padding to 16px and steps the value down to text-ds-2xl (use for dense KPI rows / narrow stat grids)
|
|
22
22
|
accentStyle: "none" | "icon" | "tint" (none [default]; icon = accent chip around icon; tint = accent surface wash + accent value)
|
|
23
23
|
iconFill: "soft" | "solid" (chip style when accentStyle="icon"; default soft)
|
|
@@ -127,10 +127,10 @@ For runtime toggling inside React components, use `useColorMode`:
|
|
|
127
127
|
import { useColorMode } from "@devalok/shilp-sutra/hooks/use-color-mode";
|
|
128
128
|
|
|
129
129
|
export function ThemeToggle() {
|
|
130
|
-
const {
|
|
130
|
+
const { colorMode, toggleColorMode } = useColorMode();
|
|
131
131
|
return (
|
|
132
|
-
<button onClick={
|
|
133
|
-
{
|
|
132
|
+
<button onClick={toggleColorMode} aria-label="Toggle theme">
|
|
133
|
+
{colorMode === "dark" ? "☀" : "☾"}
|
|
134
134
|
</button>
|
|
135
135
|
);
|
|
136
136
|
}
|
|
@@ -121,10 +121,10 @@ Wire the runtime hook from anywhere in the app (e.g., a header button):
|
|
|
121
121
|
import { useColorMode } from "@devalok/shilp-sutra/hooks/use-color-mode";
|
|
122
122
|
|
|
123
123
|
export function ThemeToggle() {
|
|
124
|
-
const {
|
|
124
|
+
const { colorMode, toggleColorMode } = useColorMode();
|
|
125
125
|
return (
|
|
126
|
-
<button onClick={
|
|
127
|
-
{
|
|
126
|
+
<button onClick={toggleColorMode} aria-label="Toggle theme">
|
|
127
|
+
{colorMode === "dark" ? "☀" : "☾"}
|
|
128
128
|
</button>
|
|
129
129
|
);
|
|
130
130
|
}
|
|
@@ -151,8 +151,8 @@ Both `<Suspense>` and `<LoadingSkeleton>` are server-safe.
|
|
|
151
151
|
import { useColorMode } from "@devalok/shilp-sutra/hooks/use-color-mode";
|
|
152
152
|
|
|
153
153
|
export default function ServerPage() {
|
|
154
|
-
const {
|
|
155
|
-
return <div>{
|
|
154
|
+
const { colorMode } = useColorMode(); // breaks
|
|
155
|
+
return <div>{colorMode}</div>;
|
|
156
156
|
}
|
|
157
157
|
```
|
|
158
158
|
|
package/llms.txt
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @devalok/shilp-sutra
|
|
2
2
|
|
|
3
|
-
> Radix UI + Tailwind 4 (CSS-first) + CVA design system for Devalok apps, v0.
|
|
3
|
+
> Radix UI + Tailwind 4 (CSS-first) + CVA design system for Devalok apps, v0.52.0.
|
|
4
4
|
> Built on the same primitives as shadcn/ui but with DIFFERENT prop APIs — never guess from shadcn knowledge; verify every prop.
|
|
5
5
|
> This file is a ROUTER: it tells you what exists and where to get details. Do not look for prop tables here — fetch them per component (MCP tool or per-component doc file below).
|
|
6
6
|
|
|
@@ -22,15 +22,15 @@ import {
|
|
|
22
22
|
- Header / actions / footer are built in as slots (`CardHeader`, `CardAction`, `CardFooter`) — don't reach for a wrapper. (`<ContentCard>` is deprecated; use Card slots.)
|
|
23
23
|
- Need just a tinted region with no card affordance? Use a `<div className="bg-surface-raised">` (rare; usually Card is right).
|
|
24
24
|
|
|
25
|
-
Card renders on `surface-raised`
|
|
25
|
+
Card renders on `surface-raised`. The `default` variant is tonal — a surface-tone shift plus a whisper hairline (`border-card`), no shadow. **Never** override its background or border.
|
|
26
26
|
|
|
27
27
|
## Variants
|
|
28
28
|
|
|
29
29
|
| Variant | Use |
|
|
30
30
|
|---|---|
|
|
31
|
-
| `default` (default) | `surface-raised` + `
|
|
32
|
-
| `elevated` |
|
|
33
|
-
| `outline` | `surface-raised` + border-only (no shadow). Dense lists where
|
|
31
|
+
| `default` (default) | `surface-raised` + tonal `border-card` hairline, no shadow. Standard card — depth from tone, not a drop shadow. |
|
|
32
|
+
| `elevated` | `shadow-raised-hover`, no border. Use when a card must visibly pop (hero, dragged tile, spotlight panel). |
|
|
33
|
+
| `outline` | `surface-raised` + strong border-only (no shadow). Dense lists where stacked shadowed cards would feel too lifted. |
|
|
34
34
|
| `flat` | `surface-raised` + no shadow, no border. For cards inside an already-elevated container. |
|
|
35
35
|
|
|
36
36
|
## Colors
|
|
@@ -103,6 +103,26 @@ Available as `accent-1` … `accent-12`. Apply via `bg-accent-N`, `text-accent-N
|
|
|
103
103
|
|
|
104
104
|
The same 12-step pattern repeats for `error-*`, `success-*`, `warning-*`, `info-*` (subset: 2, 3, 4, 5, 6, 7, 9, 10, 11).
|
|
105
105
|
|
|
106
|
+
## Component `color` prop — support matrix
|
|
107
|
+
|
|
108
|
+
The `color` prop accepts a different set per component, because each serves a different job. **The shared intent set — safe to pass to all three from one token — is `accent` · `error` · `success` · `warning` · `info` · `neutral`.**
|
|
109
|
+
|
|
110
|
+
| color | Button | Card | Badge |
|
|
111
|
+
|---|:---:|:---:|:---:|
|
|
112
|
+
| `accent` | ✅ | ✅ | ✅ |
|
|
113
|
+
| `error` | ✅ | ✅ | ✅ |
|
|
114
|
+
| `success` | ✅ | ✅ | ✅ |
|
|
115
|
+
| `warning` | ✅ | ✅ | ✅ |
|
|
116
|
+
| `info` | ✅ | ✅ | ✅ |
|
|
117
|
+
| `neutral` | ✅ | ✅ | ✅ |
|
|
118
|
+
| `default` | — | ✅ | ✅ |
|
|
119
|
+
| `teal` `amber` `slate` `indigo` `cyan` `orange` `emerald` | — | — | ✅ |
|
|
120
|
+
| `custom` (`--badge-color`) | — | — | ✅ |
|
|
121
|
+
|
|
122
|
+
- **Button / Card** carry the six **semantic intents** — they communicate *state or emphasis* (a destructive action, a warning panel). That set is aligned across both.
|
|
123
|
+
- **Badge** adds a **category palette** (`teal`, `amber`, `indigo`, …) plus `custom`, because badges label/categorize many peer items where hue is a taxonomy, not a status. Those category hues are intentionally **not** on Button/Card — a "teal primary button" would read as an intent that doesn't exist.
|
|
124
|
+
- To tint a Card + Badge + Button set from one variable, constrain the token to the shared intent set above.
|
|
125
|
+
|
|
106
126
|
## Theming — never hardcode
|
|
107
127
|
|
|
108
128
|
Consumers swap the accent by overriding `--color-accent-1` through `--color-accent-12` in a `:root { }` block placed **after** the kit's CSS import. Dark mode is derived algorithmically — no separate dark overrides needed.
|
|
@@ -18,17 +18,14 @@ Dark tokens are **algorithmically derived** from the OKLCH primitives, not hand-
|
|
|
18
18
|
import { useColorMode } from '@devalok/shilp-sutra/hooks/use-color-mode'
|
|
19
19
|
|
|
20
20
|
function ThemeToggle() {
|
|
21
|
-
const {
|
|
22
|
-
//
|
|
23
|
-
//
|
|
21
|
+
const { colorMode, setColorMode, toggleColorMode } = useColorMode()
|
|
22
|
+
// colorMode: 'light' | 'dark' | 'system'
|
|
23
|
+
// setColorMode(next): set an explicit mode · toggleColorMode(): flip light↔dark
|
|
24
24
|
|
|
25
25
|
return (
|
|
26
|
-
<Button
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
>
|
|
30
|
-
<Icon icon={resolvedMode === 'dark' ? IconSun : IconMoon} />
|
|
31
|
-
{resolvedMode === 'dark' ? 'Light' : 'Dark'}
|
|
26
|
+
<Button variant="soft" onClick={toggleColorMode}>
|
|
27
|
+
<Icon icon={colorMode === 'dark' ? IconSun : IconMoon} />
|
|
28
|
+
{colorMode === 'dark' ? 'Light' : 'Dark'}
|
|
32
29
|
</Button>
|
|
33
30
|
)
|
|
34
31
|
}
|