@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.
Files changed (38) 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/astro-theme/ThemeScript.astro +16 -3
  15. package/dist/button/button.styles.ts +4 -0
  16. package/dist/button/button.ts +24 -1
  17. package/dist/button/index.ts +2 -0
  18. package/dist/empty-state/empty-state.styles.ts +42 -0
  19. package/dist/empty-state/empty-state.ts +27 -0
  20. package/dist/empty-state/index.ts +14 -0
  21. package/dist/index.ts +22 -17
  22. package/dist/input/index.ts +2 -0
  23. package/dist/input/input.styles.ts +4 -0
  24. package/dist/input/input.ts +39 -3
  25. package/dist/password-input/index.ts +9 -0
  26. package/dist/password-input/password-input.styles.ts +10 -0
  27. package/dist/password-input/password-input.ts +45 -0
  28. package/dist/segmented-control/index.ts +13 -0
  29. package/dist/segmented-control/segmented-control.styles.ts +42 -0
  30. package/dist/segmented-control/segmented-control.ts +80 -0
  31. package/dist/separator/index.ts +12 -0
  32. package/dist/separator/separator.styles.ts +34 -0
  33. package/dist/separator/separator.ts +53 -0
  34. package/dist/social-auth-button/index.ts +14 -0
  35. package/dist/social-auth-button/social-auth-button.styles.ts +16 -0
  36. package/dist/social-auth-button/social-auth-button.ts +51 -0
  37. package/dist/theme/theme-script.ts +71 -7
  38. package/package.json +24 -19
@@ -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'
package/dist/index.ts CHANGED
@@ -2,6 +2,7 @@
2
2
  export * from './astro-accordion/index.ts';
3
3
  export * from './shared/index.ts';
4
4
  export * from './astro-ai/index.ts';
5
+ export * from './astro-analytics/index.ts';
5
6
  export * from './astro-animated-text/index.ts';
6
7
  export * from './astro-app-shell/index.ts';
7
8
  export * from './astro-auth/index.ts';
@@ -12,14 +13,19 @@ export * from './astro-bottom-nav/index.ts';
12
13
  export * from './astro-breadcrumbs/index.ts';
13
14
  export * from './astro-button/index.ts';
14
15
  export * from './astro-calendar/index.ts';
16
+ export * from './astro-callout/index.ts';
15
17
  export * from './astro-card/index.ts';
18
+ export * from './astro-card-grid/index.ts';
19
+ export * from './astro-carousel/index.ts';
16
20
  export * from './astro-charts/index.ts';
17
21
  export * from './astro-checkbox/index.ts';
22
+ export * from './astro-code-block/index.ts';
18
23
  export * from './astro-code-editor/index.ts';
19
24
  export * from './astro-collapsible/index.ts';
20
25
  export * from './astro-command/index.ts';
21
- export * from './astro-conversation/index.ts';
26
+ export * from './astro-command-input/index.ts';
22
27
  export * from './astro-content-protection/index.ts';
28
+ export * from './astro-conversation/index.ts';
23
29
  export * from './astro-cookie-consent/index.ts';
24
30
  export * from './astro-data-table/index.ts';
25
31
  export * from './astro-date-picker/index.ts';
@@ -28,19 +34,27 @@ export * from './astro-dialog/index.ts';
28
34
  export * from './astro-diff-viewer/index.ts';
29
35
  export * from './astro-dropdown-menu/index.ts';
30
36
  export * from './astro-emoji-picker/index.ts';
37
+ export * from './astro-empty-state/index.ts';
31
38
  export * from './astro-feedback-dialog/index.ts';
39
+ export * from './astro-file-tree/index.ts';
32
40
  export * from './astro-file-upload/index.ts';
33
41
  export * from './astro-footer/index.ts';
42
+ export * from './astro-icon-system/index.ts';
34
43
  export * from './astro-inline-editor/index.ts';
35
44
  export * from './astro-input/index.ts';
36
45
  export * from './astro-input-group/index.ts';
37
46
  export * from './astro-install-prompt/index.ts';
38
47
  export * from './astro-keyboard-shortcut/index.ts';
39
48
  export * from './astro-language-selector/index.ts';
49
+ export * from './astro-link-card/index.ts';
50
+ export * from './astro-logger/index.ts';
40
51
  export * from './astro-markdown-renderer/index.ts';
41
52
  export * from './astro-mobile-nav/index.ts';
42
53
  export * from './astro-navbar/index.ts';
43
54
  export * from './astro-otp-input/index.ts';
55
+ export * from './astro-pagination/index.ts';
56
+ export * from './astro-password-input/index.ts';
57
+ export * from './astro-payment/index.ts';
44
58
  export * from './astro-popover/index.ts';
45
59
  export * from './astro-presence-indicator/index.ts';
46
60
  export * from './astro-progress-display/index.ts';
@@ -49,12 +63,19 @@ export * from './astro-reaction-bar/index.ts';
49
63
  export * from './astro-resizable-layout/index.ts';
50
64
  export * from './astro-rich-editor/index.ts';
51
65
  export * from './astro-search-bar/index.ts';
66
+ export * from './astro-segmented-control/index.ts';
52
67
  export * from './astro-select/index.ts';
68
+ export * from './astro-separator/index.ts';
53
69
  export * from './astro-sidebar/index.ts';
54
70
  export * from './astro-skeleton/index.ts';
71
+ export * from './astro-skip-to-content/index.ts';
55
72
  export * from './astro-slide-viewer/index.ts';
73
+ export * from './astro-slider/index.ts';
74
+ export * from './astro-social-auth-button/index.ts';
56
75
  export * from './astro-status-indicator/index.ts';
76
+ export * from './astro-steps/index.ts';
57
77
  export * from './astro-switch/index.ts';
78
+ export * from './astro-table-of-contents/index.ts';
58
79
  export * from './astro-tabs/index.ts';
59
80
  export * from './astro-textarea/index.ts';
60
81
  export * from './astro-theme/index.ts';
@@ -65,19 +86,3 @@ export * from './astro-version-selector/index.ts';
65
86
  export * from './astro-video-player/index.ts';
66
87
  export * from './astro-voice-pill/index.ts';
67
88
  export * from './astro-waveform/index.ts';
68
- export * from './astro-table-of-contents/index.ts';
69
- export * from './astro-carousel/index.ts';
70
- export * from './astro-slider/index.ts';
71
- export * from './astro-pagination/index.ts';
72
- export * from './astro-callout/index.ts';
73
- export * from './astro-steps/index.ts';
74
- export * from './astro-file-tree/index.ts';
75
- export * from './astro-icon-system/index.ts';
76
- export * from './astro-skip-to-content/index.ts';
77
- export * from './astro-code-block/index.ts';
78
- export * from './astro-link-card/index.ts';
79
- export * from './astro-card-grid/index.ts';
80
- export * from './astro-payment/index.ts';
81
- export * from './astro-command-input/index.ts';
82
- export * from './astro-logger/index.ts';
83
- export * from './astro-analytics/index.ts';
@@ -1,7 +1,9 @@
1
1
  export {
2
2
  createInput,
3
3
  getInputAriaProps,
4
+ resolveAriaInvalid,
4
5
  type InputType,
6
+ type InputValidationState,
5
7
  type InputProps,
6
8
  type InputAPI,
7
9
  } from './input.js'
@@ -8,6 +8,10 @@ export const inputVariants = cva({
8
8
  default: 'h-9',
9
9
  lg: 'h-10 text-base',
10
10
  },
11
+ validationState: {
12
+ valid: 'border-green-500 focus-visible:ring-green-500',
13
+ invalid: 'border-destructive focus-visible:ring-destructive',
14
+ },
11
15
  },
12
16
  defaultVariants: {
13
17
  size: 'default',
@@ -2,6 +2,9 @@ import type { AccessibilityProps } from '../shared/index.ts'
2
2
 
3
3
  export type InputType = 'text' | 'password' | 'email' | 'number' | 'tel' | 'url' | 'search'
4
4
 
5
+ /** High-level validation affordance for an input. */
6
+ export type InputValidationState = 'valid' | 'invalid'
7
+
5
8
  export interface InputProps {
6
9
  type?: InputType
7
10
  disabled?: boolean
@@ -9,6 +12,14 @@ export interface InputProps {
9
12
  required?: boolean
10
13
  placeholder?: string
11
14
  'aria-invalid'?: boolean
15
+ /**
16
+ * Validation affordance. The core derives `aria-invalid` from this:
17
+ * - `'valid'` → `aria-invalid={false}`
18
+ * - `'invalid'` → `aria-invalid={true}`
19
+ *
20
+ * An explicit `aria-invalid` prop always takes precedence.
21
+ */
22
+ validationState?: InputValidationState
12
23
  }
13
24
 
14
25
  export interface InputAPI {
@@ -18,22 +29,47 @@ export interface InputAPI {
18
29
  dataAttributes: Record<string, string>
19
30
  }
20
31
 
32
+ /**
33
+ * Resolve the effective `aria-invalid` value from an explicit `aria-invalid`
34
+ * prop and a high-level `validationState`. The explicit prop always wins;
35
+ * otherwise `validationState` maps `'invalid' → true`, `'valid' → false`, and
36
+ * an absent state leaves `aria-invalid` undefined (legacy behavior).
37
+ */
38
+ export function resolveAriaInvalid(
39
+ ariaInvalid: boolean | undefined,
40
+ validationState: InputValidationState | undefined,
41
+ ): boolean | undefined {
42
+ if (ariaInvalid !== undefined) {
43
+ return ariaInvalid
44
+ }
45
+ if (validationState === 'invalid') {
46
+ return true
47
+ }
48
+ if (validationState === 'valid') {
49
+ return false
50
+ }
51
+ return undefined
52
+ }
53
+
21
54
  export function createInput(props: InputProps = {}): InputAPI {
22
55
  const {
23
56
  disabled = false,
24
57
  readOnly = false,
25
58
  required = false,
26
59
  'aria-invalid': ariaInvalid,
60
+ validationState,
27
61
  } = props
28
62
 
63
+ const resolvedInvalid = resolveAriaInvalid(ariaInvalid, validationState)
64
+
29
65
  const ariaProps: InputAPI['ariaProps'] = {}
30
66
 
31
67
  if (disabled) {
32
68
  ariaProps['aria-disabled'] = true
33
69
  }
34
70
 
35
- if (ariaInvalid) {
36
- ariaProps['aria-invalid'] = true
71
+ if (resolvedInvalid !== undefined) {
72
+ ariaProps['aria-invalid'] = resolvedInvalid
37
73
  }
38
74
 
39
75
  if (required) {
@@ -50,7 +86,7 @@ export function createInput(props: InputProps = {}): InputAPI {
50
86
  dataAttributes['data-readonly'] = ''
51
87
  }
52
88
 
53
- if (ariaInvalid) {
89
+ if (resolvedInvalid === true) {
54
90
  dataAttributes['data-invalid'] = ''
55
91
  }
56
92
 
@@ -0,0 +1,9 @@
1
+ export {
2
+ createPasswordInput,
3
+ DEFAULT_REVEAL_LABEL,
4
+ DEFAULT_HIDE_LABEL,
5
+ type PasswordInputProps,
6
+ type PasswordInputAPI,
7
+ } from './password-input.js'
8
+
9
+ export { passwordToggleVariants } from './password-input.styles.js'
@@ -0,0 +1,10 @@
1
+ import { cva } from '../shared/index.ts'
2
+
3
+ /**
4
+ * Positioning classes for the reveal/hide toggle button rendered inside a
5
+ * password input's trailing padding. The wrapper must be `relative` and the
6
+ * input must reserve trailing space (e.g. `pr-10`).
7
+ */
8
+ export const passwordToggleVariants = cva({
9
+ base: 'absolute right-2 top-1/2 flex size-7 -translate-y-1/2 items-center justify-center rounded-md text-muted-foreground transition-colors hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring',
10
+ })
@@ -0,0 +1,45 @@
1
+ export interface PasswordInputProps {
2
+ /** Whether the password is currently revealed (shown as plain text). */
3
+ revealed?: boolean
4
+ /**
5
+ * Accessible label for the reveal (show password) action.
6
+ * @default 'Show password'
7
+ */
8
+ revealLabel?: string
9
+ /**
10
+ * Accessible label for the hide action.
11
+ * @default 'Hide password'
12
+ */
13
+ hideLabel?: string
14
+ }
15
+
16
+ export interface PasswordInputAPI {
17
+ /** The `type` attribute for the underlying input given the revealed state. */
18
+ inputType: 'text' | 'password'
19
+ /** The accessible label for the toggle button given the revealed state. */
20
+ toggleLabel: string
21
+ /** The `aria-pressed` value for the toggle button. */
22
+ togglePressed: boolean
23
+ }
24
+
25
+ export const DEFAULT_REVEAL_LABEL = 'Show password'
26
+ export const DEFAULT_HIDE_LABEL = 'Hide password'
27
+
28
+ /**
29
+ * State-free password-input helper. Given a `revealed` flag (owned by the
30
+ * framework adapter), it returns the input `type`, the toggle button's
31
+ * accessible label, and its `aria-pressed` value. It does not hold state.
32
+ */
33
+ export function createPasswordInput(props: PasswordInputProps = {}): PasswordInputAPI {
34
+ const {
35
+ revealed = false,
36
+ revealLabel = DEFAULT_REVEAL_LABEL,
37
+ hideLabel = DEFAULT_HIDE_LABEL,
38
+ } = props
39
+
40
+ return {
41
+ inputType: revealed ? 'text' : 'password',
42
+ toggleLabel: revealed ? hideLabel : revealLabel,
43
+ togglePressed: revealed,
44
+ }
45
+ }
@@ -0,0 +1,13 @@
1
+ export {
2
+ createSegmentedControl,
3
+ getNextSegmentIndex,
4
+ type SegmentedControlSize,
5
+ type SegmentedControlValue,
6
+ type SegmentedControlProps,
7
+ type SegmentedControlAPI,
8
+ } from './segmented-control.js'
9
+
10
+ export {
11
+ segmentedControlVariants,
12
+ segmentedControlItemVariants,
13
+ } from './segmented-control.styles.js'
@@ -0,0 +1,42 @@
1
+ import { cva } from '../shared/index.ts'
2
+
3
+ /** Pill-shaped container that holds the segments (`role="radiogroup"`). */
4
+ export const segmentedControlVariants = cva({
5
+ base: 'inline-flex items-center gap-1 rounded-lg bg-muted p-1',
6
+ variants: {
7
+ size: {
8
+ sm: '',
9
+ md: '',
10
+ },
11
+ },
12
+ defaultVariants: {
13
+ size: 'md',
14
+ },
15
+ })
16
+
17
+ /**
18
+ * A single segment (`role="radio"`). The `state` variant carries the
19
+ * active vs inactive visual treatment so adapters never inline these classes.
20
+ */
21
+ export const segmentedControlItemVariants = cva({
22
+ base: [
23
+ 'inline-flex items-center justify-center gap-1.5 rounded-md font-medium',
24
+ 'whitespace-nowrap transition-colors',
25
+ 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring',
26
+ 'focus-visible:ring-offset-2 focus-visible:ring-offset-muted',
27
+ ].join(' '),
28
+ variants: {
29
+ size: {
30
+ sm: 'px-2.5 py-1 text-xs',
31
+ md: 'px-3 py-1.5 text-sm',
32
+ },
33
+ state: {
34
+ checked: 'bg-background text-foreground shadow-sm',
35
+ unchecked: 'text-muted-foreground hover:text-foreground',
36
+ },
37
+ },
38
+ defaultVariants: {
39
+ size: 'md',
40
+ state: 'unchecked',
41
+ },
42
+ })
@@ -0,0 +1,80 @@
1
+ import type { AccessibilityProps } from '../shared/index.ts'
2
+
3
+ export type SegmentedControlSize = 'sm' | 'md'
4
+
5
+ /** Value identifying a single segment. */
6
+ export type SegmentedControlValue = string
7
+
8
+ export interface SegmentedControlProps {
9
+ /** Currently selected segment value (controlled). */
10
+ value?: SegmentedControlValue
11
+ /** Visual size of the control. */
12
+ size?: SegmentedControlSize
13
+ }
14
+
15
+ export interface SegmentedControlAPI {
16
+ /** ARIA attributes to spread on the group element. */
17
+ ariaProps: Partial<AccessibilityProps>
18
+ /** Data attributes for CSS styling hooks. */
19
+ dataAttributes: Record<string, string>
20
+ }
21
+
22
+ /**
23
+ * Build the framework-agnostic group props for a segmented control.
24
+ *
25
+ * Returns `role="radiogroup"` plus data attributes; adapters spread these
26
+ * onto their group element and handle their own rendering/state.
27
+ */
28
+ export function createSegmentedControl(
29
+ props: SegmentedControlProps = {},
30
+ ): SegmentedControlAPI {
31
+ const { value, size = 'md' } = props
32
+
33
+ const ariaProps: Partial<AccessibilityProps> = {
34
+ role: 'radiogroup',
35
+ }
36
+
37
+ const dataAttributes: Record<string, string> = {
38
+ 'data-size': size,
39
+ }
40
+ if (value !== undefined) {
41
+ dataAttributes['data-value'] = value
42
+ }
43
+
44
+ return { ariaProps, dataAttributes }
45
+ }
46
+
47
+ /**
48
+ * Pure keyboard-navigation rule shared by every adapter.
49
+ *
50
+ * Given the current index, the pressed key, and the item count, returns the
51
+ * index that focus/selection should move to. Arrow keys wrap; Home/End jump
52
+ * to the ends. Unhandled keys (or an empty group) return `current` unchanged.
53
+ *
54
+ * - ArrowRight / ArrowDown → next (wraps to 0 past the end)
55
+ * - ArrowLeft / ArrowUp → previous (wraps to last before 0)
56
+ * - Home → first
57
+ * - End → last
58
+ */
59
+ export function getNextSegmentIndex(
60
+ current: number,
61
+ key: string,
62
+ count: number,
63
+ ): number {
64
+ if (count <= 0) return current
65
+
66
+ switch (key) {
67
+ case 'ArrowRight':
68
+ case 'ArrowDown':
69
+ return (current + 1) % count
70
+ case 'ArrowLeft':
71
+ case 'ArrowUp':
72
+ return (current - 1 + count) % count
73
+ case 'Home':
74
+ return 0
75
+ case 'End':
76
+ return count - 1
77
+ default:
78
+ return current
79
+ }
80
+ }
@@ -0,0 +1,12 @@
1
+ export {
2
+ createSeparator,
3
+ type SeparatorOrientation,
4
+ type SeparatorProps,
5
+ type SeparatorAPI,
6
+ } from './separator.js'
7
+
8
+ export {
9
+ separatorVariants,
10
+ separatorLineClass,
11
+ separatorLabelClass,
12
+ } from './separator.styles.js'
@@ -0,0 +1,34 @@
1
+ import { cva } from '../shared/index.ts'
2
+
3
+ /**
4
+ * Visual variants for the separator rule.
5
+ *
6
+ * - `orientation: horizontal` → a full-width 1px line.
7
+ * - `orientation: vertical` → a full-height 1px line.
8
+ * - `labeled` → flex layout used by the labeled-divider variant, which
9
+ * arranges a centered label between two flanking lines.
10
+ */
11
+ export const separatorVariants = cva({
12
+ base: '',
13
+ variants: {
14
+ orientation: {
15
+ horizontal: 'h-px w-full bg-border',
16
+ vertical: 'w-px h-full bg-border',
17
+ },
18
+ labeled: {
19
+ true: 'flex items-center',
20
+ false: '',
21
+ },
22
+ },
23
+ defaultVariants: {
24
+ orientation: 'horizontal',
25
+ labeled: 'false',
26
+ },
27
+ })
28
+
29
+ /** Class for each flanking line in the labeled-divider variant. */
30
+ export const separatorLineClass = 'h-px flex-1 bg-border'
31
+
32
+ /** Class for the centered label in the labeled-divider variant. */
33
+ export const separatorLabelClass =
34
+ 'px-3 text-xs uppercase tracking-wide text-muted-foreground'
@@ -0,0 +1,53 @@
1
+ import type { AccessibilityProps } from '../shared/index.ts'
2
+
3
+ export type SeparatorOrientation = 'horizontal' | 'vertical'
4
+
5
+ export interface SeparatorProps {
6
+ /** Orientation of the separator. Defaults to `'horizontal'`. */
7
+ orientation?: SeparatorOrientation
8
+ /**
9
+ * When `true` (the default) the separator is purely visual and is hidden
10
+ * from assistive tech (`role="none"`). When `false` it is exposed as a
11
+ * semantic `role="separator"` with the appropriate `aria-orientation`.
12
+ */
13
+ decorative?: boolean
14
+ }
15
+
16
+ /**
17
+ * ARIA attributes for a separator. Extends the shared base with
18
+ * `aria-orientation`, which is specific to the `separator` role.
19
+ */
20
+ export interface SeparatorAriaProps extends Partial<AccessibilityProps> {
21
+ 'aria-orientation'?: SeparatorOrientation
22
+ }
23
+
24
+ export interface SeparatorAPI {
25
+ /** ARIA attributes to spread on the element. */
26
+ ariaProps: SeparatorAriaProps
27
+ /** Data attributes for CSS styling hooks. */
28
+ dataAttributes: Record<string, string>
29
+ }
30
+
31
+ /**
32
+ * Headless separator core — computes ARIA semantics and data attributes for
33
+ * a separator rule. JSX-free so it can be shared across framework adapters.
34
+ *
35
+ * Per ARIA, a decorative separator must not be announced (`role="none"`),
36
+ * while a semantic one exposes `role="separator"` and `aria-orientation`.
37
+ */
38
+ export function createSeparator(props: SeparatorProps = {}): SeparatorAPI {
39
+ const { orientation = 'horizontal', decorative = true } = props
40
+
41
+ const ariaProps: SeparatorAriaProps = decorative
42
+ ? { role: 'none' }
43
+ : { role: 'separator', 'aria-orientation': orientation }
44
+
45
+ const dataAttributes: Record<string, string> = {
46
+ 'data-orientation': orientation,
47
+ }
48
+
49
+ return {
50
+ ariaProps,
51
+ dataAttributes,
52
+ }
53
+ }
@@ -0,0 +1,14 @@
1
+ export {
2
+ createSocialAuthButton,
3
+ PROVIDER_LABELS,
4
+ PROVIDER_MONO,
5
+ type SocialProvider,
6
+ type SocialAuthButtonProps,
7
+ type SocialAuthButtonAPI,
8
+ } from './social-auth-button.js'
9
+
10
+ export {
11
+ socialAuthButtonVariants,
12
+ socialAuthRowVariants,
13
+ lastUsedBadgeVariants,
14
+ } from './social-auth-button.styles.js'
@@ -0,0 +1,16 @@
1
+ import { cva } from '../shared/index.ts'
2
+
3
+ /** Wrapper around an individual social auth button (positions the lastUsed badge). */
4
+ export const socialAuthButtonVariants = cva({
5
+ base: 'relative',
6
+ })
7
+
8
+ /** Responsive grid wrapper: one column on mobile, two columns from `sm` up. */
9
+ export const socialAuthRowVariants = cva({
10
+ base: 'grid grid-cols-1 sm:grid-cols-2 gap-3',
11
+ })
12
+
13
+ /** Floating "Last used" badge anchored to the top-right of a button. */
14
+ export const lastUsedBadgeVariants = cva({
15
+ base: 'absolute -top-2 -right-2 rounded-full bg-primary text-primary-foreground text-[10px] px-1.5',
16
+ })
@@ -0,0 +1,51 @@
1
+ export type SocialProvider = 'google' | 'github' | 'microsoft' | 'apple'
2
+
3
+ /** "Continue with …" label shown on each provider's button. */
4
+ export const PROVIDER_LABELS: Record<SocialProvider, string> = {
5
+ google: 'Continue with Google',
6
+ github: 'Continue with GitHub',
7
+ microsoft: 'Continue with Microsoft',
8
+ apple: 'Continue with Apple',
9
+ }
10
+
11
+ /**
12
+ * Which providers render their brand mark monochrome (currentColor). GitHub and
13
+ * Apple are monochrome by design so they read correctly on dark/outline
14
+ * surfaces; Google and Microsoft keep their full brand color.
15
+ */
16
+ export const PROVIDER_MONO: Record<SocialProvider, boolean> = {
17
+ google: false,
18
+ github: true,
19
+ microsoft: false,
20
+ apple: true,
21
+ }
22
+
23
+ export interface SocialAuthButtonProps {
24
+ /** Identity provider this button authenticates against. */
25
+ provider: SocialProvider
26
+ }
27
+
28
+ export interface SocialAuthButtonAPI {
29
+ /** Data attributes for CSS styling hooks. */
30
+ dataAttributes: Record<string, string>
31
+ /** "Continue with …" label for the provider. */
32
+ label: string
33
+ /** Whether the brand mark should render monochrome. */
34
+ mono: boolean
35
+ }
36
+
37
+ /**
38
+ * Framework-agnostic core for a social sign-in button. Returns the provider
39
+ * label, monochrome hint, and the `data-provider` styling hook. No React/JSX.
40
+ */
41
+ export function createSocialAuthButton(
42
+ props: SocialAuthButtonProps,
43
+ ): SocialAuthButtonAPI {
44
+ const { provider } = props
45
+
46
+ return {
47
+ dataAttributes: { 'data-provider': provider },
48
+ label: PROVIDER_LABELS[provider],
49
+ mono: PROVIDER_MONO[provider],
50
+ }
51
+ }