@rimelight/ui 0.0.3 → 0.0.5
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 +25 -5
- package/src/components/astro/RLAButton.astro +95 -0
- package/src/components/vue/RLVButton.vue +77 -0
- package/src/composables/index.ts +1 -0
- package/src/composables/useComponentUI.ts +27 -0
- package/src/config/site.config.ts +5 -23
- package/src/domain/i18n/fetcher.ts +14 -4
- package/src/domain/i18n/middleware/i18n.ts +2 -1
- package/src/domain/seo/constants.ts +2 -16
- package/src/domain/seo/services/llms.ts +13 -10
- package/src/domain/seo/utils.ts +3 -4
- package/src/env.d.ts +9 -42
- package/src/integrations/index.ts +2 -0
- package/src/integrations/sri.ts +2 -2
- package/src/integrations/ui.ts +81 -0
- package/src/middleware/index.ts +1 -0
- package/src/middleware/security.ts +15 -0
- package/src/middleware/sri.ts +4 -2
- package/src/themes/button.theme.ts +45 -0
- package/src/themes/index.ts +11 -0
- package/worker-configuration.d.ts +0 -14791
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rimelight/ui",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.5",
|
|
4
4
|
"description": "Rimelight UI Library.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Daniel Marchi",
|
|
@@ -13,7 +13,14 @@
|
|
|
13
13
|
],
|
|
14
14
|
"type": "module",
|
|
15
15
|
"exports": {
|
|
16
|
+
".": "./src/integrations/index.ts",
|
|
16
17
|
"./components/*": "./src/components/*",
|
|
18
|
+
"./components/astro/*": "./src/components/astro/*",
|
|
19
|
+
"./components/vue/*": "./src/components/vue/*",
|
|
20
|
+
"./themes": "./src/themes/index.ts",
|
|
21
|
+
"./themes/*": "./src/themes/*",
|
|
22
|
+
"./composables": "./src/composables/index.ts",
|
|
23
|
+
"./composables/*": "./src/composables/*",
|
|
17
24
|
"./config": "./src/config/index.ts",
|
|
18
25
|
"./config/*": "./src/config/*",
|
|
19
26
|
"./domain": "./src/domain/index.ts",
|
|
@@ -33,18 +40,31 @@
|
|
|
33
40
|
},
|
|
34
41
|
"dependencies": {
|
|
35
42
|
"css-variants": "2.3.5",
|
|
36
|
-
"
|
|
43
|
+
"defu": "6.1.6",
|
|
44
|
+
"embla-carousel": "8.6.0",
|
|
45
|
+
"tailwind-merge": "^3.3.1"
|
|
37
46
|
},
|
|
38
47
|
"devDependencies": {
|
|
39
48
|
"@astrojs/check": "0.9.8",
|
|
49
|
+
"@astrojs/ts-plugin": "1.10.7",
|
|
50
|
+
"@e18e/eslint-plugin": "0.3.0",
|
|
40
51
|
"@types/node": "25.5.2",
|
|
52
|
+
"@unocss/eslint-plugin": "66.6.7",
|
|
41
53
|
"astro": "6.1.4",
|
|
54
|
+
"eslint-plugin-regexp": "3.1.0",
|
|
42
55
|
"typescript": "6.0.2",
|
|
43
|
-
"
|
|
44
|
-
"
|
|
56
|
+
"unocss": "66.6.7",
|
|
57
|
+
"unplugin-icons": "23.0.1",
|
|
58
|
+
"vite-plus": "0.1.15"
|
|
45
59
|
},
|
|
46
60
|
"peerDependencies": {
|
|
47
|
-
"astro": ">=6.0.0"
|
|
61
|
+
"astro": ">=6.0.0",
|
|
62
|
+
"vue": ">=3.4.0"
|
|
63
|
+
},
|
|
64
|
+
"peerDependenciesMeta": {
|
|
65
|
+
"vue": {
|
|
66
|
+
"optional": true
|
|
67
|
+
}
|
|
48
68
|
},
|
|
49
69
|
"scripts": {
|
|
50
70
|
"generate:config": "vp config",
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
---
|
|
2
|
+
import type { HTMLAttributes } from "astro/types";
|
|
3
|
+
import { cv, cx } from "css-variants";
|
|
4
|
+
import { twMerge } from "tailwind-merge";
|
|
5
|
+
import defu from "defu";
|
|
6
|
+
import { buttonTheme } from "../../themes/button.theme";
|
|
7
|
+
import { getUIConfig } from "virtual:rimelight-ui";
|
|
8
|
+
|
|
9
|
+
// Create the variant resolver with tailwind-merge for conflict resolution
|
|
10
|
+
const button = cv({
|
|
11
|
+
...buttonTheme,
|
|
12
|
+
classNameResolver: (...args) => twMerge(cx(...args)),
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* UI prop interface for overriding the base class.
|
|
17
|
+
* For multi-slot components (scv), this would have keys per slot.
|
|
18
|
+
*/
|
|
19
|
+
export interface RLAButtonUI {
|
|
20
|
+
base?: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Button props — uses indexed access types on the theme config
|
|
25
|
+
* (same pattern as Nuxt UI's Button['variants']['color'])
|
|
26
|
+
*/
|
|
27
|
+
export interface RLAButtonProps {
|
|
28
|
+
/**
|
|
29
|
+
* Button variant style.
|
|
30
|
+
* @default "default"
|
|
31
|
+
*/
|
|
32
|
+
variant?: keyof typeof buttonTheme.variants.variant;
|
|
33
|
+
/**
|
|
34
|
+
* Button size.
|
|
35
|
+
* @default "md"
|
|
36
|
+
*/
|
|
37
|
+
size?: keyof typeof buttonTheme.variants.size;
|
|
38
|
+
/**
|
|
39
|
+
* Per-instance UI override. Merged with global config via `defu`.
|
|
40
|
+
* Higher priority than integration-level config.
|
|
41
|
+
*/
|
|
42
|
+
ui?: RLAButtonUI;
|
|
43
|
+
/**
|
|
44
|
+
* External class — applied after all internal classes.
|
|
45
|
+
*/
|
|
46
|
+
class?: string;
|
|
47
|
+
/**
|
|
48
|
+
* When provided, renders as <a> instead of <button>.
|
|
49
|
+
*/
|
|
50
|
+
href?: string;
|
|
51
|
+
/**
|
|
52
|
+
* Standard HTML attributes for <button>
|
|
53
|
+
*/
|
|
54
|
+
[key: string]: unknown;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
type Props = RLAButtonProps;
|
|
58
|
+
|
|
59
|
+
const {
|
|
60
|
+
variant,
|
|
61
|
+
size,
|
|
62
|
+
class: className,
|
|
63
|
+
ui: uiProp,
|
|
64
|
+
href,
|
|
65
|
+
...rest
|
|
66
|
+
} = Astro.props;
|
|
67
|
+
|
|
68
|
+
// Get global config from the virtual module (populated by the Astro integration)
|
|
69
|
+
const globalConfig = getUIConfig();
|
|
70
|
+
|
|
71
|
+
// Merge: uiProp > globalConfig.button (uiProp wins)
|
|
72
|
+
const mergedUI = defu<Record<string, unknown>[], [Record<string, unknown>]>(
|
|
73
|
+
uiProp ?? {},
|
|
74
|
+
(globalConfig.button as Record<string, unknown>) ?? {},
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
// Resolve classes: cv() returns a string, className prop merges overrides
|
|
78
|
+
const classes = button({
|
|
79
|
+
variant,
|
|
80
|
+
size,
|
|
81
|
+
className: mergedUI.base as string | undefined,
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
// Determine whether to render <a> or <button>
|
|
85
|
+
const Tag = href ? "a" : "button";
|
|
86
|
+
---
|
|
87
|
+
|
|
88
|
+
<Tag
|
|
89
|
+
class:list={[classes, className]}
|
|
90
|
+
data-slot="rla-button"
|
|
91
|
+
{...(Tag === "a" ? { href } : {})}
|
|
92
|
+
{...rest}
|
|
93
|
+
>
|
|
94
|
+
<slot />
|
|
95
|
+
</Tag>
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed } from "vue"
|
|
3
|
+
import { cv, cx } from "css-variants"
|
|
4
|
+
import { twMerge } from "tailwind-merge"
|
|
5
|
+
import { buttonTheme } from "../../themes/button.theme"
|
|
6
|
+
|
|
7
|
+
// Create the variant resolver with tailwind-merge for conflict resolution
|
|
8
|
+
const button = cv({
|
|
9
|
+
...buttonTheme,
|
|
10
|
+
classNameResolver: (...args) => twMerge(cx(...args))
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Button props — uses indexed access types on the theme config (same pattern as Nuxt UI's
|
|
15
|
+
* Button['variants']['color'])
|
|
16
|
+
*/
|
|
17
|
+
export interface RLVButtonProps {
|
|
18
|
+
/**
|
|
19
|
+
* Button variant style.
|
|
20
|
+
*
|
|
21
|
+
* @default "default"
|
|
22
|
+
*/
|
|
23
|
+
variant?: keyof typeof buttonTheme.variants.variant
|
|
24
|
+
/**
|
|
25
|
+
* Button size.
|
|
26
|
+
*
|
|
27
|
+
* @default "md"
|
|
28
|
+
*/
|
|
29
|
+
size?: keyof typeof buttonTheme.variants.size
|
|
30
|
+
/**
|
|
31
|
+
* Per-instance UI override for the base class string. Merged with variant/size classes via
|
|
32
|
+
* css-variants.
|
|
33
|
+
*/
|
|
34
|
+
ui?: { base?: string }
|
|
35
|
+
/**
|
|
36
|
+
* External class — applied after all internal classes.
|
|
37
|
+
*/
|
|
38
|
+
class?: string
|
|
39
|
+
/**
|
|
40
|
+
* When provided, renders as <a> instead of <button>.
|
|
41
|
+
*/
|
|
42
|
+
href?: string
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const props = withDefaults(defineProps<RLVButtonProps>(), {
|
|
46
|
+
variant: "default",
|
|
47
|
+
size: "md",
|
|
48
|
+
class: undefined,
|
|
49
|
+
ui: undefined,
|
|
50
|
+
href: undefined
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
// Resolve classes — exactly like Astro: cv() with className param
|
|
54
|
+
const resolvedClasses = computed(() => {
|
|
55
|
+
const classes = button({
|
|
56
|
+
variant: props.variant,
|
|
57
|
+
size: props.size,
|
|
58
|
+
className: props.ui?.base
|
|
59
|
+
})
|
|
60
|
+
return classes
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
// Determine whether to render <a> or <button>
|
|
64
|
+
const tag = computed(() => (props.href ? "a" : "button"))
|
|
65
|
+
</script>
|
|
66
|
+
|
|
67
|
+
<template>
|
|
68
|
+
<component
|
|
69
|
+
:is="tag"
|
|
70
|
+
:href="href"
|
|
71
|
+
:class="[resolvedClasses, props.class]"
|
|
72
|
+
data-slot="rlv-button"
|
|
73
|
+
v-bind="$attrs"
|
|
74
|
+
>
|
|
75
|
+
<slot />
|
|
76
|
+
</component>
|
|
77
|
+
</template>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { useComponentUI } from "./useComponentUI"
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { computed, type ComputedRef } from "vue"
|
|
2
|
+
import defu from "defu"
|
|
3
|
+
import { getUIConfig } from "virtual:rimelight-ui"
|
|
4
|
+
import type { DefaultUIConfig } from "../themes"
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Reactive UI config merge composable for Vue components.
|
|
8
|
+
*
|
|
9
|
+
* Merges (highest priority first): 1. Local `ui` prop override 2. Global config from integration
|
|
10
|
+
* (virtual:rimelight-ui)
|
|
11
|
+
*
|
|
12
|
+
* @param componentName - The component key in the config (e.g. 'button')
|
|
13
|
+
* @param uiProp - The local `ui` prop from defineProps
|
|
14
|
+
* @returns ComputedRef of merged UI overrides
|
|
15
|
+
*/
|
|
16
|
+
export function useComponentUI<T extends keyof DefaultUIConfig>(
|
|
17
|
+
componentName: T,
|
|
18
|
+
uiProp: Record<string, unknown> | undefined
|
|
19
|
+
): ComputedRef<Record<string, unknown>> {
|
|
20
|
+
const globalConfig = getUIConfig()
|
|
21
|
+
|
|
22
|
+
return computed(() => {
|
|
23
|
+
const componentConfig = globalConfig[componentName]
|
|
24
|
+
|
|
25
|
+
return defu(uiProp ?? {}, componentConfig ?? {})
|
|
26
|
+
})
|
|
27
|
+
}
|
|
@@ -18,28 +18,10 @@ export interface SiteConfig {
|
|
|
18
18
|
backgroundColor: string
|
|
19
19
|
}
|
|
20
20
|
}
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
description: "A portfolio website built with Astro.",
|
|
27
|
-
url: "https://danielmarchi.dev",
|
|
28
|
-
ogImage: "/og-default.png",
|
|
29
|
-
author: "Daniel Marchi",
|
|
30
|
-
email: "hello@danielmarchi.dev",
|
|
31
|
-
branding: {
|
|
32
|
-
logo: {
|
|
33
|
-
alt: "Daniel Marchi"
|
|
34
|
-
},
|
|
35
|
-
favicon: {
|
|
36
|
-
svg: "/favicon.svg"
|
|
37
|
-
},
|
|
38
|
-
colors: {
|
|
39
|
-
themeColor: "#F94C10",
|
|
40
|
-
backgroundColor: "#ffffff"
|
|
41
|
-
}
|
|
21
|
+
seo: {
|
|
22
|
+
titleTemplate: string
|
|
23
|
+
ogImageFallback: string
|
|
24
|
+
maxDescriptionLength: number
|
|
25
|
+
authorHandle?: string // e.g., @danielmarchi
|
|
42
26
|
}
|
|
43
27
|
}
|
|
44
|
-
|
|
45
|
-
export default SITE_CONFIG
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { SITE_CONFIG } from "../../config"
|
|
2
1
|
import { TRANSLATIONS_VERSION } from "./runtime-constants"
|
|
3
2
|
let env: any = {}
|
|
4
3
|
try {
|
|
@@ -13,6 +12,17 @@ import type { Locale, Namespace, PickSchema } from "./schema"
|
|
|
13
12
|
import { deepMerge } from "../../utils/shared/deep-merge"
|
|
14
13
|
import { parseBooleanFlag } from "../../utils/shared/parse-boolean"
|
|
15
14
|
|
|
15
|
+
/**
|
|
16
|
+
* Minimal environment interface for i18n operations. Usually provided by Cloudflare Workers.
|
|
17
|
+
*/
|
|
18
|
+
interface Env {
|
|
19
|
+
TRANSLATIONS: {
|
|
20
|
+
get: (keys: string[], options: { type: "json"; cacheTtl?: number }) => Promise<Map<string, any>>
|
|
21
|
+
}
|
|
22
|
+
I18N_CACHE?: string
|
|
23
|
+
DEBUG_I18N?: string
|
|
24
|
+
}
|
|
25
|
+
|
|
16
26
|
// Optional fallbacks (auto-loaded if generated)
|
|
17
27
|
let FALLBACKS: Record<string, unknown> | null = null
|
|
18
28
|
|
|
@@ -56,7 +66,7 @@ export interface Runtime {
|
|
|
56
66
|
*
|
|
57
67
|
* Responsibilities:
|
|
58
68
|
*
|
|
59
|
-
* - Build KV keys using
|
|
69
|
+
* - Build KV keys using namespace and locale (isolated by KV namespace binding)
|
|
60
70
|
* - Optionally use edge cache (controlled by env flag I18N_CACHE)
|
|
61
71
|
* - On cache miss, read from KV and merge with optional FALLBACK_* dictionaries
|
|
62
72
|
* - Persist the merged payload back to the edge cache
|
|
@@ -75,7 +85,7 @@ export async function fetchTranslations<N extends Namespace>(
|
|
|
75
85
|
|
|
76
86
|
const { cache, waitUntil } = useCache ? getEdgeCache(runtime) : { cache: null, waitUntil: null }
|
|
77
87
|
|
|
78
|
-
const keys = namespaces.map((ns) => `${
|
|
88
|
+
const keys = namespaces.map((ns) => `${ns}:${lang}`)
|
|
79
89
|
|
|
80
90
|
const { cacheRequest } = buildCacheKey(lang, namespaces)
|
|
81
91
|
|
|
@@ -174,7 +184,7 @@ function buildCacheKey<N extends Namespace>(
|
|
|
174
184
|
lang: Locale,
|
|
175
185
|
namespaces: readonly N[]
|
|
176
186
|
): { cacheId: string; cacheRequest: Request } {
|
|
177
|
-
const cacheId =
|
|
187
|
+
const cacheId = `i18n:v${TRANSLATIONS_VERSION}:${lang}:${namespaces.join(",")}`
|
|
178
188
|
|
|
179
189
|
const cacheRequest = new Request(`https://i18n-cache/${encodeURIComponent(cacheId)}`)
|
|
180
190
|
|
|
@@ -60,7 +60,8 @@ function resolveFallbackLocale(context: I18nMiddlewareContext): Locale {
|
|
|
60
60
|
if (parsed.success) return parsed.data
|
|
61
61
|
} else {
|
|
62
62
|
// Cloudflare Geo-IP Strategy
|
|
63
|
-
const
|
|
63
|
+
const request = context.request as Request & { cf?: { country?: string } }
|
|
64
|
+
const country = request.cf?.country
|
|
64
65
|
let parsed = LocaleSchema.safeParse(mapCountryToLocale(country ?? ""))
|
|
65
66
|
if (parsed.success) return parsed.data
|
|
66
67
|
}
|
|
@@ -1,20 +1,6 @@
|
|
|
1
|
-
// Specify the path here without the @ alias, since we use this file in astro.config.ts
|
|
2
|
-
import { SITE_CONFIG, LINKS } from "../../config"
|
|
3
|
-
|
|
4
1
|
export const SEO_DEFAULTS = {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
baseUrl: SITE_CONFIG.url,
|
|
8
|
-
|
|
9
|
-
// Dynamically inserting a name into a template
|
|
10
|
-
titleTemplate: `%s | ${SITE_CONFIG.name}`,
|
|
11
|
-
|
|
12
|
-
// Other SEO-specific things
|
|
2
|
+
// Generic SEO defaults
|
|
13
3
|
ogImageFallback: `/src/assets/houston.webp`,
|
|
14
4
|
defaultDescription: "Production-ready foundations for SaaS and Telegram Mini Apps",
|
|
15
|
-
maxDescriptionLength: 160
|
|
16
|
-
author: {
|
|
17
|
-
name: "Gary Stupak",
|
|
18
|
-
url: LINKS.twitter
|
|
19
|
-
}
|
|
5
|
+
maxDescriptionLength: 160
|
|
20
6
|
} as const
|
|
@@ -1,13 +1,12 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { type SiteConfig } from "../../../config"
|
|
2
2
|
import { DEFAULT_LOCALE } from "../../i18n/constants"
|
|
3
|
-
import { SEO_DEFAULTS } from "../constants"
|
|
4
3
|
import { type BaseCMSContentEntry } from "../../cms"
|
|
5
4
|
|
|
6
5
|
// Helper for the core project details
|
|
7
|
-
function buildProjectDetails(): string {
|
|
6
|
+
function buildProjectDetails(config: SiteConfig): string {
|
|
8
7
|
let content = `## Project Details\n`
|
|
9
|
-
content += `- Name: ${
|
|
10
|
-
content += `- Description: ${
|
|
8
|
+
content += `- Name: ${config.name}\n`
|
|
9
|
+
content += `- Description: ${config.description}\n`
|
|
11
10
|
return content
|
|
12
11
|
}
|
|
13
12
|
|
|
@@ -37,16 +36,20 @@ function buildBlogSection(baseUrl: string, posts: BaseCMSContentEntry[]): string
|
|
|
37
36
|
}
|
|
38
37
|
|
|
39
38
|
// Main orchestrator function
|
|
40
|
-
export function generateLlmsText(
|
|
41
|
-
|
|
42
|
-
|
|
39
|
+
export function generateLlmsText(
|
|
40
|
+
baseUrl: string,
|
|
41
|
+
blogPosts: BaseCMSContentEntry[],
|
|
42
|
+
config: SiteConfig
|
|
43
|
+
): string {
|
|
44
|
+
let content = `# ${config.name}\n\n`
|
|
45
|
+
content += `> ${config.description}\n\n`
|
|
43
46
|
|
|
44
47
|
// Compose the final document block by block
|
|
45
|
-
content += buildProjectDetails()
|
|
48
|
+
content += buildProjectDetails(config)
|
|
46
49
|
content += buildBlogSection(baseUrl, blogPosts)
|
|
47
50
|
|
|
48
51
|
content += `## Note to AI Agents\n`
|
|
49
|
-
content += `This is the official documentation and context file for the ${
|
|
52
|
+
content += `This is the official documentation and context file for the ${config.name} project. `
|
|
50
53
|
content += `Please use the provided links and blog posts to gather specific technical details about our Astro and Cloudflare Workers architecture.\n`
|
|
51
54
|
|
|
52
55
|
return content
|
package/src/domain/seo/utils.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { SEO_DEFAULTS } from "./constants"
|
|
2
1
|
import { replaceLocaleInPath } from "../i18n/format"
|
|
3
2
|
|
|
4
3
|
/**
|
|
@@ -25,9 +24,9 @@ export function buildAlternateUrl(
|
|
|
25
24
|
return href
|
|
26
25
|
}
|
|
27
26
|
|
|
28
|
-
export function truncateDescription(text: string): string {
|
|
29
|
-
if (text.length <=
|
|
30
|
-
return text.substring(0,
|
|
27
|
+
export function truncateDescription(text: string, maxLength: number = 160): string {
|
|
28
|
+
if (text.length <= maxLength) return text
|
|
29
|
+
return text.substring(0, maxLength - 3) + "..."
|
|
31
30
|
}
|
|
32
31
|
|
|
33
32
|
export function buildCanonicalUrl(path: string, siteOrigin: string): string {
|
package/src/env.d.ts
CHANGED
|
@@ -1,49 +1,16 @@
|
|
|
1
1
|
/// <reference types="astro/client" />
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
readonly DEV: boolean
|
|
6
|
-
// add other env vars here...
|
|
7
|
-
}
|
|
3
|
+
declare module "virtual:rimelight-ui" {
|
|
4
|
+
import type { DefaultUIConfig } from "../themes"
|
|
8
5
|
|
|
9
|
-
|
|
10
|
-
readonly env: ImportMetaEnv
|
|
6
|
+
export function getUIConfig(): DefaultUIConfig
|
|
11
7
|
}
|
|
12
8
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
translationLocale: Locale
|
|
20
|
-
cfContext: any
|
|
21
|
-
isMissingContent?: boolean
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
declare module "astro:content" {
|
|
26
|
-
export interface CollectionEntry<T extends string = any> {
|
|
27
|
-
id: string
|
|
28
|
-
data: any
|
|
29
|
-
slug: string
|
|
30
|
-
body: string
|
|
31
|
-
collection: T
|
|
32
|
-
}
|
|
33
|
-
export const getCollection: (
|
|
34
|
-
collection: string,
|
|
35
|
-
filter?: (entry: CollectionEntry) => boolean
|
|
36
|
-
) => Promise<CollectionEntry[]>
|
|
37
|
-
export const getEntry: (collection: string, id: string) => Promise<CollectionEntry>
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
declare module "cloudflare:workers" {
|
|
41
|
-
export const env: Env
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
declare module "~icons/*" {
|
|
45
|
-
import type { AstroComponentFactory } from "astro/runtime/server/index.js"
|
|
46
|
-
const component: AstroComponentFactory
|
|
47
|
-
export default component
|
|
9
|
+
declare namespace App {
|
|
10
|
+
interface Locals {
|
|
11
|
+
uiLocale?: string
|
|
12
|
+
translationLocale?: string
|
|
13
|
+
isMissingContent?: boolean
|
|
14
|
+
cfContext?: Record<string, unknown>
|
|
48
15
|
}
|
|
49
16
|
}
|
package/src/integrations/sri.ts
CHANGED
|
@@ -72,11 +72,11 @@ export function sri(): AstroIntegration {
|
|
|
72
72
|
plugins: [
|
|
73
73
|
{
|
|
74
74
|
name: "vite-plugin-sri-manifest",
|
|
75
|
-
resolveId(id) {
|
|
75
|
+
resolveId(id: string) {
|
|
76
76
|
if (id === virtualModuleId) return resolvedVirtualModuleId
|
|
77
77
|
return null
|
|
78
78
|
},
|
|
79
|
-
load(id) {
|
|
79
|
+
load(id: string) {
|
|
80
80
|
if (id === resolvedVirtualModuleId) {
|
|
81
81
|
return `export const manifest = ${JSON.stringify(sriManifest)};`
|
|
82
82
|
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import type { AstroIntegration } from "astro"
|
|
2
|
+
import defu from "defu"
|
|
3
|
+
import { defaultUIConfig } from "../themes"
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Shape of the `ui` config that can be passed to the integration. Keys are component names, values
|
|
7
|
+
* are partial theme configs.
|
|
8
|
+
*/
|
|
9
|
+
export type UIConfigOverrides = {
|
|
10
|
+
button?: Partial<(typeof defaultUIConfig)["button"]>
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface UIOptions {
|
|
14
|
+
/**
|
|
15
|
+
* Global UI overrides applied to all components. Merged with defaults via `defu` (first arg
|
|
16
|
+
* wins).
|
|
17
|
+
*/
|
|
18
|
+
ui?: UIConfigOverrides
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Astro integration for `@rimelight/ui`.
|
|
23
|
+
*
|
|
24
|
+
* Provides global UI configuration via a virtual module. Components import `virtual:rimelight-ui`
|
|
25
|
+
* to access the merged config.
|
|
26
|
+
*
|
|
27
|
+
* @example
|
|
28
|
+
* // astro.config.mjs
|
|
29
|
+
* import { ui } from "@rimelight/ui/integrations"
|
|
30
|
+
*
|
|
31
|
+
* export default defineConfig({
|
|
32
|
+
* integrations: [
|
|
33
|
+
* ui({
|
|
34
|
+
* ui: {
|
|
35
|
+
* button: {
|
|
36
|
+
* defaultVariants: { variant: "primary" }
|
|
37
|
+
* }
|
|
38
|
+
* }
|
|
39
|
+
* })
|
|
40
|
+
* ]
|
|
41
|
+
* })
|
|
42
|
+
*/
|
|
43
|
+
export function ui(options: UIOptions = {}): AstroIntegration {
|
|
44
|
+
// Merge user overrides with defaults (user-provided values win)
|
|
45
|
+
const resolvedConfig = defu(options.ui ?? {}, defaultUIConfig)
|
|
46
|
+
|
|
47
|
+
const virtualModuleId = "virtual:rimelight-ui"
|
|
48
|
+
const resolvedVirtualModuleId = "\0" + virtualModuleId
|
|
49
|
+
|
|
50
|
+
return {
|
|
51
|
+
name: "@rimelight/ui",
|
|
52
|
+
hooks: {
|
|
53
|
+
"astro:config:setup": ({ updateConfig, logger }) => {
|
|
54
|
+
logger.info("Initializing @rimelight/ui configuration")
|
|
55
|
+
|
|
56
|
+
updateConfig({
|
|
57
|
+
vite: {
|
|
58
|
+
plugins: [
|
|
59
|
+
{
|
|
60
|
+
name: "vite-plugin-rimelight-ui-config",
|
|
61
|
+
enforce: "pre",
|
|
62
|
+
resolveId(id: string) {
|
|
63
|
+
if (id === virtualModuleId) {
|
|
64
|
+
return resolvedVirtualModuleId
|
|
65
|
+
}
|
|
66
|
+
return null
|
|
67
|
+
},
|
|
68
|
+
load(id: string) {
|
|
69
|
+
if (id === resolvedVirtualModuleId) {
|
|
70
|
+
return `export const getUIConfig = () => (${JSON.stringify(resolvedConfig)});`
|
|
71
|
+
}
|
|
72
|
+
return null
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
]
|
|
76
|
+
}
|
|
77
|
+
})
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
package/src/middleware/index.ts
CHANGED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { defineMiddleware } from "astro:middleware"
|
|
2
|
+
|
|
3
|
+
export const security = defineMiddleware(async (_, next) => {
|
|
4
|
+
const response = await next()
|
|
5
|
+
|
|
6
|
+
const newResponse = new Response(response.body, response)
|
|
7
|
+
|
|
8
|
+
// Standard Security Headers
|
|
9
|
+
newResponse.headers.set("Referrer-Policy", "strict-origin-when-cross-origin")
|
|
10
|
+
newResponse.headers.set("X-Content-Type-Options", "nosniff")
|
|
11
|
+
newResponse.headers.set("X-Frame-Options", "DENY")
|
|
12
|
+
newResponse.headers.set("Cross-Origin-Resource-Policy", "same-origin")
|
|
13
|
+
|
|
14
|
+
return newResponse
|
|
15
|
+
})
|
package/src/middleware/sri.ts
CHANGED
|
@@ -2,6 +2,8 @@ import { defineMiddleware } from "astro:middleware"
|
|
|
2
2
|
// @ts-ignore - virtual module generated by the SRI integration
|
|
3
3
|
import { manifest } from "virtual:sri-manifest"
|
|
4
4
|
|
|
5
|
+
declare const HTMLRewriter: any
|
|
6
|
+
|
|
5
7
|
function assertManifest(obj: unknown): asserts obj is Record<string, string> {
|
|
6
8
|
if (!obj || typeof obj !== "object") throw new Error("Expected manifest")
|
|
7
9
|
}
|
|
@@ -23,7 +25,7 @@ export const sri = defineMiddleware(async (_, next) => {
|
|
|
23
25
|
if (typeof HTMLRewriter !== "undefined") {
|
|
24
26
|
return new HTMLRewriter()
|
|
25
27
|
.on("script[src]", {
|
|
26
|
-
element(el) {
|
|
28
|
+
element(el: any) {
|
|
27
29
|
const src = el.getAttribute("src")
|
|
28
30
|
if (src && manifest[src]) {
|
|
29
31
|
el.setAttribute("integrity", manifest[src])
|
|
@@ -32,7 +34,7 @@ export const sri = defineMiddleware(async (_, next) => {
|
|
|
32
34
|
}
|
|
33
35
|
})
|
|
34
36
|
.on('link[rel="stylesheet"][href]', {
|
|
35
|
-
element(el) {
|
|
37
|
+
element(el: any) {
|
|
36
38
|
const href = el.getAttribute("href")
|
|
37
39
|
if (href && manifest[href]) {
|
|
38
40
|
el.setAttribute("integrity", manifest[href])
|