@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.
Files changed (68) hide show
  1. package/README.md +46 -272
  2. package/package.json +37 -10
  3. package/src/blocks/examples/sample-block-2.tsx +27 -0
  4. package/src/blocks/examples/sample-block.tsx +26 -0
  5. package/src/blocks/index.ts +14 -0
  6. package/src/blocks/registry.ts +38 -0
  7. package/src/blocks/types.ts +23 -0
  8. package/src/components/Admin.tsx +1 -1
  9. package/src/components/ApiKeysManager.tsx +1 -1
  10. package/src/components/AuditLogsPage.tsx +1 -1
  11. package/src/components/AutoForm.tsx +2 -2
  12. package/src/components/BrandingHub.tsx +1 -1
  13. package/src/components/CreateView.tsx +1 -1
  14. package/src/components/DetailView.tsx +1 -1
  15. package/src/components/DeveloperCenter.tsx +1 -1
  16. package/src/components/EnhancedListView.tsx +1 -1
  17. package/src/components/ListView.tsx +1 -1
  18. package/src/components/LoginPage.tsx +1 -1
  19. package/src/components/MediaGallery.tsx +1 -1
  20. package/src/components/UserManagement.tsx +1 -1
  21. package/src/components/WebhookManager.tsx +2 -2
  22. package/src/components/fields/RelationshipBlockField.tsx +1 -1
  23. package/src/components/fields/RelationshipField.tsx +1 -1
  24. package/src/components/fields/UploadField.tsx +1 -6
  25. package/src/components/ui/CommandPalette.tsx +1 -1
  26. package/src/fields/examples/sample-field-2.tsx +30 -0
  27. package/src/fields/examples/sample-field.tsx +30 -0
  28. package/src/fields/index.ts +33 -0
  29. package/src/fields/registry.tsx +46 -0
  30. package/src/fields/types.ts +24 -0
  31. package/src/hooks/data.ts +116 -0
  32. package/src/hooks/examples/sample-hook-2.ts +13 -0
  33. package/src/hooks/examples/sample-hook.ts +12 -0
  34. package/src/hooks/index.ts +19 -0
  35. package/src/hooks/lifecycle.ts +81 -0
  36. package/src/hooks/types.ts +40 -0
  37. package/src/index.ts +78 -0
  38. package/src/integration.ts +52 -0
  39. package/src/pages/api/[collection]/[id]/publish.ts +2 -2
  40. package/src/pages/api/[collection]/[id]/unpublish.ts +2 -2
  41. package/src/pages/api/[collection]/[id]/versions.ts +1 -1
  42. package/src/pages/api/[collection]/[id].ts +2 -2
  43. package/src/pages/api/[collection]/index.ts +2 -2
  44. package/src/pages/api/collections.ts +1 -1
  45. package/src/pages/api/globals/[slug].ts +2 -2
  46. package/src/pages/api/graphql.ts +3 -3
  47. package/src/pages/api/media/folders.ts +1 -1
  48. package/src/pages/api/media/index.ts +1 -1
  49. package/src/pages/api/media/resize.ts +1 -1
  50. package/src/pages/api/slug-availability.ts +2 -2
  51. package/src/pages/api/storage-config.ts +1 -1
  52. package/src/pages/api/storage-status.ts +1 -1
  53. package/src/pages/api/upload.ts +1 -1
  54. package/src/plugins/examples/sample-plugin-2.ts +21 -0
  55. package/src/plugins/examples/sample-plugin.ts +21 -0
  56. package/src/plugins/index.ts +10 -0
  57. package/src/plugins/registry.ts +36 -0
  58. package/src/plugins/types.ts +22 -0
  59. package/src/styles/main.css +2 -41
  60. package/src/theme/ThemeProvider.tsx +238 -0
  61. package/src/theme/index.ts +20 -0
  62. package/src/theme/tokens.ts +222 -0
  63. package/src/components/Modal.tsx +0 -206
  64. package/src/components/index.ts +0 -29
  65. package/src/env.ts +0 -20
  66. package/src/lib/i18n.tsx +0 -353
  67. package/src/lib/validation.ts +0 -250
  68. 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 "../../../../lib/dataStore";
3
- import { collections } from "../../../../lib/config";
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,5 +1,5 @@
1
1
  import type { APIRoute } from "astro";
2
- import { collections } from "../../../lib/config";
2
+ import { collections } from "../../lib/config";
3
3
 
4
4
  export const GET: APIRoute = async () => {
5
5
  try {
@@ -1,6 +1,6 @@
1
1
  import type { APIRoute } from "astro";
2
- import { dataStore } from "../../../../lib/dataStore";
3
- import { globals } from "../../../../lib/config";
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;
@@ -1,7 +1,7 @@
1
1
  import type { APIRoute } from "astro";
2
- import { executeGraphQL } from "../../../lib/graphql/schema";
3
- import { dataStore } from "../../../lib/dataStore";
4
- import { collections } from "../../../lib/config";
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 "../../../../lib/storage";
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 "../../../../lib/storage";
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 "../../../../lib/storage";
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 "../../../lib/dataStore";
3
- import { collections } from "../../../lib/config";
2
+ import { dataStore } from "../../lib/dataStore";
3
+ import { collections } from "../../lib/config";
4
4
 
5
5
  dataStore.initialize(collections);
6
6
 
@@ -1,5 +1,5 @@
1
1
  import type { APIRoute } from "astro";
2
- import { getStorageConfig } from "../../../lib/storage";
2
+ import { getStorageConfig } from "../../lib/storage";
3
3
 
4
4
  export const GET: APIRoute = async () => {
5
5
  try {
@@ -1,5 +1,5 @@
1
1
  import type { APIRoute } from "astro";
2
- import { getDatabaseConfig } from "../../../lib/db";
2
+ import { getDatabaseConfig } from "../../lib/db";
3
3
 
4
4
  interface RawStorageSettings {
5
5
  provider?: string;
@@ -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 "../../../lib/db";
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
+ }
@@ -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: "Serotiva Sans", ui-sans-serif, system-ui, sans-serif;
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";