@rimelight/ui 0.0.22 → 0.0.24

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rimelight/ui",
3
- "version": "0.0.22",
3
+ "version": "0.0.24",
4
4
  "private": false,
5
5
  "description": "Rimelight Entertainment UI Library.",
6
6
  "keywords": [
@@ -48,6 +48,7 @@
48
48
  "./middleware": "./src/middleware/index.ts",
49
49
  "./middleware/*": "./src/middleware/*",
50
50
  "./nuxt": "./src/nuxt.ts",
51
+ "./nuxt/types": "./src/nuxt-types.ts",
51
52
  "./presets": "./src/presets/index.ts",
52
53
  "./presets/*": "./src/presets/*",
53
54
  "./*": "./src/*"
@@ -70,7 +71,6 @@
70
71
  "tailwindcss": "^4.2.3",
71
72
  "turndown": "^7.2.4",
72
73
  "turndown-plugin-gfm": "^1.0.2",
73
- "unplugin-icons": "23.0.1",
74
74
  "vite-plugin-virtual": "^0.5.0"
75
75
  },
76
76
  "devDependencies": {
@@ -1,7 +1,5 @@
1
1
  ---
2
2
  import RLAButton from "./astro/RLAButton.astro";
3
- import IconSun from "~icons/lucide/sun"
4
- import IconMoon from "~icons/lucide/moon"
5
3
 
6
4
  export interface ThemeToggleProps {
7
5
  class?: string;
@@ -23,8 +21,8 @@ const { class: className, size = "md" } = Astro.props as Props
23
21
  variant="ghost"
24
22
  aria-label="Toggle theme"
25
23
  >
26
- <IconSun class="sun-wrapper block dark:hidden" />
27
- <IconMoon class="moon-wrapper hidden dark:block" />
24
+ <span class="sun-wrapper block dark:hidden i-lucide-sun" />
25
+ <span class="moon-wrapper hidden dark:block i-lucide-moon" />
28
26
  </RLAButton>
29
27
 
30
28
  <script>
@@ -15,36 +15,21 @@ const button = scv({
15
15
  });
16
16
 
17
17
  export interface RLAButtonProps {
18
- /**
19
- * Button variant style.
20
- * @default "solid"
21
- */
22
18
  variant?: keyof typeof buttonTheme.variants.variant;
23
- /**
24
- * Button color.
25
- * @default "primary"
26
- */
27
19
  color?: keyof typeof buttonTheme.variants.color;
28
- /**
29
- * Button size.
30
- * @default "md"
31
- */
32
20
  size?: keyof typeof buttonTheme.variants.size;
33
- /**
34
- * When provided, renders as <a> instead of <button>.
35
- */
36
21
  href?: string;
37
- /**
38
- * UI overrides for individual slots, derived from the button theme.
39
- */
40
- ui?: Partial<Record<keyof typeof buttonTheme.base, string>>;
41
- /**
42
- * External class — applied to the root element.
43
- */
22
+ label?: string;
23
+ leadingIcon?: string;
24
+ trailingIcon?: string;
25
+ loading?: boolean;
26
+ square?: boolean;
27
+ block?: boolean;
28
+ active?: boolean;
29
+ activeColor?: keyof typeof buttonTheme.variants.color;
30
+ activeVariant?: keyof typeof buttonTheme.variants.variant;
31
+ ui?: Partial<Record<string, string>>;
44
32
  class?: string;
45
- /**
46
- * Standard HTML attributes for <button>
47
- */
48
33
  [key: string]: unknown;
49
34
  }
50
35
 
@@ -55,6 +40,15 @@ const {
55
40
  class: className,
56
41
  ui: uiProp,
57
42
  href,
43
+ label,
44
+ leadingIcon,
45
+ trailingIcon,
46
+ loading = false,
47
+ square = false,
48
+ block = false,
49
+ active = false,
50
+ activeColor,
51
+ activeVariant,
58
52
  ...rest
59
53
  } = Astro.props as RLAButtonProps
60
54
 
@@ -63,23 +57,62 @@ const globalConfig = getUIConfig();
63
57
  const mergedUI = defu(
64
58
  uiProp ?? {},
65
59
  (globalConfig.button as Record<string, unknown>) ?? {},
66
- ) as Record<keyof typeof buttonTheme.base, string | undefined>;
60
+ ) as Record<string, string | undefined>;
67
61
 
68
- const { root } = button({
69
- variant,
70
- color,
62
+ const isLeading = (leadingIcon || Astro.slots.has("leading")) || loading
63
+ const isTrailing = (trailingIcon || Astro.slots.has("trailing")) || (loading && !isLeading)
64
+
65
+ const showLeadingIcon = loading
66
+ ? "i-lucide-loader-circle"
67
+ : leadingIcon
68
+
69
+ const showTrailingIcon = loading
70
+ ? "i-lucide-loader-circle"
71
+ : trailingIcon
72
+
73
+ const leadingClasses = loading ? "animate-spin" : ""
74
+ const trailingClasses = loading ? "animate-spin" : ""
75
+
76
+ const resolved = button({
77
+ variant: active && activeVariant ? activeVariant : variant,
78
+ color: active && activeColor ? activeColor : color,
71
79
  size,
80
+ block,
81
+ square: square || (!label && !Astro.slots.has("default")),
82
+ leading: isLeading,
83
+ trailing: isTrailing,
84
+ loading,
85
+ active
72
86
  });
73
87
 
74
- // Determine whether to render <a> or <button>
75
88
  const Tag = href ? "a" : "button";
76
89
  ---
77
-
78
90
  <Tag
79
- class={twMerge(root, mergedUI.root, className)}
91
+ class={twMerge(resolved.root, mergedUI.root, className)}
80
92
  data-slot="rla-button"
81
93
  {...(Tag === "a" ? { href } : {})}
82
94
  {...rest}
83
95
  >
84
- <slot />
96
+ <slot name="leading" {...{ ui: resolved }}>
97
+ {isLeading && (
98
+ <span class={twMerge(resolved.leadingIcon, mergedUI.leadingIcon)} data-slot="leading-icon">
99
+ <span class={twMerge(showLeadingIcon, leadingClasses)} aria-hidden="true" />
100
+ </span>
101
+ )}
102
+ </slot>
103
+
104
+ {label !== undefined && label !== null && (
105
+ <span class={twMerge(resolved.label, mergedUI.label)} data-slot="label">
106
+ {label}
107
+ </span>
108
+ )}
109
+ {!label && <slot />}
110
+
111
+ <slot name="trailing" {...{ ui: resolved }}>
112
+ {isTrailing && (
113
+ <span class={twMerge(resolved.trailingIcon, mergedUI.trailingIcon)} data-slot="trailing-icon">
114
+ <span class={twMerge(showTrailingIcon, trailingClasses)} aria-hidden="true" />
115
+ </span>
116
+ )}
117
+ </slot>
85
118
  </Tag>
@@ -0,0 +1,61 @@
1
+ ---
2
+ import { scv, cx } from "css-variants"
3
+ import { twMerge } from "tailwind-merge"
4
+ import defu from "defu"
5
+ import { iconTheme } from "../../themes/icon.theme"
6
+ import { getUIConfig } from "virtual:rimelight-ui"
7
+
8
+ const icon = scv({
9
+ slots: [...iconTheme.slots],
10
+ base: iconTheme.base,
11
+ variants: iconTheme.variants,
12
+ defaultVariants: iconTheme.defaultVariants,
13
+ classNameResolver: (...args) => twMerge(cx(...args))
14
+ })
15
+
16
+ export interface RLAIconProps {
17
+ /**
18
+ * Icon name following the UnoCSS icons format.
19
+ * @example "i-lucide-home"
20
+ */
21
+ name: string
22
+ /**
23
+ * Size variant for the icon.
24
+ */
25
+ size?: keyof typeof iconTheme.variants.size
26
+ /**
27
+ * External class applied to the root element.
28
+ */
29
+ class?: string
30
+ /**
31
+ * UI overrides for individual slots.
32
+ */
33
+ ui?: Partial<Record<keyof typeof iconTheme.base, string>>
34
+ [key: string]: unknown
35
+ }
36
+
37
+ const {
38
+ name,
39
+ size = "md",
40
+ class: className,
41
+ ui: uiProp,
42
+ ...rest
43
+ } = Astro.props as RLAIconProps
44
+
45
+ const globalConfig = getUIConfig()
46
+
47
+ const mergedUI = defu(
48
+ uiProp ?? {},
49
+ (globalConfig.icon as Record<string, unknown>) ?? {}
50
+ ) as Record<keyof typeof iconTheme.base, string | undefined>
51
+
52
+ const { root } = icon({ size })
53
+ ---
54
+
55
+ <span
56
+ class={twMerge(root, mergedUI.root, className, name)}
57
+ data-slot="rla-icon"
58
+ aria-hidden="true"
59
+ {...rest}
60
+ >
61
+ </span>
@@ -0,0 +1,100 @@
1
+ ---
2
+ import { scv, cx } from "css-variants"
3
+ import { twMerge } from "tailwind-merge"
4
+ import defu from "defu"
5
+ import { logoTheme } from "../../themes/logo.theme.ts"
6
+ import { getUIConfig } from "virtual:rimelight-ui"
7
+
8
+ const logo = scv({
9
+ slots: [...logoTheme.slots],
10
+ base: logoTheme.base,
11
+ variants: logoTheme.variants,
12
+ defaultVariants: logoTheme.defaultVariants,
13
+ classNameResolver: (...args) => twMerge(cx(...args)),
14
+ });
15
+
16
+ export interface RLALogoProps {
17
+ /**
18
+ * The variant of the logo to display.
19
+ * @default "logomark"
20
+ */
21
+ variant?: string;
22
+ /**
23
+ * Override the color mode.
24
+ */
25
+ mode?: "color" | "white" | "black";
26
+ /**
27
+ * The URL to link to.
28
+ * @default "/"
29
+ */
30
+ href?: string;
31
+ /**
32
+ * UI overrides for individual slots, derived from the logo theme.
33
+ */
34
+ ui?: Partial<Record<keyof typeof logoTheme.base, string>>;
35
+ /**
36
+ * External class — applied to the root element.
37
+ */
38
+ class?: string;
39
+ /**
40
+ * The alt text for the logo image.
41
+ */
42
+ alt?: string;
43
+ /**
44
+ * Standard HTML attributes for <a>
45
+ */
46
+ [key: string]: unknown;
47
+ }
48
+
49
+ const {
50
+ variant = "logomark",
51
+ mode,
52
+ href = "/",
53
+ class: className,
54
+ ui: uiProp,
55
+ alt,
56
+ ...rest
57
+ } = Astro.props as RLALogoProps;
58
+
59
+ const globalConfig = getUIConfig();
60
+
61
+ const mergedUI = defu(
62
+ uiProp ?? {},
63
+ (globalConfig.logo as Record<string, unknown>) ?? {},
64
+ ) as Record<keyof typeof logoTheme.base, string | undefined>;
65
+
66
+ const { root } = logo();
67
+
68
+ const rcLogos = globalConfig.logos as Record<string, any>;
69
+ let logoSrc: string | null = null;
70
+
71
+ if (rcLogos && typeof rcLogos === "object") {
72
+ let src = rcLogos[variant];
73
+ if (src && typeof src === "object") {
74
+ logoSrc = src[mode || "color"] || src.black || src.color;
75
+ } else if (typeof src === "string") {
76
+ logoSrc = src;
77
+ }
78
+ }
79
+
80
+ // Support for UnoCSS icons (e.g. `i-heroicons-home`)
81
+ const isIcon = logoSrc && (logoSrc.includes(":") || logoSrc.startsWith("i-"));
82
+ ---
83
+
84
+ <a
85
+ class={twMerge(root, mergedUI.root, className)}
86
+ href={href}
87
+ aria-label={alt || variant}
88
+ data-slot="rla-logo"
89
+ {...rest}
90
+ >
91
+ {logoSrc ? (
92
+ isIcon ? (
93
+ <span class:list={[logoSrc, "size-full block shrink-0"]} aria-hidden="true" />
94
+ ) : (
95
+ <img src={logoSrc} alt={alt || ""} class="size-full block object-contain shrink-0" />
96
+ )
97
+ ) : (
98
+ <slot />
99
+ )}
100
+ </a>
@@ -0,0 +1,71 @@
1
+ ---
2
+ import { scv, cx } from "css-variants"
3
+ import { twMerge } from "tailwind-merge"
4
+ import defu from "defu"
5
+ import { placeholderTheme } from "../../themes/placeholder.theme.ts"
6
+ import { getUIConfig } from "virtual:rimelight-ui"
7
+
8
+ const placeholder = scv({
9
+ slots: [...placeholderTheme.slots],
10
+ base: placeholderTheme.base,
11
+ variants: placeholderTheme.variants,
12
+ compoundVariants: [...placeholderTheme.compoundVariants],
13
+ defaultVariants: placeholderTheme.defaultVariants,
14
+ classNameResolver: (...args) => twMerge(cx(...args))
15
+ })
16
+
17
+ export interface RLAPlaceholderProps {
18
+ /**
19
+ * UI overrides for individual slots, derived from the placeholder theme.
20
+ */
21
+ ui?: Partial<Record<keyof typeof placeholderTheme.base, string>>
22
+ /**
23
+ * External class — applied to the root element.
24
+ */
25
+ class?: string
26
+ /**
27
+ * Standard HTML attributes.
28
+ */
29
+ [key: string]: unknown
30
+ }
31
+
32
+ const {
33
+ class: className,
34
+ ui: uiProp,
35
+ ...rest
36
+ } = Astro.props as RLAPlaceholderProps
37
+
38
+ const globalConfig = getUIConfig()
39
+
40
+ const mergedUI = defu(
41
+ uiProp ?? {},
42
+ (globalConfig.placeholder as Record<string, unknown>) ?? {}
43
+ ) as Record<keyof typeof placeholderTheme.base, string | undefined>
44
+
45
+ const { base, svg } = placeholder()
46
+ ---
47
+
48
+ <div class={twMerge(base, mergedUI.base, className)} {...rest}>
49
+ <svg class={twMerge(svg, mergedUI.svg)}>
50
+ <defs>
51
+ <pattern
52
+ id="pattern-5c1e4f0e-62d5-498b-8ff0-cf77bb448c8e"
53
+ x="0"
54
+ y="0"
55
+ width="10"
56
+ height="10"
57
+ patternUnits="userSpaceOnUse"
58
+ >
59
+ <path d="M-3 13 15-5M-5 5l18-18M-1 21 17 3" />
60
+ </pattern>
61
+ </defs>
62
+ <rect
63
+ stroke="none"
64
+ fill="url(#pattern-5c1e4f0e-62d5-498b-8ff0-cf77bb448c8e)"
65
+ width="100%"
66
+ height="100%"
67
+ />
68
+ </svg>
69
+
70
+ <slot />
71
+ </div>
@@ -1,97 +1,139 @@
1
- <script setup lang="ts">
2
- import { scv, cx } from "css-variants"
3
- import { twMerge } from "tailwind-merge"
4
- import defu from "defu"
5
- import { buttonTheme } from "../../themes/button.theme.ts"
6
- import { getUIConfig } from "virtual:rimelight-ui"
7
-
8
- const button = scv({
9
- slots: [...buttonTheme.slots],
10
- base: buttonTheme.base,
11
- variants: buttonTheme.variants,
12
- compoundVariants: [...buttonTheme.compoundVariants],
13
- defaultVariants: buttonTheme.defaultVariants,
14
- classNameResolver: (...args) => twMerge(cx(...args))
15
- })
16
-
17
- export interface RLVButtonProps {
18
- /**
19
- * Button variant.
20
- *
21
- * @default "solid"
22
- */
23
- variant?: keyof typeof buttonTheme.variants.variant
24
- /**
25
- * Button color.
26
- *
27
- * @default "primary"
28
- */
29
- color?: keyof typeof buttonTheme.variants.color
30
- /**
31
- * Button size.
32
- *
33
- * @default "md"
34
- */
35
- size?: keyof typeof buttonTheme.variants.size
36
- /**
37
- * When provided, renders as <a> instead of <button>.
38
- */
39
- href?: string
40
- /**
41
- * UI overrides for individual slots, derived from the button theme.
42
- */
43
- ui?: Partial<Record<keyof typeof buttonTheme.base, string>>
44
- /**
45
- * External class — applied to the root element.
46
- */
47
- class?: string
48
- /**
49
- * Standard HTML attributes for <button>
50
- */
51
- [key: string]: unknown
52
- }
53
-
54
- const {
55
- variant = "solid",
56
- color = "primary",
57
- size = "md",
58
- class: className,
59
- ui: uiProp,
60
- href
61
- } = defineProps<RLVButtonProps>()
62
-
63
- defineSlots<{
64
- /**
65
- * The default slot for the button content (e.g., text, icons).
66
- */
67
- default(props: {}): any
68
- }>()
69
-
70
- const globalConfig = getUIConfig()
71
-
72
- const mergedUI = defu(
73
- uiProp ?? {},
74
- (globalConfig.button as Record<string, unknown>) ?? {}
75
- ) as Record<keyof typeof buttonTheme.base, string | undefined>
76
-
77
- const { root } = button({
78
- variant,
79
- color,
80
- size
81
- })
82
-
83
- // Determine whether to render <a> or <button>
84
- const Tag = href ? "a" : "button"
85
- </script>
86
-
87
- <template>
88
- <component
89
- v-bind="$attrs"
90
- :is="Tag"
91
- :href="href"
92
- :class="twMerge(root, mergedUI.root, className)"
93
- data-slot="rlv-button"
94
- >
95
- <slot />
96
- </component>
97
- </template>
1
+ <script setup lang="ts">
2
+ import { computed, useSlots } from "vue"
3
+ import { scv, cx } from "css-variants"
4
+ import { twMerge } from "tailwind-merge"
5
+ import defu from "defu"
6
+ import { buttonTheme } from "../../themes/button.theme.ts"
7
+ import { getUIConfig } from "virtual:rimelight-ui"
8
+
9
+ const button = scv({
10
+ slots: [...buttonTheme.slots],
11
+ base: buttonTheme.base,
12
+ variants: buttonTheme.variants,
13
+ compoundVariants: [...buttonTheme.compoundVariants],
14
+ defaultVariants: buttonTheme.defaultVariants,
15
+ classNameResolver: (...args) => twMerge(cx(...args))
16
+ })
17
+
18
+ export interface RLVButtonProps {
19
+ variant?: keyof typeof buttonTheme.variants.variant
20
+ color?: keyof typeof buttonTheme.variants.color
21
+ size?: keyof typeof buttonTheme.variants.size
22
+ href?: string
23
+ label?: string
24
+ leadingIcon?: string
25
+ trailingIcon?: string
26
+ loading?: boolean
27
+ square?: boolean
28
+ block?: boolean
29
+ active?: boolean
30
+ activeColor?: keyof typeof buttonTheme.variants.color
31
+ activeVariant?: keyof typeof buttonTheme.variants.variant
32
+ ui?: Partial<Record<string, string>>
33
+ class?: string
34
+ [key: string]: unknown
35
+ }
36
+
37
+ const {
38
+ variant = "solid",
39
+ color = "primary",
40
+ size = "md",
41
+ class: className,
42
+ ui: uiProp,
43
+ href,
44
+ label,
45
+ leadingIcon,
46
+ trailingIcon,
47
+ loading = false,
48
+ square = false,
49
+ block = false,
50
+ active = false,
51
+ activeColor,
52
+ activeVariant
53
+ } = defineProps<RLVButtonProps>()
54
+
55
+ const slots = useSlots()
56
+
57
+ const isLeading = computed(() => !!(leadingIcon || slots.leading) || loading)
58
+ const isTrailing = computed(() => !!(trailingIcon || slots.trailing) || (loading && !isLeading.value))
59
+
60
+ const showLeadingIcon = computed(() => {
61
+ if (loading) return "i-lucide-loader-circle"
62
+ return leadingIcon
63
+ })
64
+
65
+ const showTrailingIcon = computed(() => {
66
+ if (loading) return "i-lucide-loader-circle"
67
+ return trailingIcon
68
+ })
69
+
70
+ const leadingIconClass = computed(() => {
71
+ return loading && isLeading.value ? "animate-spin" : ""
72
+ })
73
+
74
+ const trailingIconClass = computed(() => {
75
+ return loading && isTrailing.value ? "animate-spin" : ""
76
+ })
77
+
78
+ defineSlots<{
79
+ default(props: {}): any
80
+ leading(props: { ui: Record<string, string> }): any
81
+ trailing(props: { ui: Record<string, string> }): any
82
+ }>()
83
+
84
+ const globalConfig = getUIConfig()
85
+
86
+ const mergedUI = defu(
87
+ uiProp ?? {},
88
+ (globalConfig.button as Record<string, unknown>) ?? {}
89
+ ) as Record<string, string | undefined>
90
+
91
+ const resolved = computed(() => button({
92
+ variant: active && activeVariant ? activeVariant : variant,
93
+ color: active && activeColor ? activeColor : color,
94
+ size,
95
+ block,
96
+ square: square || (!label && !slots.default),
97
+ leading: isLeading.value,
98
+ trailing: isTrailing.value,
99
+ loading,
100
+ active
101
+ }))
102
+
103
+ const Tag = href ? "a" : "button"
104
+ </script>
105
+
106
+ <template>
107
+ <component
108
+ v-bind="$attrs"
109
+ :is="Tag"
110
+ :href="href"
111
+ :class="twMerge(resolved.root, mergedUI.root, className)"
112
+ data-slot="rlv-button"
113
+ >
114
+ <slot name="leading" :ui="resolved">
115
+ <span
116
+ v-if="isLeading"
117
+ :class="twMerge(resolved.leadingIcon, mergedUI.leadingIcon)"
118
+ data-slot="leading-icon"
119
+ >
120
+ <span :class="[showLeadingIcon, leadingIconClass]" aria-hidden="true" />
121
+ </span>
122
+ </slot>
123
+
124
+ <span v-if="label !== undefined && label !== null" :class="twMerge(resolved.label, mergedUI.label)" data-slot="label">
125
+ {{ label }}
126
+ </span>
127
+ <slot v-else />
128
+
129
+ <slot name="trailing" :ui="resolved">
130
+ <span
131
+ v-if="isTrailing"
132
+ :class="twMerge(resolved.trailingIcon, mergedUI.trailingIcon)"
133
+ data-slot="trailing-icon"
134
+ >
135
+ <span :class="[showTrailingIcon, trailingIconClass]" aria-hidden="true" />
136
+ </span>
137
+ </slot>
138
+ </component>
139
+ </template>
@@ -0,0 +1,64 @@
1
+ <script setup lang="ts">
2
+ import { computed } from "vue"
3
+ import { scv, cx } from "css-variants"
4
+ import { twMerge } from "tailwind-merge"
5
+ import { useUi } from "../../composables"
6
+ import { iconTheme } from "../../themes/icon.theme"
7
+
8
+ const icon = scv({
9
+ slots: [...iconTheme.slots],
10
+ base: iconTheme.base,
11
+ variants: iconTheme.variants,
12
+ defaultVariants: iconTheme.defaultVariants,
13
+ classNameResolver: (...args) => twMerge(cx(...args))
14
+ })
15
+
16
+ export interface RLVIconProps {
17
+ /**
18
+ * Icon name following the UnoCSS icons format. Can be a string like "i-lucide-home" or a Vue
19
+ * component.
20
+ *
21
+ * @example
22
+ * "i-lucide-home"
23
+ */
24
+ name: string
25
+ /**
26
+ * Size variant for the icon.
27
+ */
28
+ size?: keyof typeof iconTheme.variants.size
29
+ /**
30
+ * External class applied to the root element.
31
+ */
32
+ class?: string
33
+ /**
34
+ * UI overrides for individual slots.
35
+ */
36
+ ui?: Partial<Record<keyof typeof iconTheme.base, string>>
37
+ }
38
+
39
+ const { name, size = "md", class: className, ui: uiProp } = defineProps<RLVIconProps>()
40
+
41
+ const mergedUI = useUi("icon", uiProp as Record<string, unknown> | undefined)
42
+
43
+ const resolvedClasses = computed(() => {
44
+ const { root } = icon({ size })
45
+ return {
46
+ root: twMerge(root, mergedUI.value.root, className)
47
+ }
48
+ })
49
+
50
+ defineSlots<{
51
+ default(props: {}): any
52
+ }>()
53
+ </script>
54
+
55
+ <template>
56
+ <span
57
+ v-bind="$attrs"
58
+ :class="[resolvedClasses.root, name]"
59
+ data-slot="rlv-icon"
60
+ aria-hidden="true"
61
+ >
62
+ <slot />
63
+ </span>
64
+ </template>
@@ -0,0 +1,123 @@
1
+ <script setup lang="ts">
2
+ import { computed } from "vue"
3
+ import { scv, cx } from "css-variants"
4
+ import { twMerge } from "tailwind-merge"
5
+ import defu from "defu"
6
+ import { logoTheme } from "../../themes/logo.theme.ts"
7
+ import { getUIConfig } from "virtual:rimelight-ui"
8
+
9
+ const logo = scv({
10
+ slots: [...logoTheme.slots],
11
+ base: logoTheme.base,
12
+ variants: logoTheme.variants,
13
+ defaultVariants: logoTheme.defaultVariants,
14
+ classNameResolver: (...args) => twMerge(cx(...args))
15
+ })
16
+
17
+ export interface RLVLogoProps {
18
+ /**
19
+ * The variant of the logo to display.
20
+ *
21
+ * @default "logomark"
22
+ */
23
+ variant?: string
24
+ /**
25
+ * Override the color mode.
26
+ */
27
+ mode?: "color" | "white" | "black"
28
+ /**
29
+ * The URL to link to.
30
+ *
31
+ * @default "/"
32
+ */
33
+ href?: string
34
+ /**
35
+ * UI overrides for individual slots, derived from the logo theme.
36
+ */
37
+ ui?: Partial<Record<keyof typeof logoTheme.base, string>>
38
+ /**
39
+ * External class — applied to the root element.
40
+ */
41
+ class?: string
42
+ /**
43
+ * The alt text for the logo image.
44
+ */
45
+ alt?: string
46
+ /**
47
+ * Standard HTML attributes for <a>
48
+ */
49
+ [key: string]: unknown
50
+ }
51
+
52
+ const props = withDefaults(defineProps<RLVLogoProps>(), {
53
+ variant: "logomark",
54
+ href: "/"
55
+ })
56
+
57
+ defineSlots<{
58
+ /**
59
+ * The default slot for the logo content (e.g., text, custom elements).
60
+ */
61
+ default(props: {}): any
62
+ }>()
63
+
64
+ const globalConfig = getUIConfig()
65
+
66
+ const mergedUI = computed(
67
+ () =>
68
+ defu(props.ui ?? {}, (globalConfig.logo as Record<string, unknown>) ?? {}) as Record<
69
+ keyof typeof logoTheme.base,
70
+ string | undefined
71
+ >
72
+ )
73
+
74
+ const resolvedClasses = computed(() => {
75
+ const { root } = logo()
76
+ return {
77
+ root: twMerge(root, mergedUI.value.root, props.class)
78
+ }
79
+ })
80
+
81
+ const logoSrc = computed<string | null>(() => {
82
+ const rcLogos = globalConfig.logos as Record<string, any>
83
+ let src: any = null
84
+
85
+ if (rcLogos && typeof rcLogos === "object") {
86
+ src = rcLogos[props.variant]
87
+ if (src && typeof src === "object") {
88
+ src = src[props.mode || "color"] || src.black || src.color
89
+ }
90
+ }
91
+
92
+ if (src && typeof src === "string" && src) return src
93
+
94
+ return null
95
+ })
96
+
97
+ const isIcon = computed(() => {
98
+ const src = logoSrc.value
99
+ if (!src) return false
100
+ if (src.startsWith("http") || src.startsWith("/") || src.startsWith(".")) return false
101
+ return src.includes(":") || src.startsWith("i-")
102
+ })
103
+
104
+ const Tag = computed(() => (props.href ? "a" : "div"))
105
+ </script>
106
+
107
+ <template>
108
+ <component
109
+ v-bind="$attrs"
110
+ :is="Tag"
111
+ :href="Tag === 'a' ? href : undefined"
112
+ :aria-label="alt || variant"
113
+ :class="resolvedClasses.root"
114
+ data-slot="rlv-logo"
115
+ >
116
+ <template v-if="logoSrc">
117
+ <!-- Using mode='svg' pattern is not strictly needed for UnoCSS icons -->
118
+ <span v-if="isIcon" :class="[logoSrc, 'size-full block shrink-0']" aria-hidden="true" />
119
+ <img v-else :src="logoSrc" :alt="alt || ''" class="size-full block object-contain shrink-0" />
120
+ </template>
121
+ <slot v-else />
122
+ </component>
123
+ </template>
@@ -0,0 +1,76 @@
1
+ <script setup lang="ts">
2
+ import { scv, cx } from "css-variants"
3
+ import { twMerge } from "tailwind-merge"
4
+ import defu from "defu"
5
+ import { placeholderTheme } from "../../themes/placeholder.theme.ts"
6
+ import { getUIConfig } from "virtual:rimelight-ui"
7
+
8
+ const placeholder = scv({
9
+ slots: [...placeholderTheme.slots],
10
+ base: placeholderTheme.base,
11
+ variants: placeholderTheme.variants,
12
+ compoundVariants: [...placeholderTheme.compoundVariants],
13
+ defaultVariants: placeholderTheme.defaultVariants,
14
+ classNameResolver: (...args) => twMerge(cx(...args))
15
+ })
16
+
17
+ export interface RLVPlaceholderProps {
18
+ /**
19
+ * UI overrides for individual slots, derived from the placeholder theme.
20
+ */
21
+ ui?: Partial<Record<keyof typeof placeholderTheme.base, string>>
22
+ /**
23
+ * External class — applied to the root element.
24
+ */
25
+ class?: string
26
+ /**
27
+ * Standard HTML attributes.
28
+ */
29
+ [key: string]: unknown
30
+ }
31
+
32
+ const { class: className, ui: uiProp } = defineProps<RLVPlaceholderProps>()
33
+
34
+ defineSlots<{
35
+ /**
36
+ * The default slot for placeholder content.
37
+ */
38
+ default(props: {}): any
39
+ }>()
40
+
41
+ const globalConfig = getUIConfig()
42
+
43
+ const mergedUI = defu(
44
+ uiProp ?? {},
45
+ (globalConfig.placeholder as Record<string, unknown>) ?? {}
46
+ ) as Record<keyof typeof placeholderTheme.base, string | undefined>
47
+
48
+ const { base, svg } = placeholder()
49
+ </script>
50
+
51
+ <template>
52
+ <div :class="twMerge(base, mergedUI.base, className)" v-bind="$attrs">
53
+ <svg :class="twMerge(svg, mergedUI.svg)">
54
+ <defs>
55
+ <pattern
56
+ id="pattern-5c1e4f0e-62d5-498b-8ff0-cf77bb448c8e"
57
+ x="0"
58
+ y="0"
59
+ width="10"
60
+ height="10"
61
+ patternUnits="userSpaceOnUse"
62
+ >
63
+ <path d="M-3 13 15-5M-5 5l18-18M-1 21 17 3" />
64
+ </pattern>
65
+ </defs>
66
+ <rect
67
+ stroke="none"
68
+ fill="url(#pattern-5c1e4f0e-62d5-498b-8ff0-cf77bb448c8e)"
69
+ width="100%"
70
+ height="100%"
71
+ />
72
+ </svg>
73
+
74
+ <slot />
75
+ </div>
76
+ </template>
@@ -3,7 +3,6 @@ import UnoCSS from "unocss/astro"
3
3
  import vue from "@astrojs/vue"
4
4
  import uiPlugin from "@nuxt/ui/vite"
5
5
  import defu from "defu"
6
- import Icons from "unplugin-icons/vite"
7
6
  import tailwindcss from "@tailwindcss/vite"
8
7
  import { defaultUIConfig } from "../themes"
9
8
 
@@ -14,9 +13,25 @@ export type UIConfigOverrides = {
14
13
  "button"?: Partial<(typeof defaultUIConfig)["button"]>
15
14
  "header"?: Partial<(typeof defaultUIConfig)["header"]>
16
15
  "scroll-to-top"?: Partial<(typeof defaultUIConfig)["scroll-to-top"]>
16
+ "logo"?: Partial<(typeof defaultUIConfig)["logo"]>
17
17
  }
18
18
 
19
19
  export interface UIOptions {
20
+ /**
21
+ * Configuration for the Logo component variants.
22
+ */
23
+ logos?: Record<
24
+ string,
25
+ | string
26
+ | {
27
+ light?: string
28
+ dark?: string
29
+ color?: string
30
+ white?: string
31
+ black?: string
32
+ [key: string]: string | undefined
33
+ }
34
+ >
20
35
  /**
21
36
  * Global UI overrides applied to all components. Merged with defaults via `defu` (first arg
22
37
  * wins).
@@ -47,7 +62,8 @@ export interface UIOptions {
47
62
  * })
48
63
  */
49
64
  export function ui(options: UIOptions = {}): AstroIntegration[] {
50
- const resolvedConfig = defu(options.ui ?? {}, defaultUIConfig)
65
+ const resolvedUI = defu(options.ui ?? {}, defaultUIConfig)
66
+ const resolvedConfig = { ...resolvedUI, logos: options.logos ?? {} }
51
67
 
52
68
  const virtualModuleId = "virtual:rimelight-ui"
53
69
  const resolvedVirtualModuleId = "\0" + virtualModuleId
@@ -74,10 +90,6 @@ export function ui(options: UIOptions = {}): AstroIntegration[] {
74
90
  router: false,
75
91
  scanPackages: ["@rimelight/ui"]
76
92
  }),
77
- Icons({
78
- compiler: "astro",
79
- autoInstall: true
80
- }),
81
93
  tailwindcss(),
82
94
  {
83
95
  name: "vite-plugin-rimelight-ui-config",
@@ -0,0 +1,3 @@
1
+ export type * from "@nuxt/ui/runtime/types"
2
+ export type * from "@nuxt/ui/runtime/composables"
3
+ export type * from "@nuxt/ui/runtime/utils"
@@ -2,6 +2,7 @@ import {
2
2
  definePreset,
3
3
  presetTypography,
4
4
  presetWind4,
5
+ presetIcons,
5
6
  transformerDirectives,
6
7
  transformerVariantGroup
7
8
  } from "unocss"
@@ -23,6 +24,12 @@ export default definePreset(() => {
23
24
  "font-family": "var(--font-mono), monospace"
24
25
  }
25
26
  }
27
+ }),
28
+ presetIcons({
29
+ extraProperties: {
30
+ "display": "inline-block",
31
+ "vertical-align": "middle"
32
+ }
26
33
  })
27
34
  ],
28
35
  transformers: [transformerDirectives(), transformerVariantGroup()],
@@ -3,7 +3,7 @@
3
3
  * set of variants including color logic.
4
4
  */
5
5
  export const buttonTheme = {
6
- slots: ["root"],
6
+ slots: ["root", "label", "leadingIcon", "trailingIcon"],
7
7
  base: {
8
8
  root: [
9
9
  "inline-flex items-center justify-center gap-4xs rounded-md font-medium ws-nowrap",
@@ -11,12 +11,16 @@ export const buttonTheme = {
11
11
  "transition-all outline-none focus-visible:ring-3",
12
12
  "disabled:pointer-events-none disabled:op-50",
13
13
  "aria-invalid:border-error-500 aria-invalid:ring-error-500/20"
14
- ]
14
+ ],
15
+ label: "truncate",
16
+ leadingIcon: "shrink-0",
17
+ trailingIcon: "shrink-0"
15
18
  },
16
19
  variants: {
17
20
  variant: {
18
21
  solid: { root: "b shadow-xs" },
19
- outlined: { root: "b shadow-xs border" },
22
+ outline: { root: "b shadow-xs border" },
23
+ soft: { root: "" },
20
24
  subtle: { root: "" },
21
25
  ghost: { root: "" },
22
26
  link: { root: "p-0 h-auto" }
@@ -33,11 +37,24 @@ export const buttonTheme = {
33
37
  source: { root: "" }
34
38
  },
35
39
  size: {
36
- xs: { root: "h-7 px-2xs text-xs [&_svg]:size-3.5" },
37
- sm: { root: "h-8 px-xs text-sm [&_svg]:size-4" },
38
- md: { root: "h-9 px-sm text-sm [&_svg]:size-4" },
39
- lg: { root: "h-10 px-md text-base [&_svg]:size-4.5" },
40
- xl: { root: "h-11 px-lg text-base [&_svg]:size-5" }
40
+ xs: { root: "h-7 px-2xs text-xs [&_svg]:size-3.5", leadingIcon: "[&_svg]:size-3.5", trailingIcon: "[&_svg]:size-3.5" },
41
+ sm: { root: "h-8 px-xs text-sm [&_svg]:size-4", leadingIcon: "[&_svg]:size-4", trailingIcon: "[&_svg]:size-4" },
42
+ md: { root: "h-9 px-sm text-sm [&_svg]:size-4", leadingIcon: "[&_svg]:size-4", trailingIcon: "[&_svg]:size-4" },
43
+ lg: { root: "h-10 px-md text-base [&_svg]:size-4.5", leadingIcon: "[&_svg]:size-4.5", trailingIcon: "[&_svg]:size-4.5" },
44
+ xl: { root: "h-11 px-lg text-base [&_svg]:size-5", leadingIcon: "[&_svg]:size-5", trailingIcon: "[&_svg]:size-5" }
45
+ },
46
+ block: {
47
+ true: { root: "w-full justify-center", trailingIcon: "ms-auto" }
48
+ },
49
+ square: {
50
+ true: { root: "" }
51
+ },
52
+ leading: { true: {} },
53
+ trailing: { true: {} },
54
+ loading: { true: {} },
55
+ active: {
56
+ true: { root: "" },
57
+ false: { root: "" }
41
58
  }
42
59
  },
43
60
  compoundVariants: [
@@ -105,63 +122,63 @@ export const buttonTheme = {
105
122
  }
106
123
  },
107
124
  {
108
- variant: "outlined",
125
+ variant: "outline",
109
126
  color: "primary",
110
127
  classNames: {
111
128
  root: "border-primary/20 text-primary-600 bg-primary/5 hover:bg-primary/10 hover:border-primary/30 focus-visible:ring-primary/40"
112
129
  }
113
130
  },
114
131
  {
115
- variant: "outlined",
132
+ variant: "outline",
116
133
  color: "secondary",
117
134
  classNames: {
118
135
  root: "border-secondary/20 text-secondary-600 bg-secondary/5 hover:bg-secondary/10 hover:border-secondary/30 focus-visible:ring-secondary/40"
119
136
  }
120
137
  },
121
138
  {
122
- variant: "outlined",
139
+ variant: "outline",
123
140
  color: "info",
124
141
  classNames: {
125
142
  root: "border-info/20 text-info-600 bg-info/5 hover:bg-info/10 hover:border-info/30 focus-visible:ring-info/40"
126
143
  }
127
144
  },
128
145
  {
129
- variant: "outlined",
146
+ variant: "outline",
130
147
  color: "success",
131
148
  classNames: {
132
149
  root: "border-success/20 text-success-600 bg-success/5 hover:bg-success/10 hover:border-success/30 focus-visible:ring-success/40"
133
150
  }
134
151
  },
135
152
  {
136
- variant: "outlined",
153
+ variant: "outline",
137
154
  color: "warning",
138
155
  classNames: {
139
156
  root: "border-warning/20 text-warning-600 bg-warning/5 hover:bg-warning/10 hover:border-warning/30 focus-visible:ring-warning/40"
140
157
  }
141
158
  },
142
159
  {
143
- variant: "outlined",
160
+ variant: "outline",
144
161
  color: "error",
145
162
  classNames: {
146
163
  root: "border-error/20 text-error-600 bg-error/5 hover:bg-error/10 hover:border-error/30 focus-visible:ring-error/40"
147
164
  }
148
165
  },
149
166
  {
150
- variant: "outlined",
167
+ variant: "outline",
151
168
  color: "commentary",
152
169
  classNames: {
153
170
  root: "border-commentary/20 text-commentary-600 bg-commentary/5 hover:bg-commentary/10 hover:border-commentary/30 focus-visible:ring-commentary/40"
154
171
  }
155
172
  },
156
173
  {
157
- variant: "outlined",
174
+ variant: "outline",
158
175
  color: "ideation",
159
176
  classNames: {
160
177
  root: "border-ideation/20 text-ideation-600 bg-ideation/5 hover:bg-ideation/10 hover:border-ideation/30 focus-visible:ring-ideation/40"
161
178
  }
162
179
  },
163
180
  {
164
- variant: "outlined",
181
+ variant: "outline",
165
182
  color: "source",
166
183
  classNames: {
167
184
  root: "border-source/20 text-source-600 bg-source/5 hover:bg-source/10 hover:border-source/30 focus-visible:ring-source/40"
@@ -355,6 +372,130 @@ export const buttonTheme = {
355
372
  classNames: {
356
373
  root: "text-source-600 hover:underline hover:decoration-source/30"
357
374
  }
375
+ },
376
+ // soft variant compound variants
377
+ {
378
+ variant: "soft",
379
+ color: "primary",
380
+ classNames: {
381
+ root: "text-primary-600 bg-primary/10 hover:bg-primary/20 focus-visible:ring-primary/40"
382
+ }
383
+ },
384
+ {
385
+ variant: "soft",
386
+ color: "secondary",
387
+ classNames: {
388
+ root: "text-secondary-600 bg-secondary/10 hover:bg-secondary/20 focus-visible:ring-secondary/40"
389
+ }
390
+ },
391
+ {
392
+ variant: "soft",
393
+ color: "info",
394
+ classNames: {
395
+ root: "text-info-600 bg-info/10 hover:bg-info/20 focus-visible:ring-info/40"
396
+ }
397
+ },
398
+ {
399
+ variant: "soft",
400
+ color: "success",
401
+ classNames: {
402
+ root: "text-success-600 bg-success/10 hover:bg-success/20 focus-visible:ring-success/40"
403
+ }
404
+ },
405
+ {
406
+ variant: "soft",
407
+ color: "warning",
408
+ classNames: {
409
+ root: "text-warning-600 bg-warning/10 hover:bg-warning/20 focus-visible:ring-warning/40"
410
+ }
411
+ },
412
+ {
413
+ variant: "soft",
414
+ color: "error",
415
+ classNames: {
416
+ root: "text-error-600 bg-error/10 hover:bg-error/20 focus-visible:ring-error/40"
417
+ }
418
+ },
419
+ {
420
+ variant: "soft",
421
+ color: "commentary",
422
+ classNames: {
423
+ root: "text-commentary-600 bg-commentary/10 hover:bg-commentary/20 focus-visible:ring-commentary/40"
424
+ }
425
+ },
426
+ {
427
+ variant: "soft",
428
+ color: "ideation",
429
+ classNames: {
430
+ root: "text-ideation-600 bg-ideation/10 hover:bg-ideation/20 focus-visible:ring-ideation/40"
431
+ }
432
+ },
433
+ {
434
+ variant: "soft",
435
+ color: "source",
436
+ classNames: {
437
+ root: "text-source-600 bg-source/10 hover:bg-source/20 focus-visible:ring-source/40"
438
+ }
439
+ },
440
+ // square + size compound variants (explicit width to match height)
441
+ {
442
+ square: true,
443
+ size: "xs",
444
+ classNames: {
445
+ root: "p-1 w-7"
446
+ }
447
+ },
448
+ {
449
+ square: true,
450
+ size: "sm",
451
+ classNames: {
452
+ root: "p-1.5 w-8"
453
+ }
454
+ },
455
+ {
456
+ square: true,
457
+ size: "md",
458
+ classNames: {
459
+ root: "p-1.5 w-9"
460
+ }
461
+ },
462
+ {
463
+ square: true,
464
+ size: "lg",
465
+ classNames: {
466
+ root: "p-2 w-10"
467
+ }
468
+ },
469
+ {
470
+ square: true,
471
+ size: "xl",
472
+ classNames: {
473
+ root: "p-2 w-11"
474
+ }
475
+ },
476
+ // loading compound variants
477
+ {
478
+ loading: true,
479
+ leading: true,
480
+ classNames: {
481
+ leadingIcon: "animate-spin"
482
+ }
483
+ },
484
+ {
485
+ loading: true,
486
+ leading: false,
487
+ trailing: true,
488
+ classNames: {
489
+ trailingIcon: "animate-spin"
490
+ }
491
+ },
492
+ {
493
+ loading: true,
494
+ leading: false,
495
+ trailing: false,
496
+ classNames: {
497
+ trailingIcon: "animate-spin"
498
+ }
358
499
  }
359
500
  ],
360
501
  defaultVariants: {
@@ -0,0 +1,18 @@
1
+ export const iconTheme = {
2
+ slots: ["root"],
3
+ base: {
4
+ root: ["inline-block", "align-middle"]
5
+ },
6
+ variants: {
7
+ size: {
8
+ xs: { root: ["h-3", "w-3"] },
9
+ sm: { root: ["h-4", "w-4"] },
10
+ md: { root: ["h-5", "w-5"] },
11
+ lg: { root: ["h-6", "w-6"] },
12
+ xl: { root: ["h-8", "w-8"] }
13
+ }
14
+ },
15
+ defaultVariants: {
16
+ size: "md"
17
+ }
18
+ } as const
@@ -2,6 +2,9 @@ import { headerTheme } from "./header.theme"
2
2
  import { footerTheme } from "./footer.theme"
3
3
  import { scrollToTopTheme } from "./scroll-to-top.theme"
4
4
  import { buttonTheme } from "./button.theme"
5
+ import { logoTheme } from "./logo.theme"
6
+ import { iconTheme } from "./icon.theme"
7
+ import { placeholderTheme } from "./placeholder.theme"
5
8
 
6
9
  /**
7
10
  * Default UI configuration for all components. This is the baseline that gets merged with
@@ -10,8 +13,11 @@ import { buttonTheme } from "./button.theme"
10
13
  export const defaultUIConfig = {
11
14
  "header": headerTheme,
12
15
  "footer": footerTheme,
16
+ "logo": logoTheme,
13
17
  "scroll-to-top": scrollToTopTheme,
14
- "button": buttonTheme
18
+ "button": buttonTheme,
19
+ "icon": iconTheme,
20
+ "placeholder": placeholderTheme
15
21
  } as const
16
22
 
17
23
  export type DefaultUIConfig = typeof defaultUIConfig
@@ -0,0 +1,8 @@
1
+ export const logoTheme = {
2
+ slots: ["root"],
3
+ base: {
4
+ root: "flex items-center justify-center transition-opacity hover:opacity-80 shrink-0 select-none overflow-hidden"
5
+ },
6
+ variants: {},
7
+ defaultVariants: {}
8
+ } as const
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Theme configuration for the Placeholder component. Defines the slots, base styles for a
3
+ * placeholder container with a decorative SVG pattern overlay.
4
+ */
5
+ export const placeholderTheme = {
6
+ slots: ["base", "svg"],
7
+ base: {
8
+ base: "relative flex items-center justify-center overflow-hidden rounded-sm border-1 border-dashed border-primary px-4 opacity-75",
9
+ svg: "absolute inset-0 h-full w-full stroke-primary/10"
10
+ },
11
+ variants: {},
12
+ compoundVariants: [],
13
+ defaultVariants: {}
14
+ } as const