@refraction-ui/astro 0.11.1 → 0.12.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 (33) hide show
  1. package/dist/astro-empty-state/EmptyState.astro +57 -0
  2. package/dist/astro-empty-state/index.ts +1 -0
  3. package/dist/astro-input/Input.astro +5 -1
  4. package/dist/astro-password-input/PasswordInput.astro +102 -0
  5. package/dist/astro-password-input/index.ts +1 -0
  6. package/dist/astro-segmented-control/SegmentedControl.astro +106 -0
  7. package/dist/astro-segmented-control/SegmentedControlItem.astro +32 -0
  8. package/dist/astro-segmented-control/index.ts +1 -0
  9. package/dist/astro-separator/Separator.astro +49 -0
  10. package/dist/astro-separator/index.ts +1 -0
  11. package/dist/astro-social-auth-button/SocialAuthButton.astro +110 -0
  12. package/dist/astro-social-auth-button/SocialAuthRow.astro +13 -0
  13. package/dist/astro-social-auth-button/index.ts +1 -0
  14. package/dist/empty-state/empty-state.styles.ts +42 -0
  15. package/dist/empty-state/empty-state.ts +27 -0
  16. package/dist/empty-state/index.ts +14 -0
  17. package/dist/index.ts +22 -17
  18. package/dist/input/index.ts +2 -0
  19. package/dist/input/input.styles.ts +4 -0
  20. package/dist/input/input.ts +39 -3
  21. package/dist/password-input/index.ts +9 -0
  22. package/dist/password-input/password-input.styles.ts +10 -0
  23. package/dist/password-input/password-input.ts +45 -0
  24. package/dist/segmented-control/index.ts +13 -0
  25. package/dist/segmented-control/segmented-control.styles.ts +42 -0
  26. package/dist/segmented-control/segmented-control.ts +80 -0
  27. package/dist/separator/index.ts +12 -0
  28. package/dist/separator/separator.styles.ts +34 -0
  29. package/dist/separator/separator.ts +53 -0
  30. package/dist/social-auth-button/index.ts +14 -0
  31. package/dist/social-auth-button/social-auth-button.styles.ts +16 -0
  32. package/dist/social-auth-button/social-auth-button.ts +51 -0
  33. package/package.json +24 -19
@@ -0,0 +1,57 @@
1
+ ---
2
+ import { cn } from '../shared/index.ts'
3
+ import {
4
+ createEmptyState,
5
+ emptyStateVariants,
6
+ emptyStateIconChipVariants,
7
+ emptyStateTitleClass,
8
+ emptyStateDescriptionClass,
9
+ emptyStateActionsClass,
10
+ type EmptyStateTone,
11
+ } from '../empty-state/index.ts'
12
+
13
+ interface Props extends astroHTML.JSX.HTMLAttributes {
14
+ /** Tone that tints the icon chip. Defaults to `neutral`. */
15
+ tone?: EmptyStateTone
16
+ /** Wrap the content in a bordered card surface. */
17
+ bordered?: boolean
18
+ }
19
+
20
+ const {
21
+ tone = 'neutral',
22
+ bordered = false,
23
+ class: className,
24
+ ...props
25
+ } = Astro.props
26
+
27
+ const api = createEmptyState({ tone })
28
+ const classes = cn(
29
+ emptyStateVariants({ bordered: bordered ? 'true' : 'false' }),
30
+ className,
31
+ )
32
+ ---
33
+
34
+ <div class={classes} {...api.dataAttributes} {...props}>
35
+ {
36
+ Astro.slots.has('icon') && (
37
+ <div aria-hidden="true" class={emptyStateIconChipVariants({ tone })}>
38
+ <slot name="icon" />
39
+ </div>
40
+ )
41
+ }
42
+ <h3 class={emptyStateTitleClass}><slot name="title" /></h3>
43
+ {
44
+ Astro.slots.has('description') && (
45
+ <p class={emptyStateDescriptionClass}>
46
+ <slot name="description" />
47
+ </p>
48
+ )
49
+ }
50
+ {
51
+ Astro.slots.has('actions') && (
52
+ <div class={emptyStateActionsClass}>
53
+ <slot name="actions" />
54
+ </div>
55
+ )
56
+ }
57
+ </div>
@@ -0,0 +1 @@
1
+ export {}
@@ -3,6 +3,7 @@ import {
3
3
  createInput,
4
4
  inputVariants,
5
5
  type InputType,
6
+ type InputValidationState,
6
7
  } from '../input/index.ts'
7
8
  import { cn } from '../shared/index.ts'
8
9
 
@@ -13,6 +14,7 @@ interface Props extends astroHTML.JSX.HTMLAttributes {
13
14
  readonly?: boolean
14
15
  required?: boolean
15
16
  'aria-invalid'?: boolean
17
+ validationState?: InputValidationState
16
18
  }
17
19
 
18
20
  const {
@@ -23,6 +25,7 @@ const {
23
25
  readonly,
24
26
  required,
25
27
  'aria-invalid': ariaInvalid,
28
+ validationState,
26
29
  ...props
27
30
  } = Astro.props
28
31
 
@@ -32,9 +35,10 @@ const api = createInput({
32
35
  readOnly: readonly,
33
36
  required,
34
37
  'aria-invalid': ariaInvalid === true ? true : undefined,
38
+ validationState,
35
39
  })
36
40
 
37
- const classes = cn(inputVariants({ size }), className)
41
+ const classes = cn(inputVariants({ size, validationState }), className)
38
42
  ---
39
43
 
40
44
  <input
@@ -0,0 +1,102 @@
1
+ ---
2
+ import { inputVariants, type InputValidationState } from '../input/index.ts'
3
+ import {
4
+ createPasswordInput,
5
+ passwordToggleVariants,
6
+ } from '../password-input/index.ts'
7
+ import { cn } from '../shared/index.ts'
8
+
9
+ interface Props extends astroHTML.JSX.HTMLAttributes {
10
+ size?: 'sm' | 'default' | 'lg'
11
+ disabled?: boolean
12
+ readonly?: boolean
13
+ required?: boolean
14
+ validationState?: InputValidationState
15
+ revealLabel?: string
16
+ hideLabel?: string
17
+ }
18
+
19
+ const {
20
+ size,
21
+ class: className,
22
+ disabled,
23
+ readonly,
24
+ required,
25
+ validationState,
26
+ revealLabel = 'Show password',
27
+ hideLabel = 'Hide password',
28
+ ...props
29
+ } = Astro.props
30
+
31
+ // Render masked initially; progressive enhancement toggles it client-side.
32
+ const { inputType, toggleLabel, togglePressed } = createPasswordInput({
33
+ revealed: false,
34
+ revealLabel,
35
+ hideLabel,
36
+ })
37
+
38
+ const inputClasses = cn(inputVariants({ size, validationState }), 'pr-10', className)
39
+ const ariaInvalid =
40
+ validationState === 'invalid' ? true : validationState === 'valid' ? false : undefined
41
+ ---
42
+
43
+ <div class="relative" data-rfr-password-input>
44
+ <input
45
+ type={inputType}
46
+ class={inputClasses}
47
+ disabled={disabled}
48
+ readonly={readonly}
49
+ required={required}
50
+ aria-invalid={ariaInvalid}
51
+ data-rfr-password-field
52
+ {...props}
53
+ />
54
+ <button
55
+ type="button"
56
+ aria-label={toggleLabel}
57
+ aria-pressed={togglePressed}
58
+ class={passwordToggleVariants()}
59
+ data-rfr-password-toggle
60
+ data-reveal-label={revealLabel}
61
+ data-hide-label={hideLabel}
62
+ >
63
+ <svg
64
+ aria-hidden="true"
65
+ viewBox="0 0 24 24"
66
+ fill="none"
67
+ stroke="currentColor"
68
+ stroke-width="2"
69
+ stroke-linecap="round"
70
+ stroke-linejoin="round"
71
+ class="size-4"
72
+ >
73
+ <path d="M2 12s3-7 10-7 10 7 10 7-3 7-10 7-10-7-10-7Z"></path>
74
+ <circle cx="12" cy="12" r="3"></circle>
75
+ </svg>
76
+ </button>
77
+ </div>
78
+
79
+ <script>
80
+ document
81
+ .querySelectorAll<HTMLElement>('[data-rfr-password-input]')
82
+ .forEach((root) => {
83
+ const field = root.querySelector<HTMLInputElement>(
84
+ '[data-rfr-password-field]',
85
+ )
86
+ const toggle = root.querySelector<HTMLButtonElement>(
87
+ '[data-rfr-password-toggle]',
88
+ )
89
+ if (!field || !toggle) return
90
+
91
+ const revealLabel = toggle.dataset.revealLabel ?? 'Show password'
92
+ const hideLabel = toggle.dataset.hideLabel ?? 'Hide password'
93
+
94
+ toggle.addEventListener('click', () => {
95
+ const revealed = field.type === 'text'
96
+ const next = !revealed
97
+ field.type = next ? 'text' : 'password'
98
+ toggle.setAttribute('aria-pressed', String(next))
99
+ toggle.setAttribute('aria-label', next ? hideLabel : revealLabel)
100
+ })
101
+ })
102
+ </script>
@@ -0,0 +1 @@
1
+ export {}
@@ -0,0 +1,106 @@
1
+ ---
2
+ import { segmentedControlVariants, createSegmentedControl } from '../segmented-control/index.ts'
3
+ import { cn } from '../shared/index.ts'
4
+
5
+ interface Props extends astroHTML.JSX.HTMLAttributes {
6
+ value?: string
7
+ size?: 'sm' | 'md'
8
+ name?: string
9
+ }
10
+
11
+ const { value, size = 'md', name, class: className, ...props } = Astro.props
12
+
13
+ const api = createSegmentedControl({ value, size })
14
+ ---
15
+
16
+ <div
17
+ class={cn(segmentedControlVariants({ size }), className)}
18
+ data-rfr-segmented-group
19
+ data-rfr-segmented-size={size}
20
+ data-rfr-segmented-value={value || ''}
21
+ {...api.ariaProps}
22
+ {...props}
23
+ >
24
+ <slot />
25
+ {name && <input type="hidden" name={name} value={value || ''} data-rfr-segmented-input />}
26
+ </div>
27
+
28
+ <script>
29
+ function getNextSegmentIndex(current: number, key: string, count: number): number {
30
+ if (count <= 0) return current
31
+ switch (key) {
32
+ case 'ArrowRight':
33
+ case 'ArrowDown':
34
+ return (current + 1) % count
35
+ case 'ArrowLeft':
36
+ case 'ArrowUp':
37
+ return (current - 1 + count) % count
38
+ case 'Home':
39
+ return 0
40
+ case 'End':
41
+ return count - 1
42
+ default:
43
+ return current
44
+ }
45
+ }
46
+
47
+ document.querySelectorAll<HTMLElement>('[data-rfr-segmented-group]').forEach((group) => {
48
+ const items = () => group.querySelectorAll<HTMLButtonElement>('[data-rfr-segmented-item]')
49
+
50
+ function selectItem(item: HTMLButtonElement) {
51
+ if (item.disabled) return
52
+ const value = item.getAttribute('data-rfr-segmented-item-value') || ''
53
+
54
+ items().forEach((btn) => {
55
+ const btnValue = btn.getAttribute('data-rfr-segmented-item-value') || ''
56
+ const isSelected = btnValue === value
57
+ btn.setAttribute('aria-checked', String(isSelected))
58
+ btn.setAttribute('tabindex', isSelected ? '0' : '-1')
59
+ btn.setAttribute('data-state', isSelected ? 'checked' : 'unchecked')
60
+
61
+ if (isSelected) {
62
+ btn.classList.add('bg-background', 'text-foreground', 'shadow-sm')
63
+ btn.classList.remove('text-muted-foreground', 'hover:text-foreground')
64
+ } else {
65
+ btn.classList.remove('bg-background', 'text-foreground', 'shadow-sm')
66
+ btn.classList.add('text-muted-foreground', 'hover:text-foreground')
67
+ }
68
+ })
69
+
70
+ group.setAttribute('data-rfr-segmented-value', value)
71
+
72
+ const input = group.querySelector('[data-rfr-segmented-input]') as HTMLInputElement | null
73
+ if (input) input.value = value
74
+
75
+ group.dispatchEvent(new CustomEvent('change', { detail: { value } }))
76
+ }
77
+
78
+ group.addEventListener('click', (e) => {
79
+ const item = (e.target as HTMLElement).closest<HTMLButtonElement>('[data-rfr-segmented-item]')
80
+ if (item) selectItem(item)
81
+ })
82
+
83
+ group.addEventListener('keydown', (e) => {
84
+ const target = e.target as HTMLElement
85
+ if (!target.hasAttribute('data-rfr-segmented-item')) return
86
+
87
+ const allItems = Array.from(items()).filter((i) => !i.disabled)
88
+ const currentIndex = allItems.indexOf(target as HTMLButtonElement)
89
+ if (currentIndex === -1) return
90
+
91
+ if (e.key === ' ' || e.key === 'Enter') {
92
+ e.preventDefault()
93
+ selectItem(target as HTMLButtonElement)
94
+ return
95
+ }
96
+
97
+ const nextIndex = getNextSegmentIndex(currentIndex, e.key, allItems.length)
98
+ if (nextIndex !== currentIndex) {
99
+ e.preventDefault()
100
+ const nextItem = allItems[nextIndex]
101
+ nextItem.focus()
102
+ selectItem(nextItem)
103
+ }
104
+ })
105
+ })
106
+ </script>
@@ -0,0 +1,32 @@
1
+ ---
2
+ import { segmentedControlItemVariants } from '../segmented-control/index.ts'
3
+ import { cn } from '../shared/index.ts'
4
+
5
+ interface Props extends astroHTML.JSX.HTMLAttributes {
6
+ value: string
7
+ size?: 'sm' | 'md'
8
+ checked?: boolean
9
+ disabled?: boolean
10
+ }
11
+
12
+ const { value, size = 'md', checked = false, disabled = false, class: className, ...props } = Astro.props
13
+ ---
14
+
15
+ <button
16
+ type="button"
17
+ role="radio"
18
+ aria-checked={checked}
19
+ aria-disabled={disabled || undefined}
20
+ tabindex={checked ? 0 : -1}
21
+ disabled={disabled || undefined}
22
+ data-state={checked ? 'checked' : 'unchecked'}
23
+ data-rfr-segmented-item
24
+ data-rfr-segmented-item-value={value}
25
+ class={cn(
26
+ segmentedControlItemVariants({ size, state: checked ? 'checked' : 'unchecked' }),
27
+ className,
28
+ )}
29
+ {...props}
30
+ >
31
+ <slot />
32
+ </button>
@@ -0,0 +1 @@
1
+ export {}
@@ -0,0 +1,49 @@
1
+ ---
2
+ import {
3
+ createSeparator,
4
+ separatorVariants,
5
+ separatorLineClass,
6
+ separatorLabelClass,
7
+ type SeparatorOrientation,
8
+ } from '../separator/index.ts'
9
+ import { cn } from '../shared/index.ts'
10
+
11
+ interface Props extends astroHTML.JSX.HTMLAttributes {
12
+ orientation?: SeparatorOrientation
13
+ /**
14
+ * Optional centered label (horizontal only). Renders a flanking line on
15
+ * each side — the "labeled divider" variant. May also be supplied via the
16
+ * default slot.
17
+ */
18
+ label?: string
19
+ decorative?: boolean
20
+ }
21
+
22
+ const {
23
+ orientation = 'horizontal',
24
+ label,
25
+ decorative = true,
26
+ class: className,
27
+ ...props
28
+ } = Astro.props
29
+
30
+ const hasLabel = orientation === 'horizontal' && (label != null || Astro.slots.has('default'))
31
+ const { ariaProps, dataAttributes } = createSeparator({ orientation, decorative })
32
+ ---
33
+
34
+ {
35
+ hasLabel ? (
36
+ <div role="none" class={cn(separatorVariants({ labeled: 'true' }), className)} {...props}>
37
+ <span class={separatorLineClass} />
38
+ <span class={separatorLabelClass}>{label}<slot /></span>
39
+ <span class={separatorLineClass} />
40
+ </div>
41
+ ) : (
42
+ <div
43
+ class={cn(separatorVariants({ orientation }), className)}
44
+ {...ariaProps}
45
+ {...dataAttributes}
46
+ {...props}
47
+ />
48
+ )
49
+ }
@@ -0,0 +1 @@
1
+ export {}
@@ -0,0 +1,110 @@
1
+ ---
2
+ import {
3
+ createSocialAuthButton,
4
+ socialAuthButtonVariants,
5
+ lastUsedBadgeVariants,
6
+ type SocialProvider,
7
+ } from '../social-auth-button/index.ts'
8
+ import { cn } from '../shared/index.ts'
9
+
10
+ interface Props extends astroHTML.JSX.HTMLAttributes {
11
+ provider: SocialProvider
12
+ lastUsed?: boolean
13
+ loading?: boolean
14
+ disabled?: boolean
15
+ }
16
+
17
+ const {
18
+ provider,
19
+ lastUsed,
20
+ loading,
21
+ disabled,
22
+ class: className,
23
+ ...props
24
+ } = Astro.props
25
+
26
+ const { label, mono, dataAttributes } = createSocialAuthButton({ provider })
27
+
28
+ // Mirrors the react-button outline variant base + outline classes so the Astro
29
+ // and React buttons render identically.
30
+ const buttonClasses = cn(
31
+ 'inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0',
32
+ 'border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground',
33
+ 'w-full justify-center gap-2',
34
+ className,
35
+ )
36
+
37
+ const iconFill = (color: string) => (mono ? 'currentColor' : color)
38
+ ---
39
+
40
+ <div class={socialAuthButtonVariants()}>
41
+ <button
42
+ type="button"
43
+ class={buttonClasses}
44
+ disabled={disabled || loading}
45
+ {...dataAttributes}
46
+ {...props}
47
+ >
48
+ {loading ? (
49
+ <svg
50
+ class="animate-spin h-4 w-4"
51
+ xmlns="http://www.w3.org/2000/svg"
52
+ fill="none"
53
+ viewBox="0 0 24 24"
54
+ aria-hidden="true"
55
+ >
56
+ <circle
57
+ class="opacity-25"
58
+ cx="12"
59
+ cy="12"
60
+ r="10"
61
+ stroke="currentColor"
62
+ stroke-width="4"
63
+ />
64
+ <path
65
+ class="opacity-75"
66
+ fill="currentColor"
67
+ d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
68
+ />
69
+ </svg>
70
+ ) : (
71
+ <svg
72
+ class="shrink-0"
73
+ width="18"
74
+ height="18"
75
+ viewBox="0 0 24 24"
76
+ xmlns="http://www.w3.org/2000/svg"
77
+ aria-hidden="true"
78
+ >
79
+ {provider === 'google' && (
80
+ <path
81
+ fill={iconFill('#4285F4')}
82
+ d="M21.35 11.1H12v2.92h5.35c-.23 1.4-1.65 4.1-5.35 4.1a5.99 5.99 0 0 1 0-11.98c1.71 0 2.86.73 3.52 1.36l2.4-2.31C16.46 3.7 14.43 2.8 12 2.8a9.2 9.2 0 1 0 0 18.4c5.31 0 8.82-3.73 8.82-8.99 0-.6-.07-1.06-.17-1.51Z"
83
+ />
84
+ )}
85
+ {provider === 'github' && (
86
+ <path
87
+ fill="currentColor"
88
+ d="M12 2a10 10 0 0 0-3.16 19.49c.5.09.68-.22.68-.48l-.01-1.7c-2.78.6-3.37-1.34-3.37-1.34-.45-1.16-1.11-1.46-1.11-1.46-.91-.62.07-.61.07-.61 1 .07 1.53 1.03 1.53 1.03.9 1.53 2.36 1.09 2.94.83.09-.65.35-1.09.63-1.34-2.22-.25-4.55-1.11-4.55-4.94 0-1.09.39-1.98 1.03-2.68-.1-.25-.45-1.27.1-2.65 0 0 .84-.27 2.75 1.02a9.6 9.6 0 0 1 5 0c1.91-1.29 2.75-1.02 2.75-1.02.55 1.38.2 2.4.1 2.65.64.7 1.03 1.59 1.03 2.68 0 3.84-2.34 4.69-4.57 4.94.36.31.68.92.68 1.85l-.01 2.74c0 .27.18.58.69.48A10 10 0 0 0 12 2Z"
89
+ />
90
+ )}
91
+ {provider === 'microsoft' && (
92
+ <Fragment>
93
+ <path fill={iconFill('#F25022')} d="M3 3h8v8H3z" />
94
+ <path fill={iconFill('#7FBA00')} d="M13 3h8v8h-8z" />
95
+ <path fill={iconFill('#00A4EF')} d="M3 13h8v8H3z" />
96
+ <path fill={iconFill('#FFB900')} d="M13 13h8v8h-8z" />
97
+ </Fragment>
98
+ )}
99
+ {provider === 'apple' && (
100
+ <path
101
+ fill="currentColor"
102
+ d="M16.37 12.78c-.03-2.62 2.14-3.88 2.24-3.94-1.22-1.79-3.12-2.03-3.79-2.06-1.61-.16-3.15.95-3.97.95-.82 0-2.08-.93-3.42-.9-1.76.03-3.39 1.02-4.29 2.6-1.83 3.18-.47 7.88 1.31 10.46.87 1.26 1.91 2.67 3.27 2.62 1.31-.05 1.81-.85 3.4-.85 1.58 0 2.03.85 3.42.82 1.41-.02 2.31-1.28 3.17-2.55.99-1.46 1.4-2.88 1.42-2.95-.03-.01-2.73-1.05-2.76-4.15M13.9 5.1c.72-.88 1.21-2.09 1.08-3.3-1.04.04-2.3.69-3.04 1.56-.66.77-1.24 2.01-1.09 3.19 1.16.09 2.34-.59 3.05-1.45"
103
+ />
104
+ )}
105
+ </svg>
106
+ )}
107
+ {label}
108
+ </button>
109
+ {lastUsed && <span class={lastUsedBadgeVariants()}>Last used</span>}
110
+ </div>
@@ -0,0 +1,13 @@
1
+ ---
2
+ import { socialAuthRowVariants } from '../social-auth-button/index.ts'
3
+ import { cn } from '../shared/index.ts'
4
+
5
+ interface Props extends astroHTML.JSX.HTMLAttributes {}
6
+
7
+ const { class: className, ...props } = Astro.props
8
+ const classes = cn(socialAuthRowVariants(), className)
9
+ ---
10
+
11
+ <div class={classes} {...props}>
12
+ <slot />
13
+ </div>
@@ -0,0 +1 @@
1
+ export {}
@@ -0,0 +1,42 @@
1
+ import { cva } from '../shared/index.ts'
2
+
3
+ /**
4
+ * Container for the EmptyState column. The `bordered` variant wraps the
5
+ * content in a card surface.
6
+ */
7
+ export const emptyStateVariants = cva({
8
+ base: 'flex flex-col items-center text-center gap-3 p-8',
9
+ variants: {
10
+ bordered: {
11
+ true: 'rounded-xl border border-border bg-card',
12
+ false: '',
13
+ },
14
+ },
15
+ defaultVariants: {
16
+ bordered: 'false',
17
+ },
18
+ })
19
+
20
+ /**
21
+ * Tone-tinted classes for the icon chip. Tone styling lives here as a single
22
+ * source of truth (no scattered inline literals in the adapters).
23
+ */
24
+ export const emptyStateIconChipVariants = cva({
25
+ base: 'flex size-12 items-center justify-center rounded-full',
26
+ variants: {
27
+ tone: {
28
+ neutral: 'bg-muted text-muted-foreground',
29
+ success: 'bg-green-500/10 text-green-600',
30
+ warning: 'bg-yellow-500/10 text-yellow-600',
31
+ danger: 'bg-destructive/10 text-destructive',
32
+ },
33
+ },
34
+ defaultVariants: {
35
+ tone: 'neutral',
36
+ },
37
+ })
38
+
39
+ /** Layout class helpers for the title/description/actions slots. */
40
+ export const emptyStateTitleClass = 'text-lg font-semibold text-foreground'
41
+ export const emptyStateDescriptionClass = 'text-sm text-muted-foreground'
42
+ export const emptyStateActionsClass = 'flex items-center justify-center gap-2'
@@ -0,0 +1,27 @@
1
+ export type EmptyStateTone = 'neutral' | 'success' | 'warning' | 'danger'
2
+
3
+ export interface EmptyStateOptions {
4
+ /** Tone that tints the icon chip. Defaults to `neutral`. */
5
+ tone?: EmptyStateTone
6
+ }
7
+
8
+ export interface EmptyStateAPI {
9
+ /** Data attributes for CSS styling hooks. */
10
+ dataAttributes: Record<string, string>
11
+ }
12
+
13
+ /**
14
+ * Headless EmptyState core — JSX-free. Resolves the tone into the data
15
+ * attributes that adapters spread onto the root element.
16
+ */
17
+ export function createEmptyState(options: EmptyStateOptions = {}): EmptyStateAPI {
18
+ const { tone = 'neutral' } = options
19
+
20
+ const dataAttributes: Record<string, string> = {
21
+ 'data-tone': tone,
22
+ }
23
+
24
+ return {
25
+ dataAttributes,
26
+ }
27
+ }
@@ -0,0 +1,14 @@
1
+ export {
2
+ createEmptyState,
3
+ type EmptyStateTone,
4
+ type EmptyStateOptions,
5
+ type EmptyStateAPI,
6
+ } from './empty-state.js'
7
+
8
+ export {
9
+ emptyStateVariants,
10
+ emptyStateIconChipVariants,
11
+ emptyStateTitleClass,
12
+ emptyStateDescriptionClass,
13
+ emptyStateActionsClass,
14
+ } from './empty-state.styles.js'