@refraction-ui/astro 0.11.0 → 0.11.1

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.
@@ -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 { storageKey = 'rfr-theme', attribute = 'class' } = Astro.props
10
- const scriptContent = getThemeScript(storageKey, attribute)
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',
@@ -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
- export type ButtonVariant = 'default' | 'destructive' | 'outline' | 'secondary' | 'ghost' | 'link'
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 {
@@ -1,6 +1,8 @@
1
1
  export {
2
2
  createButton,
3
3
  getButtonType,
4
+ resolveButtonVariant,
5
+ BUTTON_VARIANT_ALIASES,
4
6
  type ButtonProps,
5
7
  type ButtonAPI,
6
8
  type ButtonVariant,
@@ -4,15 +4,79 @@
4
4
  * Works with any framework (React, Astro, plain HTML).
5
5
  */
6
6
 
7
+ import type { ThemeMode } from './theme-machine.js'
8
+
9
+ export interface ThemeScriptOptions {
10
+ /** localStorage key the corresponding ThemeProvider reads. */
11
+ storageKey?: string
12
+ /** Attribute strategy: 'class' toggles `light`/`dark` classes; otherwise an HTML attribute. */
13
+ attribute?: 'class' | 'data-theme'
14
+ /**
15
+ * Theme to apply when nothing is stored AND `enableSystem` is false
16
+ * (or `defaultMode` is explicitly 'light'/'dark'). Match this to your
17
+ * ThemeProvider's `defaultMode` so the pre-paint script and provider agree.
18
+ * Defaults to `'system'` — current behavior, OS preference wins.
19
+ *
20
+ * Issue #317.
21
+ */
22
+ defaultMode?: ThemeMode
23
+ /**
24
+ * When `true` (default) and `defaultMode === 'system'`, the script falls
25
+ * through to `prefers-color-scheme`. When `false`, the script applies
26
+ * `defaultMode` directly and never reads the OS preference.
27
+ */
28
+ enableSystem?: boolean
29
+ }
30
+
31
+ /**
32
+ * Build the pre-paint inline script. Mirrors the resolution order used by
33
+ * the React/Astro `ThemeProvider`:
34
+ *
35
+ * 1. stored value (light|dark|system)
36
+ * 2. if no stored value: `defaultMode` (light|dark applied as-is)
37
+ * 3. if stored or default is 'system' AND `enableSystem`: matchMedia
38
+ * 4. else: 'light'
39
+ *
40
+ * Back-compat: with no options the script behaves exactly as before —
41
+ * stored value, then matchMedia, then 'light'.
42
+ */
7
43
  export function getThemeScript(
8
- storageKey = 'rfr-theme',
44
+ storageKeyOrOptions: string | ThemeScriptOptions = 'rfr-theme',
9
45
  attribute: 'class' | 'data-theme' = 'class',
10
46
  ): string {
11
- // This string is injected as innerHTML of a <script> tag.
12
- // It runs before any CSS/JS loads, preventing flash of wrong theme.
13
- return `(function(){try{var m=localStorage.getItem('${storageKey}');var s=window.matchMedia('(prefers-color-scheme:dark)').matches;var t=m==='dark'||(m!=='light'&&s)?'dark':'light';var d=document.documentElement;${
14
- attribute === 'class'
47
+ const opts: Required<ThemeScriptOptions> =
48
+ typeof storageKeyOrOptions === 'string'
49
+ ? {
50
+ storageKey: storageKeyOrOptions,
51
+ attribute,
52
+ defaultMode: 'system',
53
+ enableSystem: true,
54
+ }
55
+ : {
56
+ storageKey: storageKeyOrOptions.storageKey ?? 'rfr-theme',
57
+ attribute: storageKeyOrOptions.attribute ?? 'class',
58
+ defaultMode: storageKeyOrOptions.defaultMode ?? 'system',
59
+ enableSystem: storageKeyOrOptions.enableSystem ?? true,
60
+ }
61
+
62
+ // `noSystemFallback` short-circuits matchMedia entirely when the consumer
63
+ // wants brand consistency over OS preference (defaultMode='light'/'dark'
64
+ // OR enableSystem=false).
65
+ const noSystemFallback =
66
+ opts.defaultMode === 'light' || opts.defaultMode === 'dark' || !opts.enableSystem
67
+ const safeDefault: 'light' | 'dark' =
68
+ opts.defaultMode === 'dark' ? 'dark' : 'light'
69
+
70
+ const resolveExpr = noSystemFallback
71
+ ? // Stored wins → else hard default. No matchMedia call at all.
72
+ `var t=m==='dark'?'dark':(m==='light'?'light':'${safeDefault}');`
73
+ : // Original behaviour: stored → else matchMedia → else light.
74
+ `var s=window.matchMedia('(prefers-color-scheme:dark)').matches;var t=m==='dark'||(m!=='light'&&s)?'dark':'light';`
75
+
76
+ const applyExpr =
77
+ opts.attribute === 'class'
15
78
  ? "d.classList.remove('light','dark');d.classList.add(t);"
16
- : `d.setAttribute('${attribute}',t);`
17
- }d.style.colorScheme=t;}catch(e){}})()`
79
+ : `d.setAttribute('${opts.attribute}',t);`
80
+
81
+ return `(function(){try{var m=localStorage.getItem('${opts.storageKey}');${resolveExpr}var d=document.documentElement;${applyExpr}d.style.colorScheme=t;}catch(e){}})()`
18
82
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@refraction-ui/astro",
3
- "version": "0.11.0",
3
+ "version": "0.11.1",
4
4
  "description": "All Refraction UI Astro components in one package",
5
5
  "type": "module",
6
6
  "exports": {