@refraction-ui/astro 0.11.0 → 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.
- package/dist/astro-empty-state/EmptyState.astro +57 -0
- package/dist/astro-empty-state/index.ts +1 -0
- package/dist/astro-input/Input.astro +5 -1
- package/dist/astro-password-input/PasswordInput.astro +102 -0
- package/dist/astro-password-input/index.ts +1 -0
- package/dist/astro-segmented-control/SegmentedControl.astro +106 -0
- package/dist/astro-segmented-control/SegmentedControlItem.astro +32 -0
- package/dist/astro-segmented-control/index.ts +1 -0
- package/dist/astro-separator/Separator.astro +49 -0
- package/dist/astro-separator/index.ts +1 -0
- package/dist/astro-social-auth-button/SocialAuthButton.astro +110 -0
- package/dist/astro-social-auth-button/SocialAuthRow.astro +13 -0
- package/dist/astro-social-auth-button/index.ts +1 -0
- package/dist/astro-theme/ThemeScript.astro +16 -3
- package/dist/button/button.styles.ts +4 -0
- package/dist/button/button.ts +24 -1
- package/dist/button/index.ts +2 -0
- package/dist/empty-state/empty-state.styles.ts +42 -0
- package/dist/empty-state/empty-state.ts +27 -0
- package/dist/empty-state/index.ts +14 -0
- package/dist/index.ts +22 -17
- package/dist/input/index.ts +2 -0
- package/dist/input/input.styles.ts +4 -0
- package/dist/input/input.ts +39 -3
- package/dist/password-input/index.ts +9 -0
- package/dist/password-input/password-input.styles.ts +10 -0
- package/dist/password-input/password-input.ts +45 -0
- package/dist/segmented-control/index.ts +13 -0
- package/dist/segmented-control/segmented-control.styles.ts +42 -0
- package/dist/segmented-control/segmented-control.ts +80 -0
- package/dist/separator/index.ts +12 -0
- package/dist/separator/separator.styles.ts +34 -0
- package/dist/separator/separator.ts +53 -0
- package/dist/social-auth-button/index.ts +14 -0
- package/dist/social-auth-button/social-auth-button.styles.ts +16 -0
- package/dist/social-auth-button/social-auth-button.ts +51 -0
- package/dist/theme/theme-script.ts +71 -7
- 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 {}
|
|
@@ -1,13 +1,26 @@
|
|
|
1
1
|
---
|
|
2
|
-
import { getThemeScript } from '../theme/index.ts'
|
|
2
|
+
import { getThemeScript, type ThemeMode } from '../theme/index.ts'
|
|
3
3
|
|
|
4
4
|
interface Props {
|
|
5
5
|
storageKey?: string
|
|
6
6
|
attribute?: 'class' | 'data-theme'
|
|
7
|
+
/**
|
|
8
|
+
* Theme to apply when nothing is stored. Match this to the `<ThemeProvider>`
|
|
9
|
+
* client component's `defaultMode` so first-paint and hydration agree. Issue #317.
|
|
10
|
+
*/
|
|
11
|
+
defaultMode?: ThemeMode
|
|
12
|
+
/** When `false`, never fall through to `prefers-color-scheme`. Default `true`. */
|
|
13
|
+
enableSystem?: boolean
|
|
7
14
|
}
|
|
8
15
|
|
|
9
|
-
const {
|
|
10
|
-
|
|
16
|
+
const {
|
|
17
|
+
storageKey = 'rfr-theme',
|
|
18
|
+
attribute = 'class',
|
|
19
|
+
defaultMode = 'system',
|
|
20
|
+
enableSystem = true,
|
|
21
|
+
} = Astro.props
|
|
22
|
+
|
|
23
|
+
const scriptContent = getThemeScript({ storageKey, attribute, defaultMode, enableSystem })
|
|
11
24
|
---
|
|
12
25
|
|
|
13
26
|
<script set:html={scriptContent} />
|
|
@@ -17,6 +17,10 @@ export const buttonVariants = cva({
|
|
|
17
17
|
variants: {
|
|
18
18
|
variant: {
|
|
19
19
|
default: 'bg-primary text-primary-foreground shadow hover:bg-primary/90',
|
|
20
|
+
// Alias for `default` — see resolveButtonVariant + issue #201. Same classes
|
|
21
|
+
// so styling stays identical even if a consumer reaches into `buttonVariants`
|
|
22
|
+
// directly without going through resolveButtonVariant.
|
|
23
|
+
primary: 'bg-primary text-primary-foreground shadow hover:bg-primary/90',
|
|
20
24
|
destructive: 'bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90',
|
|
21
25
|
outline: 'border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground',
|
|
22
26
|
secondary: 'bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80',
|
package/dist/button/button.ts
CHANGED
|
@@ -1,7 +1,30 @@
|
|
|
1
1
|
import type { AccessibilityProps, KeyboardHandlerMap } from '../shared/index.ts'
|
|
2
2
|
import { Keys } from '../shared/index.ts'
|
|
3
3
|
|
|
4
|
-
|
|
4
|
+
/**
|
|
5
|
+
* `primary` is an alias of `default` — included so the universal React-ecosystem
|
|
6
|
+
* muscle-memory (`<Button variant="primary">`) renders as the most-emphasized
|
|
7
|
+
* button instead of falling through to the unstyled base. See issue #201.
|
|
8
|
+
*/
|
|
9
|
+
export type ButtonVariant =
|
|
10
|
+
| 'default'
|
|
11
|
+
| 'primary'
|
|
12
|
+
| 'destructive'
|
|
13
|
+
| 'outline'
|
|
14
|
+
| 'secondary'
|
|
15
|
+
| 'ghost'
|
|
16
|
+
| 'link'
|
|
17
|
+
|
|
18
|
+
/** Variants that map to the same visual style as `default`. */
|
|
19
|
+
export const BUTTON_VARIANT_ALIASES: Partial<Record<ButtonVariant, ButtonVariant>> = {
|
|
20
|
+
primary: 'default',
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Resolve an alias to its canonical variant. Pass-through for unknown/undefined. */
|
|
24
|
+
export function resolveButtonVariant(variant: ButtonVariant | undefined): ButtonVariant | undefined {
|
|
25
|
+
if (!variant) return variant
|
|
26
|
+
return BUTTON_VARIANT_ALIASES[variant] ?? variant
|
|
27
|
+
}
|
|
5
28
|
export type ButtonSize = 'xs' | 'sm' | 'default' | 'lg' | 'icon'
|
|
6
29
|
|
|
7
30
|
export interface ButtonProps {
|
package/dist/button/index.ts
CHANGED
|
@@ -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
|
+
}
|