@kyro-cms/admin 0.2.4 → 0.3.0
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/README.md +46 -272
- package/package.json +37 -10
- package/src/blocks/examples/sample-block-2.tsx +27 -0
- package/src/blocks/examples/sample-block.tsx +26 -0
- package/src/blocks/index.ts +14 -0
- package/src/blocks/registry.ts +38 -0
- package/src/blocks/types.ts +23 -0
- package/src/components/Admin.tsx +1 -1
- package/src/components/ApiKeysManager.tsx +1 -1
- package/src/components/AuditLogsPage.tsx +1 -1
- package/src/components/AutoForm.tsx +2 -2
- package/src/components/BrandingHub.tsx +1 -1
- package/src/components/CreateView.tsx +1 -1
- package/src/components/DetailView.tsx +1 -1
- package/src/components/DeveloperCenter.tsx +1 -1
- package/src/components/EnhancedListView.tsx +1 -1
- package/src/components/ListView.tsx +1 -1
- package/src/components/LoginPage.tsx +1 -1
- package/src/components/MediaGallery.tsx +1 -1
- package/src/components/UserManagement.tsx +1 -1
- package/src/components/WebhookManager.tsx +2 -2
- package/src/components/fields/RelationshipBlockField.tsx +1 -1
- package/src/components/fields/RelationshipField.tsx +1 -1
- package/src/components/fields/UploadField.tsx +1 -6
- package/src/components/ui/CommandPalette.tsx +1 -1
- package/src/fields/examples/sample-field-2.tsx +30 -0
- package/src/fields/examples/sample-field.tsx +30 -0
- package/src/fields/index.ts +33 -0
- package/src/fields/registry.tsx +46 -0
- package/src/fields/types.ts +24 -0
- package/src/hooks/data.ts +116 -0
- package/src/hooks/examples/sample-hook-2.ts +13 -0
- package/src/hooks/examples/sample-hook.ts +12 -0
- package/src/hooks/index.ts +19 -0
- package/src/hooks/lifecycle.ts +81 -0
- package/src/hooks/types.ts +40 -0
- package/src/index.ts +78 -0
- package/src/integration.ts +52 -0
- package/src/pages/api/[collection]/[id]/publish.ts +2 -2
- package/src/pages/api/[collection]/[id]/unpublish.ts +2 -2
- package/src/pages/api/[collection]/[id]/versions.ts +1 -1
- package/src/pages/api/[collection]/[id].ts +2 -2
- package/src/pages/api/[collection]/index.ts +2 -2
- package/src/pages/api/collections.ts +1 -1
- package/src/pages/api/globals/[slug].ts +2 -2
- package/src/pages/api/graphql.ts +3 -3
- package/src/pages/api/media/folders.ts +1 -1
- package/src/pages/api/media/index.ts +1 -1
- package/src/pages/api/media/resize.ts +1 -1
- package/src/pages/api/slug-availability.ts +2 -2
- package/src/pages/api/storage-config.ts +1 -1
- package/src/pages/api/storage-status.ts +1 -1
- package/src/pages/api/upload.ts +1 -1
- package/src/plugins/examples/sample-plugin-2.ts +21 -0
- package/src/plugins/examples/sample-plugin.ts +21 -0
- package/src/plugins/index.ts +10 -0
- package/src/plugins/registry.ts +36 -0
- package/src/plugins/types.ts +22 -0
- package/src/styles/main.css +2 -41
- package/src/theme/ThemeProvider.tsx +238 -0
- package/src/theme/index.ts +20 -0
- package/src/theme/tokens.ts +222 -0
- package/src/components/Modal.tsx +0 -206
- package/src/components/index.ts +0 -29
- package/src/env.ts +0 -20
- package/src/lib/i18n.tsx +0 -353
- package/src/lib/validation.ts +0 -250
- package/src/pages/api/globals/[slug]/test.ts +0 -171
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { APIRoute } from "astro";
|
|
2
|
-
import { dataStore } from "
|
|
3
|
-
import { collections } from "
|
|
2
|
+
import { dataStore } from "../../../lib/dataStore";
|
|
3
|
+
import { collections } from "../../../lib/config";
|
|
4
4
|
import { getAuthAdapter } from "../../../lib/db";
|
|
5
5
|
|
|
6
6
|
dataStore.initialize(collections);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { APIRoute } from "astro";
|
|
2
|
-
import { dataStore } from "
|
|
3
|
-
import { globals } from "
|
|
2
|
+
import { dataStore } from "../../../lib/dataStore";
|
|
3
|
+
import { globals } from "../../../lib/config";
|
|
4
4
|
|
|
5
5
|
export const GET: APIRoute = async ({ params }) => {
|
|
6
6
|
const slug = params.slug as string;
|
package/src/pages/api/graphql.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { APIRoute } from "astro";
|
|
2
|
-
import { executeGraphQL } from "
|
|
3
|
-
import { dataStore } from "
|
|
4
|
-
import { collections } from "
|
|
2
|
+
import { executeGraphQL } from "../../lib/graphql/schema";
|
|
3
|
+
import { dataStore } from "../../lib/dataStore";
|
|
4
|
+
import { collections } from "../../lib/config";
|
|
5
5
|
|
|
6
6
|
dataStore.initialize(collections);
|
|
7
7
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { APIRoute } from "astro";
|
|
2
2
|
import fs from "fs/promises";
|
|
3
|
-
import { getStorageConfig } from "
|
|
3
|
+
import { getStorageConfig } from "../../../lib/storage";
|
|
4
4
|
import { S3Client, ListObjectsV2Command } from "@aws-sdk/client-s3";
|
|
5
5
|
|
|
6
6
|
function isCloudProvider(provider: string): boolean {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { APIRoute } from "astro";
|
|
2
2
|
import { getMediaService, type MediaItem } from "../../../lib/MediaService";
|
|
3
|
-
import { constructMediaUrl, getStorageConfig } from "
|
|
3
|
+
import { constructMediaUrl, getStorageConfig } from "../../../lib/storage";
|
|
4
4
|
|
|
5
5
|
export const GET: APIRoute = async ({ url }) => {
|
|
6
6
|
let mediaService: any = null;
|
|
@@ -5,7 +5,7 @@ import fs from "fs/promises";
|
|
|
5
5
|
import fsSync from "fs";
|
|
6
6
|
import https from "https";
|
|
7
7
|
import { createHash } from "crypto";
|
|
8
|
-
import { getStorageConfig } from "
|
|
8
|
+
import { getStorageConfig } from "../../../lib/storage";
|
|
9
9
|
|
|
10
10
|
// Cache configuration
|
|
11
11
|
const CACHE_BASE = path.join(process.cwd(), ".cache", "kyro-media", "resize");
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { APIRoute } from "astro";
|
|
2
|
-
import { dataStore } from "
|
|
3
|
-
import { collections } from "
|
|
2
|
+
import { dataStore } from "../../lib/dataStore";
|
|
3
|
+
import { collections } from "../../lib/config";
|
|
4
4
|
|
|
5
5
|
dataStore.initialize(collections);
|
|
6
6
|
|
package/src/pages/api/upload.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { APIRoute } from "astro";
|
|
2
2
|
import { MediaService, type Dialect } from "@kyro-cms/core";
|
|
3
|
-
import { getDatabaseConfig, runMigrations } from "
|
|
3
|
+
import { getDatabaseConfig, runMigrations } from "../../lib/db";
|
|
4
4
|
|
|
5
5
|
const MAX_FILE_SIZE = 50 * 1024 * 1024; // 50MB
|
|
6
6
|
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { KyroPlugin } from "../types.js";
|
|
2
|
+
import { registerPlugin } from "../registry.js";
|
|
3
|
+
|
|
4
|
+
// Second MVP plugin demonstrating beforeDeploy hook usage
|
|
5
|
+
const samplePlugin2: KyroPlugin = {
|
|
6
|
+
name: "sample-plugin-2",
|
|
7
|
+
version: "0.1.0",
|
|
8
|
+
description: "Second MVP plugin demonstrating beforeDeploy hook",
|
|
9
|
+
hooks: {
|
|
10
|
+
beforeDeploy: (ctx) => {
|
|
11
|
+
// Lightweight side-effect; in real plugins, you could validate config, migrations, etc.
|
|
12
|
+
void ctx;
|
|
13
|
+
console.log("[Kyro Admin] sample-plugin-2 beforeDeploy");
|
|
14
|
+
return { success: true };
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
registerPlugin(samplePlugin2);
|
|
20
|
+
|
|
21
|
+
export default samplePlugin2;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { KyroPlugin } from "../types.js";
|
|
2
|
+
import { registerPlugin } from "../registry.js";
|
|
3
|
+
|
|
4
|
+
// Simple MVP plugin demonstrating registration and an onAdminReady hook
|
|
5
|
+
const samplePlugin: KyroPlugin = {
|
|
6
|
+
name: "sample-plugin",
|
|
7
|
+
version: "0.1.0",
|
|
8
|
+
description: "A tiny sample plugin to demonstrate the extensibility surface",
|
|
9
|
+
hooks: {
|
|
10
|
+
onAdminReady: () => {
|
|
11
|
+
// Lightweight side-effect; in real plugins this could mount UI or register editors
|
|
12
|
+
// eslint-disable-next-line no-console
|
|
13
|
+
console.log("[ Kyro Admin ] sample-plugin: onAdminReady executed");
|
|
14
|
+
return;
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
registerPlugin(samplePlugin);
|
|
20
|
+
|
|
21
|
+
export default samplePlugin;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export {
|
|
2
|
+
registerPlugin,
|
|
3
|
+
unregisterPlugin,
|
|
4
|
+
getPlugin,
|
|
5
|
+
getPlugins,
|
|
6
|
+
getPluginsWithHook,
|
|
7
|
+
} from "./registry.ts";
|
|
8
|
+
export type { KyroPlugin } from "./types.ts";
|
|
9
|
+
export { default as samplePlugin } from "./examples/sample-plugin";
|
|
10
|
+
export { default as samplePlugin2 } from "./examples/sample-plugin-2.ts";
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { KyroPlugin } from "./types.ts";
|
|
2
|
+
|
|
3
|
+
const plugins: Map<string, KyroPlugin> = new Map();
|
|
4
|
+
|
|
5
|
+
export function registerPlugin(plugin: KyroPlugin): void {
|
|
6
|
+
if (!plugin.name || typeof plugin.name !== "string") {
|
|
7
|
+
throw new Error("Plugin must have a valid name");
|
|
8
|
+
}
|
|
9
|
+
if (plugins.has(plugin.name)) {
|
|
10
|
+
console.warn(`Plugin "${plugin.name}" is already registered. Overwriting.`);
|
|
11
|
+
}
|
|
12
|
+
plugins.set(plugin.name, plugin);
|
|
13
|
+
if (plugin.apply) {
|
|
14
|
+
plugin.apply({});
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function unregisterPlugin(name: string): void {
|
|
19
|
+
plugins.delete(name);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function getPlugin(name: string): KyroPlugin | undefined {
|
|
23
|
+
return plugins.get(name);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function getPlugins(): KyroPlugin[] {
|
|
27
|
+
return Array.from(plugins.values());
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function getPluginsWithHook<
|
|
31
|
+
K extends keyof NonNullable<KyroPlugin["hooks"]>,
|
|
32
|
+
>(hookName: K): KyroPlugin[] {
|
|
33
|
+
return Array.from(plugins.values()).filter(
|
|
34
|
+
(p) => p.hooks && typeof p.hooks[hookName] === "function",
|
|
35
|
+
);
|
|
36
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { AdminContext, HookResult } from "../hooks/types.js";
|
|
2
|
+
|
|
3
|
+
export interface KyroPlugin {
|
|
4
|
+
name: string;
|
|
5
|
+
version: string;
|
|
6
|
+
description?: string;
|
|
7
|
+
apply?: (config: Record<string, unknown>) => void;
|
|
8
|
+
hooks?: {
|
|
9
|
+
onAdminReady?: (
|
|
10
|
+
ctx: AdminContext,
|
|
11
|
+
) => void | HookResult | Promise<void | HookResult>;
|
|
12
|
+
beforeDeploy?: (
|
|
13
|
+
ctx: AdminContext,
|
|
14
|
+
) => void | HookResult | Promise<void | HookResult>;
|
|
15
|
+
afterDeploy?: (
|
|
16
|
+
ctx: AdminContext,
|
|
17
|
+
result: HookResult,
|
|
18
|
+
) => void | Promise<void>;
|
|
19
|
+
beforeRender?: (ctx: AdminContext) => void;
|
|
20
|
+
afterRender?: (ctx: AdminContext) => void;
|
|
21
|
+
};
|
|
22
|
+
}
|
package/src/styles/main.css
CHANGED
|
@@ -1,48 +1,9 @@
|
|
|
1
1
|
@import "tailwindcss";
|
|
2
|
+
@import url("https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;900&display=swap");
|
|
2
3
|
@custom-variant dark (&:where(.dark, .dark *));
|
|
3
4
|
|
|
4
5
|
@source "../../src/**/*.{astro,html,js,jsx,ts,tsx}";
|
|
5
6
|
|
|
6
|
-
@font-face {
|
|
7
|
-
font-family: "Serotiva Sans";
|
|
8
|
-
src: url("/fonts/Serotiva-Regular.woff2") format("woff2");
|
|
9
|
-
font-weight: 400;
|
|
10
|
-
font-style: normal;
|
|
11
|
-
font-display: swap;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
@font-face {
|
|
15
|
-
font-family: "Serotiva Sans";
|
|
16
|
-
src: url("/fonts/Serotiva-Medium.woff2") format("woff2");
|
|
17
|
-
font-weight: 500;
|
|
18
|
-
font-style: normal;
|
|
19
|
-
font-display: swap;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
@font-face {
|
|
23
|
-
font-family: "Serotiva Sans";
|
|
24
|
-
src: url("/fonts/Serotiva-SemiBold.woff2") format("woff2");
|
|
25
|
-
font-weight: 600;
|
|
26
|
-
font-style: normal;
|
|
27
|
-
font-display: swap;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
@font-face {
|
|
31
|
-
font-family: "Serotiva Sans";
|
|
32
|
-
src: url("/fonts/Serotiva-Bold.woff2") format("woff2");
|
|
33
|
-
font-weight: 700;
|
|
34
|
-
font-style: normal;
|
|
35
|
-
font-display: swap;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
@font-face {
|
|
39
|
-
font-family: "Serotiva Sans";
|
|
40
|
-
src: url("/fonts/Serotiva-Black.woff2") format("woff2");
|
|
41
|
-
font-weight: 900;
|
|
42
|
-
font-style: normal;
|
|
43
|
-
font-display: swap;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
7
|
:root {
|
|
47
8
|
/* Monochrome Palette */
|
|
48
9
|
--kyro-black: #0b1222;
|
|
@@ -165,7 +126,7 @@
|
|
|
165
126
|
}
|
|
166
127
|
|
|
167
128
|
@theme {
|
|
168
|
-
--font-sans: "
|
|
129
|
+
--font-sans: "Inter", ui-sans-serif, system-ui, sans-serif;
|
|
169
130
|
|
|
170
131
|
--color-primary: var(--kyro-black);
|
|
171
132
|
--color-primary-hover: var(--kyro-black-hover);
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
import React, {
|
|
2
|
+
createContext,
|
|
3
|
+
useContext,
|
|
4
|
+
useState,
|
|
5
|
+
useEffect,
|
|
6
|
+
useCallback,
|
|
7
|
+
type ReactNode,
|
|
8
|
+
} from "react";
|
|
9
|
+
import type { KyroTheme } from "./tokens.js";
|
|
10
|
+
import { LIGHT_THEME, DARK_THEME, mergeThemes } from "./tokens.js";
|
|
11
|
+
|
|
12
|
+
export type ThemeMode = "light" | "dark" | "system";
|
|
13
|
+
|
|
14
|
+
interface ThemeContextValue {
|
|
15
|
+
mode: ThemeMode;
|
|
16
|
+
theme: KyroTheme;
|
|
17
|
+
lightTheme: KyroTheme;
|
|
18
|
+
darkTheme: KyroTheme;
|
|
19
|
+
setMode: (mode: ThemeMode) => void;
|
|
20
|
+
updateTheme: (overrides: Partial<KyroTheme>) => void;
|
|
21
|
+
getCssVar: (key: string) => string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const ThemeContext = createContext<ThemeContextValue | null>(null);
|
|
25
|
+
|
|
26
|
+
export function useTheme() {
|
|
27
|
+
const context = useContext(ThemeContext);
|
|
28
|
+
if (!context) {
|
|
29
|
+
return {
|
|
30
|
+
mode: "light" as ThemeMode,
|
|
31
|
+
theme: LIGHT_THEME,
|
|
32
|
+
lightTheme: LIGHT_THEME,
|
|
33
|
+
darkTheme: DARK_THEME,
|
|
34
|
+
setMode: () => {},
|
|
35
|
+
updateTheme: () => {},
|
|
36
|
+
getCssVar: (key: string) => `var(--kyro-${key})`,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
return context;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
interface ThemeProviderProps {
|
|
43
|
+
children: ReactNode;
|
|
44
|
+
defaultMode?: ThemeMode;
|
|
45
|
+
light?: Partial<KyroTheme>;
|
|
46
|
+
dark?: Partial<KyroTheme>;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function applyThemeToDOM(config: KyroTheme) {
|
|
50
|
+
const root = document.documentElement;
|
|
51
|
+
if (!root) return;
|
|
52
|
+
|
|
53
|
+
// Apply colors
|
|
54
|
+
if (config.colors) {
|
|
55
|
+
Object.entries(config.colors).forEach(([key, value]) => {
|
|
56
|
+
if (value) {
|
|
57
|
+
root.style.setProperty(`--kyro-${key}`, value);
|
|
58
|
+
root.style.setProperty(
|
|
59
|
+
`--kyro-${key}-light`,
|
|
60
|
+
adjustBrightness(value, 0.9),
|
|
61
|
+
);
|
|
62
|
+
root.style.setProperty(
|
|
63
|
+
`--kyro-${key}-dark`,
|
|
64
|
+
adjustBrightness(value, 0.8),
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Apply typography
|
|
71
|
+
if (config.typography) {
|
|
72
|
+
if (config.typography.fontFamily) {
|
|
73
|
+
root.style.setProperty(
|
|
74
|
+
"--kyro-font-family",
|
|
75
|
+
config.typography.fontFamily,
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
if (config.typography.fontFamilyMono) {
|
|
79
|
+
root.style.setProperty(
|
|
80
|
+
"--kyro-font-mono",
|
|
81
|
+
config.typography.fontFamilyMono,
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Apply spacing
|
|
87
|
+
if (config.spacing) {
|
|
88
|
+
Object.entries(config.spacing).forEach(([key, value]) => {
|
|
89
|
+
if (value) root.style.setProperty(`--kyro-spacing-${key}`, value);
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Apply border radius
|
|
94
|
+
if (config.borderRadius) {
|
|
95
|
+
Object.entries(config.borderRadius).forEach(([key, value]) => {
|
|
96
|
+
if (value) root.style.setProperty(`--kyro-radius-${key}`, value);
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Apply shadows
|
|
101
|
+
if (config.shadows) {
|
|
102
|
+
Object.entries(config.shadows).forEach(([key, value]) => {
|
|
103
|
+
if (value) root.style.setProperty(`--kyro-shadow-${key}`, value);
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Apply block theme overrides
|
|
108
|
+
if (config.blocks) {
|
|
109
|
+
if (config.blocks.card) {
|
|
110
|
+
Object.entries(config.blocks.card).forEach(([key, value]) => {
|
|
111
|
+
if (value) root.style.setProperty(`--kyro-block-card-${key}`, value);
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
if (config.blocks.hero?.background) {
|
|
115
|
+
root.style.setProperty(
|
|
116
|
+
"--kyro-block-hero-bg",
|
|
117
|
+
config.blocks.hero.background,
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
if (config.blocks.code) {
|
|
121
|
+
Object.entries(config.blocks.code).forEach(([key, value]) => {
|
|
122
|
+
if (value) root.style.setProperty(`--kyro-block-code-${key}`, value);
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Apply field theme overrides
|
|
128
|
+
if (config.fields) {
|
|
129
|
+
if (config.fields.input) {
|
|
130
|
+
Object.entries(config.fields.input).forEach(([key, value]) => {
|
|
131
|
+
if (value) root.style.setProperty(`--kyro-field-input-${key}`, value);
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
if (config.fields.upload) {
|
|
135
|
+
if (config.fields.upload.dropzoneBackground) {
|
|
136
|
+
root.style.setProperty(
|
|
137
|
+
"--kyro-field-upload-dropzone-bg",
|
|
138
|
+
config.fields.upload.dropzoneBackground,
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function adjustBrightness(hex: string, factor: number): string {
|
|
146
|
+
if (!hex.startsWith("#")) return hex;
|
|
147
|
+
const r = parseInt(hex.slice(1, 3), 16);
|
|
148
|
+
const g = parseInt(hex.slice(3, 5), 16);
|
|
149
|
+
const b = parseInt(hex.slice(5, 7), 16);
|
|
150
|
+
const adjust = (c: number) =>
|
|
151
|
+
Math.round(c * factor)
|
|
152
|
+
.toString(16)
|
|
153
|
+
.padStart(2, "0");
|
|
154
|
+
return `#${adjust(r)}${adjust(g)}${adjust(b)}`;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export function ThemeProvider({
|
|
158
|
+
children,
|
|
159
|
+
defaultMode = "light",
|
|
160
|
+
light: lightOverrides,
|
|
161
|
+
dark: darkOverrides,
|
|
162
|
+
}: ThemeProviderProps) {
|
|
163
|
+
const [mode, setMode] = useState<ThemeMode>(defaultMode);
|
|
164
|
+
const [baseLight, setBaseLight] = useState<Partial<KyroTheme>>(
|
|
165
|
+
lightOverrides || {},
|
|
166
|
+
);
|
|
167
|
+
const [baseDark, setBaseDark] = useState<Partial<KyroTheme>>(
|
|
168
|
+
darkOverrides || {},
|
|
169
|
+
);
|
|
170
|
+
|
|
171
|
+
const lightTheme = mergeThemes(LIGHT_THEME, baseLight);
|
|
172
|
+
const darkTheme = mergeThemes(DARK_THEME, baseDark);
|
|
173
|
+
|
|
174
|
+
const getResolvedTheme = useCallback((): KyroTheme => {
|
|
175
|
+
if (mode === "system") {
|
|
176
|
+
if (typeof window !== "undefined") {
|
|
177
|
+
return window.matchMedia("(prefers-color-scheme: dark)").matches
|
|
178
|
+
? darkTheme
|
|
179
|
+
: lightTheme;
|
|
180
|
+
}
|
|
181
|
+
return lightTheme;
|
|
182
|
+
}
|
|
183
|
+
return mode === "dark" ? darkTheme : lightTheme;
|
|
184
|
+
}, [mode, lightTheme, darkTheme]);
|
|
185
|
+
|
|
186
|
+
const [theme, setTheme] = useState<KyroTheme>(getResolvedTheme());
|
|
187
|
+
|
|
188
|
+
// Apply theme on mode/customization change
|
|
189
|
+
useEffect(() => {
|
|
190
|
+
const resolved = getResolvedTheme();
|
|
191
|
+
setTheme(resolved);
|
|
192
|
+
applyThemeToDOM(resolved);
|
|
193
|
+
}, [getResolvedTheme]);
|
|
194
|
+
|
|
195
|
+
// Handle system theme changes
|
|
196
|
+
useEffect(() => {
|
|
197
|
+
if (mode !== "system") return;
|
|
198
|
+
const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
|
|
199
|
+
const handler = () => {
|
|
200
|
+
const resolved = getResolvedTheme();
|
|
201
|
+
setTheme(resolved);
|
|
202
|
+
applyThemeToDOM(resolved);
|
|
203
|
+
};
|
|
204
|
+
mediaQuery.addEventListener("change", handler);
|
|
205
|
+
return () => mediaQuery.removeEventListener("change", handler);
|
|
206
|
+
}, [mode, getResolvedTheme]);
|
|
207
|
+
|
|
208
|
+
const updateTheme = useCallback((overrides: Partial<KyroTheme>) => {
|
|
209
|
+
setBaseLight((prev) => ({ ...prev, ...overrides }));
|
|
210
|
+
setBaseDark((prev) => ({ ...prev, ...overrides }));
|
|
211
|
+
}, []);
|
|
212
|
+
|
|
213
|
+
const getCssVar = useCallback((key: string) => `var(--kyro-${key})`, []);
|
|
214
|
+
|
|
215
|
+
return (
|
|
216
|
+
<ThemeContext.Provider
|
|
217
|
+
value={{
|
|
218
|
+
mode,
|
|
219
|
+
theme,
|
|
220
|
+
lightTheme,
|
|
221
|
+
darkTheme,
|
|
222
|
+
setMode,
|
|
223
|
+
updateTheme,
|
|
224
|
+
getCssVar,
|
|
225
|
+
}}
|
|
226
|
+
>
|
|
227
|
+
{children}
|
|
228
|
+
</ThemeContext.Provider>
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
export const LightThemeProvider = (
|
|
233
|
+
props: Omit<ThemeProviderProps, "defaultMode">,
|
|
234
|
+
) => <ThemeProvider defaultMode="light" {...props} />;
|
|
235
|
+
|
|
236
|
+
export const DarkThemeProvider = (
|
|
237
|
+
props: Omit<ThemeProviderProps, "defaultMode">,
|
|
238
|
+
) => <ThemeProvider defaultMode="dark" {...props} />;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export {
|
|
2
|
+
ThemeProvider,
|
|
3
|
+
LightThemeProvider,
|
|
4
|
+
DarkThemeProvider,
|
|
5
|
+
useTheme,
|
|
6
|
+
} from "./ThemeProvider.tsx";
|
|
7
|
+
export type { ThemeMode } from "./ThemeProvider.tsx";
|
|
8
|
+
export {
|
|
9
|
+
LIGHT_THEME,
|
|
10
|
+
DARK_THEME,
|
|
11
|
+
mergeThemes,
|
|
12
|
+
type KyroTheme,
|
|
13
|
+
type ThemeColors,
|
|
14
|
+
type ThemeTypography,
|
|
15
|
+
type ThemeSpacing,
|
|
16
|
+
type ThemeRadius,
|
|
17
|
+
type ThemeShadows,
|
|
18
|
+
type BlockThemeOverrides,
|
|
19
|
+
type FieldThemeOverrides,
|
|
20
|
+
} from "./tokens.ts";
|