@devalok/shilp-sutra 0.51.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.
Files changed (47) hide show
  1. package/AGENTS.md +1 -1
  2. package/MIGRATION.md +6 -0
  3. package/dist/tokens/semantic.css +20 -1
  4. package/dist/ui/button-group.js +2 -0
  5. package/dist/ui/button-group.js.map +1 -1
  6. package/dist/ui/button-processing.d.ts.map +1 -1
  7. package/dist/ui/button-processing.js +1 -0
  8. package/dist/ui/button-processing.js.map +1 -1
  9. package/dist/ui/button.d.ts +3 -3
  10. package/dist/ui/button.d.ts.map +1 -1
  11. package/dist/ui/button.js +26 -0
  12. package/dist/ui/button.js.map +1 -1
  13. package/dist/ui/color-input.d.ts.map +1 -1
  14. package/dist/ui/color-input.js +82 -82
  15. package/dist/ui/color-input.js.map +1 -1
  16. package/dist/ui/icon.d.ts +2 -0
  17. package/dist/ui/icon.d.ts.map +1 -1
  18. package/dist/ui/icon.js +31 -26
  19. package/dist/ui/icon.js.map +1 -1
  20. package/dist/ui/search-input.d.ts.map +1 -1
  21. package/dist/ui/search-input.js +1 -0
  22. package/dist/ui/search-input.js.map +1 -1
  23. package/dist/ui/segmented-control.d.ts +28 -6
  24. package/dist/ui/segmented-control.d.ts.map +1 -1
  25. package/dist/ui/segmented-control.js +61 -43
  26. package/dist/ui/segmented-control.js.map +1 -1
  27. package/dist/ui/split-button.d.ts.map +1 -1
  28. package/dist/ui/split-button.js +7 -0
  29. package/dist/ui/split-button.js.map +1 -1
  30. package/docs/components/ui/button.md +2 -2
  31. package/docs/components/ui/segmented-control.md +31 -11
  32. package/docs/recipes/install-remix.md +3 -3
  33. package/docs/recipes/install-vite.md +3 -3
  34. package/docs/recipes/server-components.md +2 -2
  35. package/llms.txt +1 -1
  36. package/make-kit/foundations/color.md +20 -0
  37. package/make-kit/foundations/dark-mode.md +6 -9
  38. package/make-kit/foundations/icons.md +3 -3
  39. package/make-kit/setup.md +4 -4
  40. package/mcp-manifest.json +61 -10
  41. package/package.json +1 -1
  42. package/scripts/welcome.mjs +59 -1
  43. package/skill/SKILL.md +1 -1
  44. package/skill/references/components.md +1 -1
  45. package/skill/references/server-components.md +2 -2
  46. package/skill/references/setup-remix.md +3 -3
  47. package/skill/references/setup-vite.md +3 -3
@@ -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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"}
@@ -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: "default" | "solid"
9
+ variant: "soft" | "solid"
10
10
  options: SegmentedControlOption[] (REQUIRED)
11
- selectedId: string (REQUIRED)
12
- onSelect: (id: string) => void (REQUIRED)
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: string, icon?: ComponentType<{ className?: string }> }
20
+ SegmentedControlOption = { id: string, text?: React.ReactNode, icon?: IconInput, ariaLabel?: string }
17
21
  SegmentedControlSize = 'sm' | 'md' | 'lg'
18
- SegmentedControlVariant = 'default' | 'solid'
22
+ SegmentedControlVariant = 'soft' | 'solid'
19
23
 
20
24
  ## Defaults
21
25
  size: "md"
22
- variant: "default"
26
+ variant: "soft"
27
+ fullWidth: false
23
28
 
24
29
  ## Example
25
30
  ```jsx
26
31
  <SegmentedControl
27
32
  size="md"
28
- variant="default"
33
+ variant="soft"
29
34
  options={[
30
35
  { id: 'list', text: 'List' },
31
36
  { id: 'grid', text: 'Grid' },
32
37
  ]}
33
- selectedId={viewMode}
34
- onSelect={setViewMode}
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
- - Fully controlled there's no `defaultSelectedId`. Manage state in parent.
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 only — selectedId + onSelect are required
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
 
@@ -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 { mode, toggle } = useColorMode();
130
+ const { colorMode, toggleColorMode } = useColorMode();
131
131
  return (
132
- <button onClick={toggle} aria-label="Toggle theme">
133
- {mode === "dark" ? "☀" : "☾"}
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 { mode, toggle } = useColorMode();
124
+ const { colorMode, toggleColorMode } = useColorMode();
125
125
  return (
126
- <button onClick={toggle} aria-label="Toggle theme">
127
- {mode === "dark" ? "☀" : "☾"}
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 { mode } = useColorMode(); // breaks
155
- return <div>{mode}</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.51.0.
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
 
@@ -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 { mode, resolvedMode, setMode } = useColorMode()
22
- // mode: 'light' | 'dark' | 'system'
23
- // resolvedMode: 'light' | 'dark' (after resolving 'system')
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
- variant="soft"
28
- onClick={() => setMode(resolvedMode === 'dark' ? 'light' : 'dark')}
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
  }
@@ -17,7 +17,7 @@ import { Icon } from '@devalok/shilp-sutra/ui/icon'
17
17
  import { IconHome, IconUser, IconSettings } from '@tabler/icons-react'
18
18
 
19
19
  <Icon icon={IconHome} />
20
- <Icon icon={IconUser} size={20} />
20
+ <Icon icon={IconUser} size="lg" />
21
21
  <Icon icon={IconSettings} className="text-fg-muted" />
22
22
  ```
23
23
 
@@ -55,7 +55,7 @@ Don't add `className="h-4 w-4"` on every Icon. Wrap a subtree:
55
55
  ```tsx
56
56
  import { IconProvider } from '@devalok/shilp-sutra/ui/icon-context'
57
57
 
58
- <IconProvider size={16}>
58
+ <IconProvider size="sm">
59
59
  <NavSection>
60
60
  <Icon icon={IconHome} />
61
61
  <Icon icon={IconUser} />
@@ -64,7 +64,7 @@ import { IconProvider } from '@devalok/shilp-sutra/ui/icon-context'
64
64
  </IconProvider>
65
65
  ```
66
66
 
67
- Override per-icon with `<Icon icon={...} size={24} />`.
67
+ Override per-icon with `<Icon icon={...} size="xl" />`.
68
68
 
69
69
  Default sizes by component context:
70
70
 
package/make-kit/setup.md CHANGED
@@ -53,7 +53,7 @@ import { IconProvider } from '@devalok/shilp-sutra/ui/icon-context'
53
53
  export default function App({ children }) {
54
54
  return (
55
55
  <MotionProvider reducedMotion="user">
56
- <IconProvider size={16}>
56
+ <IconProvider size="sm">
57
57
  {children}
58
58
  <Toaster />
59
59
  </IconProvider>
@@ -63,7 +63,7 @@ export default function App({ children }) {
63
63
  ```
64
64
 
65
65
  - `MotionProvider` — required. `reducedMotion="user"` respects OS preference. Without this provider, motion primitives still work but reduced-motion is ignored.
66
- - `IconProvider` — optional but recommended. Sets default icon size for all `<Icon>` children. Override per-call with `<Icon size={20} />`.
66
+ - `IconProvider` — optional but recommended. Sets default icon size for all `<Icon>` children. Size is a tier (`"xs" | "sm" | "md" | "lg" | "xl" | "2xl"`), not a pixel number. Override per-call with `<Icon size="lg" />`.
67
67
  - `Toaster` — only needed if the app calls `toast(...)`. Mount exactly once.
68
68
 
69
69
  ## Dark mode toggle
@@ -74,9 +74,9 @@ Add the `.dark` class on `<html>` or `<body>` to flip the entire token system to
74
74
  import { useColorMode } from '@devalok/shilp-sutra/hooks/use-color-mode'
75
75
 
76
76
  function ThemeToggle() {
77
- const { mode, setMode } = useColorMode() // 'light' | 'dark' | 'system'
77
+ const { colorMode, setColorMode } = useColorMode() // 'light' | 'dark' | 'system'
78
78
  return (
79
- <Button onClick={() => setMode(mode === 'dark' ? 'light' : 'dark')}>
79
+ <Button onClick={() => setColorMode(colorMode === 'dark' ? 'light' : 'dark')}>
80
80
  Toggle theme
81
81
  </Button>
82
82
  )
package/mcp-manifest.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "$schema": "./mcp-manifest.schema.json",
3
3
  "manifestVersion": "1.2.0",
4
4
  "package": "@devalok/shilp-sutra",
5
- "packageVersion": "0.51.0",
5
+ "packageVersion": "0.52.0",
6
6
  "components": {
7
7
  "accordion": {
8
8
  "displayName": "Accordion",
@@ -1192,6 +1192,7 @@
1192
1192
  "error",
1193
1193
  "success",
1194
1194
  "warning",
1195
+ "info",
1195
1196
  "neutral"
1196
1197
  ]
1197
1198
  },
@@ -1302,6 +1303,7 @@
1302
1303
  "error",
1303
1304
  "success",
1304
1305
  "warning",
1306
+ "info",
1305
1307
  "neutral"
1306
1308
  ]
1307
1309
  },
@@ -5250,7 +5252,7 @@
5250
5252
  "type": {
5251
5253
  "name": "enum",
5252
5254
  "value": [
5253
- "default",
5255
+ "soft",
5254
5256
  "solid"
5255
5257
  ]
5256
5258
  },
@@ -5263,16 +5265,24 @@
5263
5265
  },
5264
5266
  "required": true
5265
5267
  },
5266
- "selectedId": {
5268
+ "value": {
5267
5269
  "type": {
5268
- "name": "string"
5270
+ "name": "object",
5271
+ "raw": "string // controlled"
5269
5272
  },
5270
- "required": true
5273
+ "required": false
5271
5274
  },
5272
- "onSelect": {
5275
+ "defaultValue": {
5276
+ "type": {
5277
+ "name": "object",
5278
+ "raw": "string // uncontrolled initial"
5279
+ },
5280
+ "required": false
5281
+ },
5282
+ "onValueChange": {
5273
5283
  "type": {
5274
5284
  "name": "function",
5275
- "raw": "(id: string) => void (REQUIRED)"
5285
+ "raw": "(id: string) => void"
5276
5286
  },
5277
5287
  "required": false
5278
5288
  },
@@ -5281,6 +5291,26 @@
5281
5291
  "name": "boolean"
5282
5292
  },
5283
5293
  "required": false
5294
+ },
5295
+ "fullWidth": {
5296
+ "type": {
5297
+ "name": "boolean"
5298
+ },
5299
+ "required": false
5300
+ },
5301
+ "selectedId": {
5302
+ "type": {
5303
+ "name": "object",
5304
+ "raw": "string // @deprecated — use value"
5305
+ },
5306
+ "required": false
5307
+ },
5308
+ "onSelect": {
5309
+ "type": {
5310
+ "name": "function",
5311
+ "raw": "(id: string) => void // @deprecated — use onValueChange"
5312
+ },
5313
+ "required": false
5284
5314
  }
5285
5315
  },
5286
5316
  "composition": {
@@ -5288,20 +5318,29 @@
5288
5318
  "**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.",
5289
5319
  "**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\"`.",
5290
5320
  "**Option icons** auto-size based on the `size` prop — don't set explicit icon sizes.",
5291
- "Fully controlled there's no `defaultSelectedId`. Manage state in parent.",
5321
+ "**`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.",
5322
+ "**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.",
5323
+ "**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.",
5324
+ "**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.",
5325
+ "**Touch targets** — each segment has a 44px minimum hit area (via `touch-target`) even though the visual height stays dense.",
5326
+ "**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).",
5292
5327
  "Built from scratch (no Radix primitive) — standard HTML buttons with `role=\"radio\"` + `aria-checked` and roving tabindex."
5293
5328
  ]
5294
5329
  },
5295
5330
  "docPath": "docs/components/ui/segmented-control.md",
5296
5331
  "examples": [
5297
- "<SegmentedControl\n size=\"md\"\n variant=\"default\"\n options={[\n { id: 'list', text: 'List' },\n { id: 'grid', text: 'Grid' },\n ]}\n selectedId={viewMode}\n onSelect={setViewMode}\n/>"
5332
+ "<SegmentedControl\n size=\"md\"\n variant=\"soft\"\n options={[\n { id: 'list', text: 'List' },\n { id: 'grid', text: 'Grid' },\n ]}\n value={viewMode}\n onValueChange={setViewMode}\n/>"
5298
5333
  ],
5299
5334
  "gotchas": [
5300
- "Controlled only — selectedId + onSelect are required",
5335
+ "Controlled (`value`) or uncontrolled (`defaultValue`) `selectedId`/`onSelect` are deprecated aliases",
5301
5336
  "Uses data-driven API (options prop), not compound children",
5302
5337
  "Use Tabs (not SegmentedControl) when you need associated content panels per option"
5303
5338
  ],
5304
5339
  "changes": [
5340
+ {
5341
+ "version": "0.52.0",
5342
+ "summary": "**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`."
5343
+ },
5305
5344
  {
5306
5345
  "version": "0.38.0",
5307
5346
  "summary": "**Removed** (BREAKING) deprecated `variant=\"accent\"` alias. Use `variant=\"solid\"`."
@@ -13279,6 +13318,14 @@
13279
13318
  "name": "--color-backdrop",
13280
13319
  "value": "Canvas"
13281
13320
  },
13321
+ {
13322
+ "name": "--color-segment-track",
13323
+ "value": "color-mix(in oklch, oklch(1 0 0) 7%, transparent)"
13324
+ },
13325
+ {
13326
+ "name": "--color-segment-thumb",
13327
+ "value": "var(--neutral-3)"
13328
+ },
13282
13329
  {
13283
13330
  "name": "--color-overlay",
13284
13331
  "value": "Canvas"
@@ -13933,6 +13980,10 @@
13933
13980
  "name": "--shadow-raised-hover",
13934
13981
  "value": "var(--shadow-sm-internal)"
13935
13982
  },
13983
+ {
13984
+ "name": "--shadow-segment",
13985
+ "value": "0 1px 2px -0.5px oklch(var(--shadow-color) / calc(0.10 * var(--shadow-strength))), 0 3px 8px -2px oklch(var(--shadow-color) / calc(0.09 * var(--shadow-strength)))"
13986
+ },
13936
13987
  {
13937
13988
  "name": "--shadow-floating",
13938
13989
  "value": "var(--shadow-md-internal)"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@devalok/shilp-sutra",
3
- "version": "0.51.0",
3
+ "version": "0.52.0",
4
4
  "description": "Devalok Design System — accessible React components, OKLCH design tokens, and Tailwind 4 CSS-first setup. Ships with AI-agent setup recipes.",
5
5
  "license": "MIT",
6
6
  "author": "Devalok Design & Strategy Studios <shilp-sutra@devalok.in>",
@@ -183,7 +183,7 @@ function buildFullBanner(version, prevVersion) {
183
183
  lines.push(row(` ${colour('https://shilp-sutra.devalok.in/themer', DIM)}`))
184
184
  lines.push(colour(EMPTY, PINK_DIM))
185
185
  lines.push(row(` ${colour('▸', PINK)} Wire your AI agent (Claude Code / Cursor / Codex):`))
186
- lines.push(row(` ${colour('connect the live docs MCP https://shilp-sutra.devalok.in/mcp', DIM)}`))
186
+ lines.push(row(` ${colour('live docs MCP added to .mcp.json — approve it to enable', DIM)}`))
187
187
  lines.push(row(` ${colour('(version-exact setup + peer preflight; beats guessing)', DIM)}`))
188
188
  lines.push(row(` ${colour('or copy the skill: cp -r node_modules/@devalok/shilp-sutra/skill \\', DIM)}`))
189
189
  lines.push(row(` ${colour('~/.claude/skills/shilp-sutra', DIM)}`))
@@ -215,6 +215,59 @@ function buildCompactBanner(version, prevVersion) {
215
215
  ].join('\n')
216
216
  }
217
217
 
218
+ // ── MCP auto-discovery ───────────────────────────────────────────────────────
219
+ // Write a project-scoped `.mcp.json` pointing at the hosted docs MCP so an AI
220
+ // coding agent DISCOVERS it right after install. This runs even when stdout is
221
+ // piped (unlike the banner) — a config file is not console noise, and the agent
222
+ // that just ran `install` is exactly who should find it. It is never silent-
223
+ // forced: Claude Code (and peers) still PROMPT the user to approve a project
224
+ // MCP server before enabling it. Safety: additive merge (never clobbers other
225
+ // servers or an existing shilp-sutra entry), skips CI and dev installs, honours
226
+ // opt-out, and a write-once sentinel so a user who deletes it is not re-nagged.
227
+ const SEP = process.platform === 'win32' ? '\\' : '/'
228
+ const MCP_URL = 'https://shilp-sutra.devalok.in/mcp'
229
+
230
+ function tryWriteMcpConfig() {
231
+ try {
232
+ if (process.env.SHILP_SUTRA_NO_WELCOME === '1' || process.env.SHILP_SUTRA_NO_WELCOME === 'true') return
233
+ if (process.env.SHILP_SUTRA_NO_MCP === '1' || process.env.SHILP_SUTRA_NO_MCP === 'true') return
234
+ if (process.env.CI) return // writing agent config into a CI checkout is pointless/unwanted
235
+
236
+ const initCwd = process.env.INIT_CWD
237
+ const cwd = process.cwd()
238
+ const isInsideNodeModules = cwd.includes(`${SEP}node_modules${SEP}`) || cwd.includes('/node_modules/')
239
+ if (!isInsideNodeModules) return // dev install inside the DS repo itself
240
+ if (!initCwd || initCwd === cwd) return // no consumer root → unusual context
241
+
242
+ // Write-once-ever sentinel (survives re-installs; respects user deletion of .mcp.json)
243
+ const parts = PKG_DIR.split(/[/\\]/)
244
+ const nmIdx = parts.lastIndexOf('node_modules')
245
+ const sentinel = nmIdx === -1 ? null : join(parts.slice(0, nmIdx + 1).join(SEP), '.shilp-sutra-mcp-written')
246
+ if (sentinel && existsSync(sentinel)) return
247
+
248
+ const target = join(initCwd, '.mcp.json')
249
+ let config = { mcpServers: {} }
250
+ if (existsSync(target)) {
251
+ try {
252
+ config = JSON.parse(readFileSync(target, 'utf-8'))
253
+ } catch {
254
+ return // existing but unparseable — never clobber a hand-authored config
255
+ }
256
+ if (!config || typeof config !== 'object') return
257
+ if (!config.mcpServers || typeof config.mcpServers !== 'object') config.mcpServers = {}
258
+ if (config.mcpServers['shilp-sutra']) {
259
+ if (sentinel) writeFileSync(sentinel, MCP_URL + '\n')
260
+ return // already declared — leave the consumer's version untouched
261
+ }
262
+ }
263
+ config.mcpServers['shilp-sutra'] = { type: 'http', url: MCP_URL }
264
+ writeFileSync(target, JSON.stringify(config, null, 2) + '\n')
265
+ if (sentinel) writeFileSync(sentinel, MCP_URL + '\n')
266
+ } catch {
267
+ // Never break the consumer install — a failed config write is a no-op.
268
+ }
269
+ }
270
+
218
271
  // ── Main ────────────────────────────────────────────────────────────────────
219
272
  function main() {
220
273
  // --preview / --compact bypass all guards. Used by maintainers + by the
@@ -223,6 +276,11 @@ function main() {
223
276
  const preview = process.argv.includes('--preview')
224
277
  const forceCompact = process.argv.includes('--compact')
225
278
 
279
+ // MCP auto-discovery runs regardless of TTY — an agent-run (piped) install is
280
+ // precisely when the agent should discover the docs MCP. Must come before the
281
+ // TTY skip below, which only governs the human-facing banner.
282
+ if (!preview) tryWriteMcpConfig()
283
+
226
284
  if (!preview) {
227
285
  const skipReason = shouldSkip()
228
286
  if (skipReason) return // silent
package/skill/SKILL.md CHANGED
@@ -3,7 +3,7 @@ name: shilp-sutra
3
3
  description: Add, configure, and use components from Devalok's shilp-sutra design system (@devalok/shilp-sutra) — a Tailwind 4 + React 19 + CVA library with 110+ accessible components, OKLCH design tokens, framer-motion animations, and per-component RSC-safe entry points. Use this skill whenever the user mentions shilp-sutra, Devalok, the @devalok npm scope, or asks to install/add/style/theme UI in any React project that already depends on the package — even if they don't name it explicitly. Use it instead of generic shadcn/ui, MUI, or Chakra knowledge when shilp-sutra is in the project. Covers Next.js (App + Pages), Vite, Astro, Remix, TanStack Start setup playbooks; component API and variant reference; brand token customization; Server Component import patterns; and a troubleshoot tree for the thirteen most common breakages.
4
4
  license: MIT
5
5
  metadata:
6
- version: "0.51.0"
6
+ version: "0.52.0"
7
7
  author: Devalok Design & Strategy Studios
8
8
  homepage: https://github.com/devalok-design/shilp-sutra
9
9
  npm: https://www.npmjs.com/package/@devalok/shilp-sutra
@@ -2,7 +2,7 @@
2
2
 
3
3
  # @devalok/shilp-sutra
4
4
 
5
- > Radix UI + Tailwind 4 (CSS-first) + CVA design system for Devalok apps, v0.51.0.
5
+ > Radix UI + Tailwind 4 (CSS-first) + CVA design system for Devalok apps, v0.52.0.
6
6
  > Built on the same primitives as shadcn/ui but with DIFFERENT prop APIs — never guess from shadcn knowledge; verify every prop.
7
7
  > 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).
8
8
 
@@ -153,8 +153,8 @@ Both `<Suspense>` and `<LoadingSkeleton>` are server-safe.
153
153
  import { useColorMode } from "@devalok/shilp-sutra/hooks/use-color-mode";
154
154
 
155
155
  export default function ServerPage() {
156
- const { mode } = useColorMode(); // breaks
157
- return <div>{mode}</div>;
156
+ const { colorMode } = useColorMode(); // breaks
157
+ return <div>{colorMode}</div>;
158
158
  }
159
159
  ```
160
160
 
@@ -129,10 +129,10 @@ For runtime toggling inside React components, use `useColorMode`:
129
129
  import { useColorMode } from "@devalok/shilp-sutra/hooks/use-color-mode";
130
130
 
131
131
  export function ThemeToggle() {
132
- const { mode, toggle } = useColorMode();
132
+ const { colorMode, toggleColorMode } = useColorMode();
133
133
  return (
134
- <button onClick={toggle} aria-label="Toggle theme">
135
- {mode === "dark" ? "☀" : "☾"}
134
+ <button onClick={toggleColorMode} aria-label="Toggle theme">
135
+ {colorMode === "dark" ? "☀" : "☾"}
136
136
  </button>
137
137
  );
138
138
  }