@rimelight/ui 0.0.6 → 0.0.7

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/LICENSE CHANGED
@@ -1,21 +1,21 @@
1
- MIT License
2
-
3
- Copyright (c) 2026 Daniel Marchi
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Daniel Marchi
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rimelight/ui",
3
- "version": "0.0.6",
3
+ "version": "0.0.7",
4
4
  "private": false,
5
5
  "description": "Rimelight Entertainment UI Library.",
6
6
  "homepage": "https://rimelight.com/docs",
@@ -14,7 +14,6 @@
14
14
  },
15
15
  "files": [
16
16
  "src",
17
- "i18n-data.json",
18
17
  "worker-configuration.d.ts",
19
18
  "README.md",
20
19
  "LICENSE"
@@ -22,19 +21,17 @@
22
21
  "type": "module",
23
22
  "exports": {
24
23
  ".": "./src/integrations/index.ts",
24
+ "./styles/*": "./src/styles/*",
25
25
  "./components/*": "./src/components/*",
26
26
  "./components/astro/*": "./src/components/astro/*",
27
27
  "./components/vue/*": "./src/components/vue/*",
28
+ "./components/nuxt/*": "./src/components/nuxt/*",
28
29
  "./themes": "./src/themes/index.ts",
29
30
  "./themes/*": "./src/themes/*",
30
31
  "./composables": "./src/composables/index.ts",
31
32
  "./composables/*": "./src/composables/*",
32
33
  "./config": "./src/config/index.ts",
33
34
  "./config/*": "./src/config/*",
34
- "./domain": "./src/domain/index.ts",
35
- "./domain/*": "./src/domain/*",
36
- "./lib": "./src/lib/index.ts",
37
- "./lib/*": "./src/lib/*",
38
35
  "./utils": "./src/utils/index.ts",
39
36
  "./utils/*": "./src/utils/*",
40
37
  "./integrations": "./src/integrations/index.ts",
@@ -47,11 +44,14 @@
47
44
  "access": "public"
48
45
  },
49
46
  "dependencies": {
47
+ "@astrojs/vue": "6.0.1",
50
48
  "@iconify-json/lucide": "^1.2.102",
49
+ "@nuxt/ui": "4.6.1",
51
50
  "css-variants": "2.3.5",
52
51
  "defu": "6.1.6",
53
52
  "embla-carousel": "8.6.0",
54
53
  "tailwind-merge": "^3.3.1",
54
+ "tailwindcss": "4.2.2",
55
55
  "unplugin-icons": "23.0.1"
56
56
  },
57
57
  "devDependencies": {
@@ -62,9 +62,8 @@
62
62
  "@unocss/eslint-plugin": "66.6.8",
63
63
  "astro": "6.1.4",
64
64
  "eslint-plugin-regexp": "3.1.0",
65
- "typescript": "6.0.2",
66
65
  "unocss": "66.6.8",
67
- "vite-plus": "0.1.16"
66
+ "vite-plus": "0.1.18"
68
67
  },
69
68
  "peerDependencies": {
70
69
  "astro": ">=6.0.0",
@@ -6,11 +6,6 @@ import Separator from "@rimelight/ui/components/separator/Separator.astro";
6
6
  import Avatar from "@rimelight/ui/components/avatar/Avatar.astro";
7
7
  import AvatarImage from "@rimelight/ui/components/avatar/AvatarImage.astro";
8
8
  import avatarAsset from "../assets/team/Daniel-Marchi_0000_00.webp";
9
- import { fetchTranslations } from "@rimelight/ui/domain";
10
-
11
- const { translationLocale, cfContext } = Astro.locals;
12
- const runtime = { cf: Astro.request.cf, cfContext };
13
- const { blog: t } = await fetchTranslations(runtime, translationLocale, ["blog"]);
14
9
 
15
10
  // Support both Astro content collections and CMS documents
16
11
  export type CMSPostDocument = {
@@ -31,6 +26,13 @@ export interface BlogPostProps extends NonNullable<Parameters<typeof blogPostVar
31
26
  class?: string;
32
27
  showImage?: boolean;
33
28
  separator?: boolean;
29
+ /** Translations for the component */
30
+ t?: {
31
+ latest?: string;
32
+ read_article?: string;
33
+ };
34
+ /** The current locale for formatting and links */
35
+ locale?: string;
34
36
  }
35
37
 
36
38
  type Props = BlogPostProps;
@@ -86,22 +88,35 @@ const blogPostVariants = scv({
86
88
  },
87
89
  });
88
90
 
89
- const { post, variant, class: className, showImage = true, separator = false } = Astro.props;
91
+ const {
92
+ post,
93
+ variant,
94
+ class: className,
95
+ showImage = true,
96
+ separator = false,
97
+ t: tProp,
98
+ locale = Astro.locals.astroI18n?.locale || "en"
99
+ } = Astro.props;
100
+
101
+ const t = {
102
+ latest: tProp?.latest || (Astro.locals.astroI18n ? Astro.locals.astroI18n.t("blog.latest") : "Latest"),
103
+ read_article: tProp?.read_article || (Astro.locals.astroI18n ? Astro.locals.astroI18n.t("blog.read_article") : "Read Article"),
104
+ };
90
105
 
91
106
  // Detect if this is a CMS document or Astro content entry
92
107
  const isCMSDocument = "_id" in post && !("id" in post);
93
108
 
94
109
  // Extract fields based on source type
95
- const postTitle = isCMSDocument ? post.title : post.data.title;
96
- const postDescription = isCMSDocument ? (post.description || post.excerpt) : post.data.description;
97
- const postImage = isCMSDocument ? post.image : post.data.heroImage;
98
- const postFeatured = isCMSDocument ? (post.featured ?? false) : post.data.featured;
110
+ const postTitle = isCMSDocument ? post.title : (post as CollectionEntry<"blog">).data.title;
111
+ const postDescription = isCMSDocument ? (post.description || post.excerpt) : (post as CollectionEntry<"blog">).data.description;
112
+ const postImage = isCMSDocument ? post.image : (post as CollectionEntry<"blog">).data.heroImage;
113
+ const postFeatured = isCMSDocument ? (post.featured ?? false) : (post as CollectionEntry<"blog">).data.featured;
99
114
 
100
115
  // Handle date formatting
101
- const rawDate = isCMSDocument ? (post.publishedDate || post._createdAt) : post.data.publishedDate;
116
+ const rawDate = isCMSDocument ? (post.publishedDate || post._createdAt) : (post as CollectionEntry<"blog">).data.publishedDate;
102
117
  const d = rawDate ? new Date(rawDate) : null;
103
118
  const formattedDate = d && !isNaN(d.getTime())
104
- ? new Intl.DateTimeFormat(translationLocale, {
119
+ ? new Intl.DateTimeFormat(locale, {
105
120
  dateStyle: "medium",
106
121
  }).format(d)
107
122
  : "";
@@ -109,13 +124,13 @@ const formattedDate = d && !isNaN(d.getTime())
109
124
  // Get slug from CMS or Astro content
110
125
  const postSlug = isCMSDocument
111
126
  ? (post.slug || "")
112
- : post.id.replace(/^(en|pt|de|es|ja)\//, "");
127
+ : (post as CollectionEntry<"blog">).id.replace(/^(en|pt|de|es|ja)\//, "");
113
128
 
114
129
  const { root, imageWrapper, image, content, title, description, footer, authorGroup, author, link, badge, featuredBadge } = blogPostVariants({ variant });
115
130
  ---
116
131
 
117
132
  <div class="flex flex-col w-full">
118
- <a href={`/${translationLocale}/blog/${postSlug}`} class:list={[root, className]}>
133
+ <a href={Astro.locals.astroI18n ? Astro.locals.astroI18n.l(`/blog/${postSlug}`) : `/${locale}/blog/${postSlug}`} class:list={[root, className]}>
119
134
  {showImage && (
120
135
  <div class={imageWrapper}>
121
136
  {postFeatured && (
@@ -178,4 +193,3 @@ const { root, imageWrapper, image, content, title, description, footer, authorGr
178
193
  </div>
179
194
  )}
180
195
  </div>
181
-
@@ -1,17 +1,21 @@
1
1
  ---
2
2
  export interface FormattedDateProps {
3
3
  date?: Date | undefined;
4
+ locale?: string;
4
5
  }
5
6
  type Props = FormattedDateProps;
6
7
 
7
- const { date } = Astro.props;
8
+ const {
9
+ date,
10
+ locale = Astro.locals.astroI18n?.locale || "en-US"
11
+ } = Astro.props;
8
12
  ---
9
13
 
10
14
  {
11
15
  date && (
12
16
  <time datetime={date.toISOString()}>
13
17
  {
14
- date.toLocaleDateString('en-us', {
18
+ date.toLocaleDateString(locale, {
15
19
  year: 'numeric',
16
20
  month: 'short',
17
21
  day: 'numeric',
@@ -1,6 +1,7 @@
1
1
  ---
2
2
  import { ClientRouter } from 'astro:transitions';
3
3
  import type { SiteConfig } from '@/config';
4
+ import UApp from './nuxt/App.vue';
4
5
 
5
6
  export interface HeadProps {
6
7
  /** Page title */
@@ -147,4 +148,6 @@ const finalCanonical = canonical || (!is404 ? Astro.url.href : undefined);
147
148
 
148
149
  {transitions && <ClientRouter />}
149
150
 
150
- <slot />
151
+ <UApp client:load>
152
+ <slot />
153
+ </UApp>
@@ -6,11 +6,6 @@ import Separator from "@rimelight/ui/components/separator/Separator.astro";
6
6
  import Avatar from "@rimelight/ui/components/avatar/Avatar.astro";
7
7
  import AvatarImage from "@rimelight/ui/components/avatar/AvatarImage.astro";
8
8
  import avatarAsset from "../assets/team/Daniel-Marchi_0000_00.webp";
9
- import { fetchTranslations } from "@rimelight/ui/domain";
10
-
11
- const { translationLocale, cfContext } = Astro.locals;
12
- const runtime = { cf: Astro.request.cf, cfContext };
13
- const { projects: t } = await fetchTranslations(runtime, translationLocale, ["projects"]);
14
9
 
15
10
  // Support both Astro content collections and CMS documents
16
11
  export type CMSProjectDocument = {
@@ -31,6 +26,13 @@ export interface ProjectPostProps extends NonNullable<Parameters<typeof projectP
31
26
  class?: string;
32
27
  showImage?: boolean;
33
28
  separator?: boolean;
29
+ /** Translations for the component */
30
+ t?: {
31
+ latest_badge?: string;
32
+ view_project?: string;
33
+ };
34
+ /** The current locale for formatting and links */
35
+ locale?: string;
34
36
  }
35
37
 
36
38
  type Props = ProjectPostProps;
@@ -86,23 +88,36 @@ const projectPostVariants = scv({
86
88
  },
87
89
  });
88
90
 
89
- const { project, variant, class: className, showImage = true, separator = false } = Astro.props;
91
+ const {
92
+ project,
93
+ variant,
94
+ class: className,
95
+ showImage = true,
96
+ separator = false,
97
+ t: tProp,
98
+ locale = Astro.locals.astroI18n?.locale || "en"
99
+ } = Astro.props;
100
+
101
+ const t = {
102
+ latest_badge: tProp?.latest_badge || (Astro.locals.astroI18n ? Astro.locals.astroI18n.t("projects.latest_badge") : "Latest"),
103
+ view_project: tProp?.view_project || (Astro.locals.astroI18n ? Astro.locals.astroI18n.t("projects.view_project") : "View Project"),
104
+ };
90
105
 
91
106
  // Detect if this is a CMS document or Astro content entry
92
107
  const isCMSDocument = "_id" in project && !("id" in project);
93
108
 
94
109
  // Extract fields based on source type
95
- const projectTitle = isCMSDocument ? project.title : project.data.title;
96
- const projectDescription = isCMSDocument ? project.description : project.data.description;
97
- const projectImage = isCMSDocument ? project.heroImage : project.data.heroImage;
98
- const projectFeatured = isCMSDocument ? (project.featured ?? false) : project.data.featured;
99
- const projectTags = isCMSDocument ? (project.tags || []) : (project.data.tags || []);
110
+ const projectTitle = isCMSDocument ? project.title : (project as CollectionEntry<"projects">).data.title;
111
+ const projectDescription = isCMSDocument ? project.description : (project as CollectionEntry<"projects">).data.description;
112
+ const projectImage = isCMSDocument ? project.heroImage : (project as CollectionEntry<"projects">).data.heroImage;
113
+ const projectFeatured = isCMSDocument ? (project.featured ?? false) : (project as CollectionEntry<"projects">).data.featured;
114
+ const projectTags = isCMSDocument ? (project.tags || []) : ((project as CollectionEntry<"projects">).data.tags || []);
100
115
 
101
116
  // Handle date formatting
102
- const rawDate = isCMSDocument ? (project.publishedDate || project._createdAt) : project.data.publishedDate;
117
+ const rawDate = isCMSDocument ? (project.publishedDate || project._createdAt) : (project as CollectionEntry<"projects">).data.publishedDate;
103
118
  const d = rawDate ? new Date(rawDate) : null;
104
119
  const formattedDate = d && !isNaN(d.getTime())
105
- ? new Intl.DateTimeFormat(translationLocale, {
120
+ ? new Intl.DateTimeFormat(locale, {
106
121
  dateStyle: "medium",
107
122
  }).format(d)
108
123
  : "";
@@ -110,13 +125,13 @@ const formattedDate = d && !isNaN(d.getTime())
110
125
  // Get slug from CMS or Astro content
111
126
  const projectSlug = isCMSDocument
112
127
  ? (project.slug || "")
113
- : project.id.replace(/^(en|pt|de|es|ja)\//, "");
128
+ : (project as CollectionEntry<"projects">).id.replace(/^(en|pt|de|es|ja)\//, "");
114
129
 
115
130
  const { root, imageWrapper, image, content, title, description, footer, authorGroup, author, link, badge, featuredBadge } = projectPostVariants({ variant });
116
131
  ---
117
132
 
118
133
  <div class="flex flex-col w-full">
119
- <a href={`/${translationLocale}/projects/${projectSlug}`} class:list={[root, className]}>
134
+ <a href={Astro.locals.astroI18n ? Astro.locals.astroI18n.l(`/projects/${projectSlug}`) : `/${locale}/projects/${projectSlug}`} class:list={[root, className]}>
120
135
  {showImage && (
121
136
  <div class={imageWrapper}>
122
137
  {projectFeatured && (
@@ -180,4 +195,3 @@ const { root, imageWrapper, image, content, title, description, footer, authorGr
180
195
  </div>
181
196
  )}
182
197
  </div>
183
-
@@ -1,75 +1,75 @@
1
- ---
2
- import { scv, cx } from "css-variants";
3
- import { twMerge } from "tailwind-merge";
4
- import defu from "defu";
5
- import { headerTheme } from "@/themes/header.theme.ts";
6
- import { getUIConfig } from "virtual:rimelight-ui";
7
-
8
- const header = scv({
9
- slots: [...headerTheme.slots],
10
- base: headerTheme.base,
11
- variants: headerTheme.variants,
12
- defaultVariants: headerTheme.defaultVariants,
13
- classNameResolver: (...args) => twMerge(cx(...args)),
14
- });
15
-
16
- export interface RLAHeaderProps {
17
- /**
18
- * Whether to contain the header content in a max-width container.
19
- * @default true
20
- */
21
- contain?: boolean;
22
- /**
23
- * Whether the header should be sticky at the top.
24
- * @default true
25
- */
26
- sticky?: boolean;
27
- /**
28
- * UI overrides for individual slots, derived from the header theme.
29
- */
30
- ui?: Partial<Record<keyof typeof headerTheme.base, string>>;
31
- /**
32
- * External class — applied to the root element.
33
- */
34
- class?: string;
35
- /**
36
- * Standard HTML attributes for <header>
37
- */
38
- [key: string]: unknown;
39
- }
40
-
41
- const {
42
- contain = true,
43
- sticky = true,
44
- class: className,
45
- ui: uiProp,
46
- ...rest
47
- } = Astro.props as RLAHeaderProps
48
-
49
- const globalConfig = getUIConfig();
50
-
51
- const mergedUI = defu(
52
- uiProp ?? {},
53
- (globalConfig.header as Record<string, unknown>) ?? {},
54
- ) as Record<keyof typeof headerTheme.base, string | undefined>;
55
-
56
- const { root, container, left, center, right } = header({ contain, sticky });
57
- ---
58
-
59
- <header
60
- class:list={[twMerge(root, mergedUI.root, className)]}
61
- data-slot="rla-header"
62
- {...rest}
63
- >
64
- <div class:list={[twMerge(container, mergedUI.container)]}>
65
- <div class:list={[twMerge(left, mergedUI.left)]} data-slot="header-left">
66
- <slot name="left" />
67
- </div>
68
- <div class:list={[twMerge(center, mergedUI.center)]} data-slot="header-center">
69
- <slot name="center" />
70
- </div>
71
- <div class:list={[twMerge(right, mergedUI.right)]} data-slot="header-right">
72
- <slot name="right" />
73
- </div>
74
- </div>
75
- </header>
1
+ ---
2
+ import { scv, cx } from "css-variants";
3
+ import { twMerge } from "tailwind-merge";
4
+ import defu from "defu";
5
+ import { headerTheme } from "@/themes/header.theme.ts";
6
+ import { getUIConfig } from "virtual:rimelight-ui";
7
+
8
+ const header = scv({
9
+ slots: [...headerTheme.slots],
10
+ base: headerTheme.base,
11
+ variants: headerTheme.variants,
12
+ defaultVariants: headerTheme.defaultVariants,
13
+ classNameResolver: (...args) => twMerge(cx(...args)),
14
+ });
15
+
16
+ export interface RLAHeaderProps {
17
+ /**
18
+ * Whether to contain the header content in a max-width container.
19
+ * @default true
20
+ */
21
+ contain?: boolean;
22
+ /**
23
+ * Whether the header should be sticky at the top.
24
+ * @default true
25
+ */
26
+ sticky?: boolean;
27
+ /**
28
+ * UI overrides for individual slots, derived from the header theme.
29
+ */
30
+ ui?: Partial<Record<keyof typeof headerTheme.base, string>>;
31
+ /**
32
+ * External class — applied to the root element.
33
+ */
34
+ class?: string;
35
+ /**
36
+ * Standard HTML attributes for <header>
37
+ */
38
+ [key: string]: unknown;
39
+ }
40
+
41
+ const {
42
+ contain = true,
43
+ sticky = true,
44
+ class: className,
45
+ ui: uiProp,
46
+ ...rest
47
+ } = Astro.props as RLAHeaderProps
48
+
49
+ const globalConfig = getUIConfig();
50
+
51
+ const mergedUI = defu(
52
+ uiProp ?? {},
53
+ (globalConfig.header as Record<string, unknown>) ?? {},
54
+ ) as Record<keyof typeof headerTheme.base, string | undefined>;
55
+
56
+ const { root, container, left, center, right } = header({ contain, sticky });
57
+ ---
58
+
59
+ <header
60
+ class:list={[twMerge(root, mergedUI.root, className)]}
61
+ data-slot="rla-header"
62
+ {...rest}
63
+ >
64
+ <div class:list={[twMerge(container, mergedUI.container)]}>
65
+ <div class:list={[twMerge(left, mergedUI.left)]} data-slot="header-left">
66
+ <slot name="left" />
67
+ </div>
68
+ <div class:list={[twMerge(center, mergedUI.center)]} data-slot="header-center">
69
+ <slot name="center" />
70
+ </div>
71
+ <div class:list={[twMerge(right, mergedUI.right)]} data-slot="header-right">
72
+ <slot name="right" />
73
+ </div>
74
+ </div>
75
+ </header>
@@ -0,0 +1 @@
1
+ export * from "@nuxt/ui"
@@ -13,8 +13,8 @@ import type { DefaultUIConfig } from "@/themes"
13
13
  * @param uiProp - The local `ui` prop from defineProps
14
14
  * @returns ComputedRef of merged UI overrides
15
15
  */
16
- export function useUi<T extends keyof DefaultUIConfig>(
17
- componentName: T,
16
+ export function useUi(
17
+ componentName: keyof DefaultUIConfig,
18
18
  uiProp: Record<string, unknown> | undefined
19
19
  ): ComputedRef<Record<string, unknown>> {
20
20
  const globalConfig = getUIConfig()
package/src/env.d.ts CHANGED
@@ -1,16 +1,13 @@
1
- /// <reference types="astro/client" />
2
-
3
- declare module "virtual:rimelight-ui" {
4
- import type { DefaultUIConfig } from "../themes"
5
-
6
- export function getUIConfig(): DefaultUIConfig
7
- }
8
-
9
- declare namespace App {
10
- interface Locals {
11
- uiLocale?: string
12
- translationLocale?: string
13
- isMissingContent?: boolean
14
- cfContext?: Record<string, unknown>
15
- }
16
- }
1
+ /// <reference types="astro/client" />
2
+
3
+ declare module "virtual:rimelight-ui" {
4
+ import type { DefaultUIConfig } from "../themes"
5
+
6
+ export function getUIConfig(): DefaultUIConfig
7
+ }
8
+
9
+ declare namespace App {
10
+ interface Locals {
11
+ cfContext?: Record<string, unknown>
12
+ }
13
+ }
@@ -1,87 +1,106 @@
1
- import type { AstroIntegration } from "astro"
2
- import defu from "defu"
3
- import Icons from "unplugin-icons/vite"
4
- import { defaultUIConfig } from "../themes"
5
-
6
- /**
7
- * Shape of the `ui` config that can be passed to the integration. Keys are component names, values
8
- * are partial theme configs.
9
- */
10
- export type UIConfigOverrides = {
11
- button?: Partial<(typeof defaultUIConfig)["button"]>
12
- header?: Partial<(typeof defaultUIConfig)["header"]>
13
- }
14
-
15
- export interface UIOptions {
16
- /**
17
- * Global UI overrides applied to all components. Merged with defaults via `defu` (first arg
18
- * wins).
19
- */
20
- ui?: UIConfigOverrides
21
- }
22
-
23
- /**
24
- * Astro integration for `@rimelight/ui`.
25
- *
26
- * Provides global UI configuration via a virtual module. Components import `virtual:rimelight-ui`
27
- * to access the merged config.
28
- *
29
- * @example
30
- * // astro.config.mjs
31
- * import { ui } from "@rimelight/ui/integrations"
32
- *
33
- * export default defineConfig({
34
- * integrations: [
35
- * ui({
36
- * ui: {
37
- * button: {
38
- * defaultVariants: { variant: "primary" }
39
- * }
40
- * }
41
- * })
42
- * ]
43
- * })
44
- */
45
- export function ui(options: UIOptions = {}): AstroIntegration {
46
- // Merge user overrides with defaults (user-provided values win)
47
- const resolvedConfig = defu(options.ui ?? {}, defaultUIConfig)
48
-
49
- const virtualModuleId = "virtual:rimelight-ui"
50
- const resolvedVirtualModuleId = "\0" + virtualModuleId
51
-
52
- return {
53
- name: "@rimelight/ui",
54
- hooks: {
55
- "astro:config:setup": ({ updateConfig, logger }) => {
56
- logger.info("Initializing @rimelight/ui configuration")
57
-
58
- updateConfig({
59
- vite: {
60
- plugins: [
61
- Icons({
62
- compiler: "astro",
63
- autoInstall: true
64
- }),
65
- {
66
- name: "vite-plugin-rimelight-ui-config",
67
- enforce: "pre",
68
- resolveId(id: string) {
69
- if (id === virtualModuleId) {
70
- return resolvedVirtualModuleId
71
- }
72
- return null
73
- },
74
- load(id: string) {
75
- if (id === resolvedVirtualModuleId) {
76
- return `export const getUIConfig = () => (${JSON.stringify(resolvedConfig)});`
77
- }
78
- return null
79
- }
80
- }
81
- ]
82
- }
83
- })
84
- }
85
- }
86
- }
87
- }
1
+ import path from "node:path"
2
+ import { fileURLToPath } from "node:url"
3
+ import type { AstroIntegration } from "astro"
4
+ import vue from "@astrojs/vue"
5
+ import uiPlugin from "@nuxt/ui/vite"
6
+ import defu from "defu"
7
+ import Icons from "unplugin-icons/vite"
8
+ import { defaultUIConfig } from "../themes"
9
+
10
+ const nuxtUIEntry = fileURLToPath(import.meta.resolve("@nuxt/ui"))
11
+ const nuxtUIComponentsPath = path.join(path.dirname(nuxtUIEntry), "runtime/components")
12
+
13
+ /**
14
+ * Shape of the `ui` config that can be passed to the integration.
15
+ */
16
+ export type UIConfigOverrides = {
17
+ button?: Partial<(typeof defaultUIConfig)["button"]>
18
+ header?: Partial<(typeof defaultUIConfig)["header"]>
19
+ }
20
+
21
+ export interface UIOptions {
22
+ /**
23
+ * Global UI overrides applied to all components. Merged with defaults via `defu` (first arg
24
+ * wins).
25
+ */
26
+ ui?: UIConfigOverrides
27
+ }
28
+
29
+ /**
30
+ * Astro integration for `@rimelight/ui`.
31
+ *
32
+ * Provides global UI configuration via a virtual module. Components import `virtual:rimelight-ui`
33
+ * to access the merged config.
34
+ *
35
+ * @example
36
+ * // astro.config.mjs
37
+ * import { ui } from "@rimelight/ui/integrations"
38
+ *
39
+ * export default defineConfig({
40
+ * integrations: [
41
+ * ui({
42
+ * ui: {
43
+ * button: {
44
+ * defaultVariants: { variant: "primary" }
45
+ * }
46
+ * }
47
+ * })
48
+ * ]
49
+ * })
50
+ */
51
+ export function ui(options: UIOptions = {}): AstroIntegration[] {
52
+ const resolvedConfig = defu(options.ui ?? {}, defaultUIConfig)
53
+
54
+ const virtualModuleId = "virtual:rimelight-ui"
55
+ const resolvedVirtualModuleId = "\0" + virtualModuleId
56
+
57
+ const rimelightUIIntegration: AstroIntegration = {
58
+ name: "@rimelight/ui",
59
+ hooks: {
60
+ "astro:config:setup": ({ updateConfig, logger }) => {
61
+ logger.info("Initializing @rimelight/ui core")
62
+
63
+ updateConfig({
64
+ vite: {
65
+ resolve: {
66
+ alias: {
67
+ "@rimelight/ui/components/nuxt/": `${nuxtUIComponentsPath}/`
68
+ }
69
+ },
70
+ plugins: [
71
+ uiPlugin({
72
+ router: false,
73
+ scanPackages: ["@rimelight/ui"]
74
+ }),
75
+ Icons({
76
+ compiler: "astro",
77
+ autoInstall: true
78
+ }),
79
+ {
80
+ name: "vite-plugin-rimelight-ui-config",
81
+ enforce: "pre",
82
+ resolveId(id: string) {
83
+ if (id === virtualModuleId) return resolvedVirtualModuleId
84
+ return null
85
+ },
86
+ load(id: string) {
87
+ if (id === resolvedVirtualModuleId) {
88
+ return `export const getUIConfig = () => (${JSON.stringify(resolvedConfig)});`
89
+ }
90
+ return null
91
+ }
92
+ }
93
+ ]
94
+ }
95
+ })
96
+ }
97
+ }
98
+ }
99
+
100
+ return [
101
+ vue({
102
+ appEntrypoint: "@rimelight/ui/integrations/vue-entrypoint"
103
+ }),
104
+ rimelightUIIntegration
105
+ ]
106
+ }
@@ -0,0 +1,6 @@
1
+ import type { App } from "vue"
2
+ import ui from "@nuxt/ui/vue-plugin"
3
+
4
+ export default (app: App) => {
5
+ app.use(ui)
6
+ }
@@ -59,14 +59,14 @@ export const sri = defineMiddleware(async (_, next) => {
59
59
  const scriptRegex = new RegExp(`(<script[^>]+src=["']${url}["'][^>]*)(/?>)`, "g")
60
60
  modifiedHtml = modifiedHtml.replace(
61
61
  scriptRegex,
62
- `$1 integrity="${String(hash)}" crossorigin="anonymous"$2`
62
+ `$1 integrity="${hash}" crossorigin="anonymous"$2`
63
63
  )
64
64
 
65
65
  // Look for link tags with the specific URL
66
66
  const linkRegex = new RegExp(`(<link[^>]+href=["']${url}["'][^>]*)(/?>)`, "g")
67
67
  modifiedHtml = modifiedHtml.replace(
68
68
  linkRegex,
69
- `$1 integrity="${String(hash)}" crossorigin="anonymous"$2`
69
+ `$1 integrity="${hash}" crossorigin="anonymous"$2`
70
70
  )
71
71
  }
72
72
 
@@ -0,0 +1,2 @@
1
+ @import "tailwindcss";
2
+ @import "@nuxt/ui";
@@ -1,3 +1 @@
1
- export * from "./parse-boolean"
2
- export * from "./deep-merge"
3
1
  export * from "./embed"
@@ -1,128 +0,0 @@
1
- ---
2
- /**
3
- * Language Switcher Component (Extension)
4
- *
5
- * Variants:
6
- * - 'full': Shows language icon, current flag, name, and chevron
7
- * - 'label' (default): Shows language icon and current flag only
8
- *
9
- * Uses Starwind components for consistency.
10
- * Built using native Astro i18n features.
11
- */
12
-
13
- import Dropdown from "@rimelight/ui/components/dropdown/Dropdown.astro";
14
- import DropdownTrigger from "@rimelight/ui/components/dropdown/DropdownTrigger.astro";
15
- import DropdownContent from "@rimelight/ui/components/dropdown/DropdownContent.astro";
16
- import DropdownItem from "@rimelight/ui/components/dropdown/DropdownItem.astro";
17
- import Button from "@rimelight/ui/components/button/Button.astro";
18
- import Flag from "@rimelight/ui/components/icons/Flag.astro";
19
-
20
- import { SUPPORTED_LOCALES, LANGUAGES } from "@rimelight/ui/domain";
21
- import { localeToFlagCode } from "@rimelight/ui/domain";
22
- import { replaceLocaleInPath } from "@rimelight/ui/domain";
23
- import type { Locale } from "@rimelight/ui/domain";
24
-
25
- export interface LanguageSwitcherProps {
26
- variant?: 'full' | 'label';
27
- class?: string;
28
- }
29
- type Props = LanguageSwitcherProps;
30
-
31
- const { variant = 'label', class: className } = Astro.props;
32
- const { uiLocale: currentLocale } = Astro.locals;
33
- const currentPath = Astro.url.pathname;
34
-
35
- /**
36
- * Utility to compute the target URL for a locale switch
37
- * Handles standard Astro routing by replacing the locale prefix in the URL.
38
- */
39
- function getSwitchUrl(targetLocale: string) {
40
- return replaceLocaleInPath(currentPath, currentLocale as string, targetLocale);
41
- }
42
-
43
- const currentFlagCode = localeToFlagCode(currentLocale as Locale);
44
- const currentLabel = LANGUAGES[(currentLocale as Locale) as keyof typeof LANGUAGES] || currentLocale;
45
- ---
46
-
47
- <Dropdown class={className}>
48
- <DropdownTrigger asChild>
49
- <Button
50
- variant="outline"
51
- size="sm"
52
- class:list={[
53
- "rounded-full border-border/50 bg-background/50 hover:bg-background transition-all h-9 shadow-none",
54
- variant === 'full' ? "px-3xs" : "px-5xs",
55
- ]}
56
- aria-label="Select Language"
57
- >
58
- <div class="flex items-center gap-5xs">
59
- <div class="flex items-center justify-center shrink-0 opacity-70 size-4.5">
60
- <svg
61
- xmlns="http://www.w3.org/2000/svg"
62
- class="text-current size-4.5"
63
- viewBox="0 0 24 24"
64
- fill="none"
65
- stroke="currentColor"
66
- stroke-width="2"
67
- stroke-linecap="round"
68
- stroke-linejoin="round"
69
- >
70
- <path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M4 5h7" /><path d="M9 3v2c0 4.418 -2.239 8 -5 8" /><path d="M5 9c0 2.144 2.952 3.908 6.7 4" /><path d="M12 20l4 -9l4 9" /><path d="M19.1 18h-6.2" />
71
- </svg>
72
- </div>
73
-
74
- <div class="flex items-center -translate-y-[0.5px]">
75
- <Flag code={currentFlagCode} size={18} class="rounded-sm shadow-sm border border-border/20" />
76
- </div>
77
-
78
- {variant === 'full' && (
79
- <div class="flex items-center gap-5xs border-l border-border/30 pl-5xs ml-3xs">
80
- <span class="text-xs font-medium text-muted-foreground whitespace-nowrap leading-none flex items-center translate-y-[0.5px]">
81
- {currentLabel}
82
- </span>
83
- <div class="flex items-center justify-center opacity-50 -translate-y-[0.5px]">
84
- <svg class="size-3.5" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
85
- <path fill-rule="evenodd" d="M5.23 7.21a.75.75 0 011.06.02L10 11.168l3.71-3.938a.75.75 0 111.08 1.04l-4.25 4.5a.75.75 0 01-1.08 0l-4.25-4.5a.75.75 0 01.02-1.06z" clip-rule="evenodd" />
86
- </svg>
87
- </div>
88
- </div>
89
- )}
90
- </div>
91
- </Button>
92
- </DropdownTrigger>
93
-
94
- <DropdownContent align="end" class="w-6xl border-border/50 bg-background/95 backdrop-blur-md p-5xs">
95
- {SUPPORTED_LOCALES.map((locale) => {
96
- const isActive = locale === currentLocale;
97
- const href = getSwitchUrl(locale);
98
- const flagCode = localeToFlagCode(locale as Locale);
99
- const label = LANGUAGES[(locale as Locale) as keyof typeof LANGUAGES] || locale;
100
-
101
- return (
102
- <DropdownItem
103
- as="a"
104
- href={href}
105
- class:list={[
106
- "flex items-center h-10 px-3xs gap-3xs transition-colors rounded-lg",
107
- isActive ? "bg-accent/70 text-foreground font-semibold" : "text-muted-foreground hover:text-foreground"
108
- ]}
109
- >
110
- <div class="flex items-center shrink-0 -translate-y-[0.5px]">
111
- <Flag code={flagCode} size={18} class="rounded-sm shadow-sm border border-border/20" />
112
- </div>
113
- <span class="text-sm leading-none flex items-center translate-y-[0.5px]">{label}</span>
114
- {isActive && (
115
- <div class="ml-auto flex items-center justify-center h-full text-primary">
116
- <svg class="size-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
117
- <path fill-rule="evenodd" d="M16.704 4.153a.75.75 0 01.143 1.052l-8 10.5a.75.75 0 01-1.127.075l-4.5-4.5a.75.75 0 011.06-1.06l3.894 3.893 7.48-9.817a.75.75 0 011.05-.143z" clip-rule="evenodd" />
118
- </svg>
119
- </div>
120
- )}
121
- </DropdownItem>
122
- );
123
- })}
124
-
125
- </DropdownContent>
126
- </Dropdown>
127
-
128
-
@@ -1,39 +0,0 @@
1
- ---
2
- import { checkMissingTranslation } from '../domain/i18n/resolve-locale'
3
- import { fetchTranslations } from '../domain/i18n/fetcher'
4
-
5
- export interface MissingTranslationBannerProps {}
6
- type Props = MissingTranslationBannerProps;
7
-
8
- const missingType = checkMissingTranslation(
9
- Astro.locals.uiLocale,
10
- Astro.locals.isMissingContent
11
- )
12
-
13
- let bannerText: string | null = null
14
-
15
- if (missingType) {
16
- const { cfContext, translationLocale } = Astro.locals
17
- const runtime = { cf: Astro.request.cf, cfContext }
18
- const { messages } = await fetchTranslations(
19
- runtime,
20
- translationLocale,
21
- ['messages']
22
- )
23
-
24
- bannerText =
25
- missingType === 'content'
26
- ? messages.errors.ui.MISSING_TRANSLATED_CONTENT ||
27
- 'Sorry, this article is not yet available in your selected language.'
28
- : messages.errors.ui.MISSING_TRANSLATED_UI ||
29
- 'Sorry, this page is not yet fully available in your selected language.'
30
- }
31
- ---
32
-
33
- {
34
- bannerText && (
35
- <div class='rounded px-4 py-2 mb-8 max-w-160 mx-auto border border-amber-300 bg-amber-50 text-amber-900 dark:border-amber-600 dark:bg-amber-900/20 dark:text-amber-600 text-center'>
36
- {bannerText}
37
- </div>
38
- )
39
- }
@@ -1,36 +0,0 @@
1
- function isObject(value: unknown): value is Record<string, unknown> {
2
- return typeof value === "object" && value !== null && !Array.isArray(value)
3
- }
4
-
5
- /**
6
- * Deep-merge two dictionaries. - `base` provides known structure (fallback) - `overrides` can
7
- * provide partial values - Both types are preserved and merged correctly
8
- */
9
- function assertRecord(obj: unknown): asserts obj is Record<string, unknown> {
10
- if (!obj || typeof obj !== "object") throw new Error("Expected object")
11
- }
12
-
13
- function assertA<T>(obj: unknown): asserts obj is T {
14
- if (!obj) throw new Error("Expected result")
15
- }
16
-
17
- export function deepMerge<A extends object, B extends Partial<A>>(base: A, overrides: B): A {
18
- const result = { ...base }
19
- assertRecord(result)
20
-
21
- for (const key of Object.keys(overrides)) {
22
- const overrideValue = Reflect.get(overrides, key)
23
- const baseValue = Reflect.get(base, key)
24
-
25
- if (overrideValue === undefined) continue
26
-
27
- if (isObject(baseValue) && isObject(overrideValue)) {
28
- Reflect.set(result, key, deepMerge(baseValue as object, overrideValue as Partial<object>))
29
- } else {
30
- Reflect.set(result, key, overrideValue)
31
- }
32
- }
33
-
34
- assertA<A>(result)
35
- return result
36
- }
@@ -1,14 +0,0 @@
1
- /**
2
- * Parse a boolean flag from Wrangler-env string.
3
- *
4
- * Accepts: "1", "true", "on" → true "0", "false", "off" → false undefined → defaultValue
5
- */
6
- export function parseBooleanFlag(value: string | undefined, defaultValue: boolean): boolean {
7
- if (value === undefined) return defaultValue
8
-
9
- const v = value.toLowerCase()
10
- if (v === "1" || v === "true" || v === "on") return true
11
- if (v === "0" || v === "false" || v === "off") return false
12
-
13
- return defaultValue
14
- }