@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 +21 -21
- package/package.json +7 -8
- package/src/components/BlogPost.astro +29 -15
- package/src/components/FormattedDate.astro +6 -2
- package/src/components/Head.astro +4 -1
- package/src/components/ProjectPost.astro +30 -16
- package/src/components/astro/RLAHeader.astro +75 -75
- package/src/components/nuxt/index.ts +1 -0
- package/src/composables/useUi.ts +2 -2
- package/src/env.d.ts +13 -16
- package/src/integrations/ui.ts +106 -87
- package/src/integrations/vue-entrypoint.ts +6 -0
- package/src/middleware/sri.ts +2 -2
- package/src/styles/index.css +2 -0
- package/src/utils/index.ts +0 -2
- package/src/components/LanguageSwitcher.astro +0 -128
- package/src/components/MissingTranslationBanner.astro +0 -39
- package/src/utils/deep-merge.ts +0 -36
- package/src/utils/parse-boolean.ts +0 -14
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.
|
|
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.
|
|
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 {
|
|
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(
|
|
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={`/${
|
|
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 {
|
|
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(
|
|
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
|
-
<
|
|
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 {
|
|
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(
|
|
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={`/${
|
|
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"
|
package/src/composables/useUi.ts
CHANGED
|
@@ -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
|
|
17
|
-
componentName:
|
|
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
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
|
+
}
|
package/src/integrations/ui.ts
CHANGED
|
@@ -1,87 +1,106 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
3
|
-
import
|
|
4
|
-
import
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
+
}
|
package/src/middleware/sri.ts
CHANGED
|
@@ -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="${
|
|
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="${
|
|
69
|
+
`$1 integrity="${hash}" crossorigin="anonymous"$2`
|
|
70
70
|
)
|
|
71
71
|
}
|
|
72
72
|
|
package/src/utils/index.ts
CHANGED
|
@@ -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
|
-
}
|
package/src/utils/deep-merge.ts
DELETED
|
@@ -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
|
-
}
|