@kyro-cms/admin 0.9.0 → 0.9.2

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 (114) hide show
  1. package/dist/index.cjs +11715 -11292
  2. package/dist/index.cjs.map +1 -1
  3. package/dist/index.css +67 -65
  4. package/dist/index.css.map +1 -1
  5. package/dist/index.d.cts +564 -0
  6. package/dist/index.d.ts +11 -10
  7. package/dist/index.js +11326 -10912
  8. package/dist/index.js.map +1 -1
  9. package/package.json +16 -12
  10. package/src/components/ActionBar.tsx +25 -161
  11. package/src/components/Admin.tsx +2 -4
  12. package/src/components/ApiKeysManager.tsx +5 -5
  13. package/src/components/AuditLogsPage.tsx +2 -13
  14. package/src/components/AutoForm.tsx +572 -461
  15. package/src/components/BrandingHub.tsx +7 -4
  16. package/src/components/CreateView.tsx +2 -0
  17. package/src/components/DetailView.tsx +52 -65
  18. package/src/components/DeveloperCenter.tsx +8 -6
  19. package/src/components/FieldRenderer.tsx +94 -19
  20. package/src/components/ListView.tsx +57 -216
  21. package/src/components/MediaGallery.tsx +334 -367
  22. package/src/components/PluginsManager.tsx +197 -70
  23. package/src/components/RestPlayground.tsx +59 -52
  24. package/src/components/SessionsManager.tsx +1 -1
  25. package/src/components/SettingsPage.tsx +22 -0
  26. package/src/components/Sidebar.astro +13 -41
  27. package/src/components/UserManagement.tsx +153 -15
  28. package/src/components/UserMenu.tsx +30 -4
  29. package/src/components/VersionHistoryPanel.tsx +112 -119
  30. package/src/components/WebhookManager.tsx +6 -4
  31. package/src/components/blocks/ArrayBlock.tsx +6 -23
  32. package/src/components/blocks/BlockEditModal.tsx +82 -309
  33. package/src/components/blocks/CardBlock.tsx +35 -0
  34. package/src/components/blocks/ChildBlocksTree.tsx +57 -31
  35. package/src/components/blocks/GenericBlock.tsx +44 -0
  36. package/src/components/blocks/HeadingSubheadingBlock.tsx +32 -0
  37. package/src/components/blocks/HeroBlock.tsx +5 -14
  38. package/src/components/blocks/RichTextBlock.tsx +5 -5
  39. package/src/components/blocks/index.ts +5 -3
  40. package/src/components/fields/AccordionField.tsx +2 -2
  41. package/src/components/fields/ArrayField.tsx +1 -1
  42. package/src/components/fields/ArrayLayout.tsx +120 -29
  43. package/src/components/fields/BlocksField.tsx +433 -55
  44. package/src/components/fields/CardField.tsx +73 -0
  45. package/src/components/fields/CheckboxField.tsx +7 -3
  46. package/src/components/fields/DateField.tsx +4 -1
  47. package/src/components/fields/GroupLayout.tsx +2 -2
  48. package/src/components/fields/HeadingSubheadingField.tsx +43 -0
  49. package/src/components/fields/ListField.tsx +2 -2
  50. package/src/components/fields/NumberField.tsx +4 -1
  51. package/src/components/fields/RelationshipBlockField.tsx +2 -3
  52. package/src/components/fields/RelationshipField.tsx +155 -90
  53. package/src/components/fields/RichTextField.tsx +781 -0
  54. package/src/components/fields/SecretField.tsx +102 -0
  55. package/src/components/fields/SelectField.tsx +19 -6
  56. package/src/components/fields/TabsLayout.tsx +19 -9
  57. package/src/components/fields/TextField.tsx +4 -1
  58. package/src/components/fields/UploadField.tsx +122 -56
  59. package/src/components/fields/extensions/blockComponents.tsx +103 -174
  60. package/src/components/fields/extensions/blocksStore.ts +8 -1
  61. package/src/components/fields/index.ts +4 -2
  62. package/src/components/fix_imports.cjs +23 -0
  63. package/src/components/fix_imports2.cjs +19 -0
  64. package/src/components/replace_svgs.cjs +63 -0
  65. package/src/components/ui/Dropdown.tsx +7 -2
  66. package/src/components/ui/Modal.tsx +24 -27
  67. package/src/components/ui/PageHeader.tsx +5 -5
  68. package/src/components/ui/PromptModal.tsx +2 -10
  69. package/src/components/ui/SlidePanel.tsx +10 -13
  70. package/src/components/ui/SplitButton.tsx +107 -0
  71. package/src/components/ui/Toaster.tsx +0 -1
  72. package/src/components/ui/icons.tsx +110 -109
  73. package/src/components/users/UserDetail.tsx +79 -16
  74. package/src/components/users/UsersList.tsx +8 -85
  75. package/src/hooks/useAutoFormState.ts +187 -196
  76. package/src/hooks/useQueue.ts +60 -0
  77. package/src/integration.ts +148 -46
  78. package/src/kyro-cms.d.ts +7 -2
  79. package/src/layouts/AdminLayout.astro +22 -2
  80. package/src/layouts/AuthLayout.astro +67 -7
  81. package/src/lib/autoform-store.ts +90 -53
  82. package/src/lib/change-source.ts +9 -0
  83. package/src/lib/config.ts +104 -8
  84. package/src/lib/globals.ts +48 -11
  85. package/src/lib/normalize-upload-fields.ts +41 -0
  86. package/src/lib/paths.ts +2 -2
  87. package/src/lib/resolve-field-value.ts +110 -0
  88. package/src/lib/shim/use-sync-external-store-with-selector.js +30 -0
  89. package/src/lib/shim/use-sync-external-store.js +1 -0
  90. package/src/lib/stores/index.ts +1 -0
  91. package/src/lib/useResourceManager.ts +4 -4
  92. package/src/lib/vite-shim-plugin.ts +100 -0
  93. package/src/pages/[collection]/[id].astro +1 -1
  94. package/src/pages/auth/register.astro +5 -2
  95. package/src/pages/preview/[collection]/[id].astro +4 -4
  96. package/src/pages/settings/[slug].astro +2 -2
  97. package/src/styles/main.css +60 -54
  98. package/README.md +0 -46
  99. package/dist/EditorClient-Q23UXR37.cjs +0 -468
  100. package/dist/EditorClient-Q23UXR37.cjs.map +0 -1
  101. package/dist/EditorClient-T5PASFNR.js +0 -466
  102. package/dist/EditorClient-T5PASFNR.js.map +0 -1
  103. package/dist/chunk-3BGDYKTD.cjs +0 -348
  104. package/dist/chunk-3BGDYKTD.cjs.map +0 -1
  105. package/dist/chunk-EEFXLQVT.js +0 -3
  106. package/dist/chunk-EEFXLQVT.js.map +0 -1
  107. package/src/components/blocks/ButtonBlock.tsx +0 -64
  108. package/src/components/blocks/ColumnsBlock.tsx +0 -55
  109. package/src/components/blocks/DividerBlock.tsx +0 -43
  110. package/src/components/blocks/LinkBlock.tsx +0 -65
  111. package/src/components/blocks/VStackBlock.tsx +0 -29
  112. package/src/components/fields/EditorClient.tsx +0 -535
  113. package/src/components/fields/PortableTextField.tsx +0 -155
  114. package/src/components/fields/PortableTextRenderer.tsx +0 -68
@@ -1,10 +1,14 @@
1
1
  import type { AstroIntegration } from "astro";
2
2
  import path from "path";
3
3
  import fs from "fs";
4
- import { execSync } from "child_process";
5
4
  import { pathToFileURL } from "url";
5
+ import { build } from "esbuild";
6
6
  import { config as loadDotEnv } from "dotenv";
7
- import { transform } from "esbuild";
7
+ import { Worker } from "worker_threads";
8
+
9
+ const _shimDir = path.resolve(new URL(".", import.meta.url).pathname, "lib/shim");
10
+ const _shimUses = path.join(_shimDir, "use-sync-external-store.js");
11
+ const _shimUsesWs = path.join(_shimDir, "use-sync-external-store-with-selector.js");
8
12
 
9
13
  export interface KyroAdminOptions {
10
14
  basePath?: string;
@@ -46,81 +50,178 @@ export function kyroAdmin(options: KyroAdminOptions = {}): AstroIntegration {
46
50
  logger.warn(`Config file not found. Using defaults.`);
47
51
  }
48
52
 
49
- // Load the user's config and expose it globally so that
50
- // admin lib modules (config.ts, globals.ts) can access it
51
- // without needing the kyro:config Vite alias during config loading.
52
- // Load .env first since Vite hasn't processed it yet at this point
53
- // in the lifecycle, and the config module evaluates eagerly.
54
- // Use esbuild to transpile TS to ESM, then evaluate in a child
55
- // process to completely bypass Vite's module runner interception.
53
+ // Transpile the user's config and write a serialized JSON copy.
54
+ // The JSON path is injected via Vite's define so the admin can read it reliably.
55
+ const configFile = path.join(path.dirname(resolvedConfig), ".kyro-admin-config.json");
56
56
  let tmpFile = "";
57
57
  try {
58
58
  const envPath = path.join(path.dirname(resolvedConfig), ".env");
59
59
  if (fs.existsSync(envPath)) {
60
60
  loadDotEnv({ path: envPath });
61
61
  }
62
- const configContent = fs.readFileSync(resolvedConfig, "utf8");
63
- const result = await transform(configContent, {
64
- loader: "ts",
62
+ const result = await build({
63
+ entryPoints: [resolvedConfig],
64
+ bundle: true,
65
65
  format: "esm",
66
+ platform: "node",
66
67
  target: "es2022",
68
+ write: false,
67
69
  sourcemap: false,
70
+ loader: { '.ts': 'ts', '.tsx': 'tsx' },
71
+ resolveExtensions: ['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs', '.json'],
72
+ external: ['@kyro-cms/*'],
68
73
  });
69
- // Write transpiled config alongside original so Node.js can
70
- // resolve @kyro-cms/core from the project's node_modules
71
74
  tmpFile = resolvedConfig.replace(/\.ts$/, ".admin.mjs");
72
- fs.writeFileSync(tmpFile, result.code, "utf8");
73
- // Evaluate in a child process to bypass Vite's module runner.
74
- // Write a wrapper entrypoint that imports the config and prints JSON,
75
- // then execute it with tsx (handles .ts resolution from .js imports).
76
- const entryFile = tmpFile.replace(/\.admin\.mjs$/, ".admin-entry.mjs");
77
- const resultFile = tmpFile.replace(/\.admin\.mjs$/, ".admin-result.json");
78
- fs.writeFileSync(entryFile, `
79
- import cfg from './${path.basename(tmpFile)}';
80
- import fs from 'fs';
81
- const data = { collections: cfg.default?.collections || cfg?.collections || [], globals: cfg.default?.globals || cfg?.globals || [] };
82
- fs.writeFileSync('${path.basename(resultFile)}', JSON.stringify(data));
83
- `, "utf8");
84
- execSync(
85
- `npx tsx "${entryFile}"`,
86
- { cwd: path.dirname(resolvedConfig), encoding: "utf8", timeout: 15000, stdio: "pipe" },
87
- );
88
- const resultContent = fs.readFileSync(resultFile, "utf8");
89
- const configModule = JSON.parse(resultContent);
90
- try { fs.unlinkSync(resultFile); } catch {}
91
- if (configModule.error) {
92
- throw new Error(configModule.error);
75
+ fs.writeFileSync(tmpFile, result.outputFiles[0].text, "utf8");
76
+
77
+ // Use a Worker thread to load the config in an isolated context.
78
+ const workerCode = `
79
+ import { parentPort } from 'worker_threads';
80
+ import('${pathToFileURL(tmpFile).href}').then(mod => {
81
+ const cfg = mod.default || mod;
82
+ const serialize = (obj) => {
83
+ if (obj === null || obj === undefined) return obj;
84
+ if (typeof obj === 'function') return undefined;
85
+ if (Array.isArray(obj)) return obj.map(serialize);
86
+ if (typeof obj === 'object') {
87
+ const result = {};
88
+ for (const [k, v] of Object.entries(obj)) {
89
+ const sv = serialize(v);
90
+ if (sv !== undefined) result[k] = sv;
91
+ }
92
+ return result;
93
+ }
94
+ return obj;
95
+ };
96
+ parentPort.postMessage({
97
+ collections: serialize(cfg?.collections) || [],
98
+ globals: serialize(cfg?.globals) || [],
99
+ collectionOverrides: serialize(cfg?.admin?.collectionOverrides) || {},
100
+ });
101
+ }).catch(err => {
102
+ parentPort.postMessage({ error: err.message });
103
+ });
104
+ `;
105
+ const worker = new Worker(workerCode, { eval: true, env: { ...process.env, NODE_OPTIONS: '' } });
106
+ const configResult = await new Promise<any>((resolve, reject) => {
107
+ worker.on("message", resolve);
108
+ worker.on("error", reject);
109
+ const timer = setTimeout(() => {
110
+ worker.terminate();
111
+ reject(new Error("Config loading timed out"));
112
+ }, 30000);
113
+ });
114
+ worker.terminate();
115
+
116
+ if (configResult.error) {
117
+ throw new Error(configResult.error);
93
118
  }
94
- (globalThis as any).__KYRO_ADMIN_PROJECT_CONFIG__ = {
95
- collections: configModule.collections,
96
- globals: configModule.globals,
97
- adapter: configModule.adapter || null,
98
- };
119
+ fs.writeFileSync(configFile, JSON.stringify(configResult, null, 2), "utf8");
99
120
  logger.info("Project config loaded for admin");
100
121
  } catch (e: any) {
101
- logger.warn(`Could not load project config: ${e.message}`);
122
+ logger.error(`Could not load project config: ${e.message}`);
102
123
  } finally {
103
- for (const suffix of [".admin.mjs", ".admin-entry.mjs", ".admin-result.json"]) {
104
- const f = resolvedConfig.replace(/\.ts$/, suffix);
105
- if (fs.existsSync(f)) { try { fs.unlinkSync(f); } catch { /* ignore */ } }
106
- }
124
+ if (tmpFile && fs.existsSync(tmpFile)) { try { fs.unlinkSync(tmpFile); } catch { /* ignore */ } }
107
125
  }
108
126
 
109
- // Set up Vite aliases and defines for runtime use
127
+ // Set up Vite aliases, defines, and plugins for runtime use
110
128
  updateConfig({
111
129
  vite: {
130
+ plugins: [
131
+ {
132
+ name: "kyro-admin-tsx-loader",
133
+ enforce: "pre" as const,
134
+ config(_config: any) {
135
+ const existingEsbuild = _config.esbuild;
136
+ const existingExclude = existingEsbuild?.exclude;
137
+ const adminInclude = /\/node_modules\/(?!.*@kyro-cms\/(admin|core))/;
138
+ return {
139
+ esbuild: {
140
+ ...(existingEsbuild || {}),
141
+ exclude: existingExclude
142
+ ? Array.isArray(existingExclude)
143
+ ? [...existingExclude, adminInclude]
144
+ : [existingExclude, adminInclude]
145
+ : adminInclude,
146
+ },
147
+ };
148
+ },
149
+ },
150
+ {
151
+ name: "kyro-cjs-shim",
152
+ enforce: "pre" as const,
153
+ resolveId(id: string) {
154
+ if (id.includes('react/compiler-runtime')) {
155
+ return "\0react-compiler-runtime";
156
+ }
157
+ if (id === 'debug' || id.includes('debug/src/browser.js')) {
158
+ return "\0debug-browser";
159
+ }
160
+ },
161
+ load(id: string) {
162
+ if (id === "\0react-compiler-runtime") {
163
+ return `
164
+ import React from "react";
165
+ export function c(size) {
166
+ const internals = React.__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE;
167
+ if (!internals || !internals.H) {
168
+ return new Array(size);
169
+ }
170
+ return internals.H.useMemoCache(size);
171
+ }
172
+ `;
173
+ }
174
+ if (id === "\0debug-browser") {
175
+ return `
176
+ function debug(namespace) {
177
+ function d(...args) {
178
+ if (typeof localStorage !== "undefined" && localStorage.getItem("DEBUG")) {
179
+ console.log(namespace, ...args);
180
+ }
181
+ }
182
+ d.enabled = false;
183
+ return d;
184
+ }
185
+ debug.enable = function() {};
186
+ debug.disable = function() {};
187
+ debug.enabled = function() { return false; };
188
+ export default debug;
189
+ `;
190
+ }
191
+ },
192
+ },
193
+ ],
112
194
  resolve: {
113
195
  alias: {
114
196
  "kyro:config": resolvedConfig,
115
197
  },
116
198
  },
199
+ optimizeDeps: {
200
+ include: [
201
+ 'use-sync-external-store',
202
+ ],
203
+ exclude: ['debug', 'react/compiler-runtime'],
204
+ },
117
205
  define: {
118
206
  __KYRO_ADMIN_PATH__: JSON.stringify(basePath),
119
207
  __KYRO_API_PATH__: JSON.stringify(apiPath),
208
+ __KYRO_ADMIN_CONFIG_FILE__: JSON.stringify(configFile),
209
+ },
210
+ ssr: {
211
+ noExternal: ['@kyro-cms/admin', '@kyro-cms/core'],
120
212
  },
121
213
  },
122
214
  });
123
215
 
216
+ // Load the user's config at runtime via the kyro:config alias.
217
+ // We set a placeholder here; the actual config is loaded by
218
+ // admin/lib/config.ts via dynamic import of kyro:config.
219
+ (globalThis as any).__KYRO_ADMIN_PROJECT_CONFIG__ = {
220
+ collections: [],
221
+ globals: [],
222
+ adapter: null,
223
+ };
224
+
124
225
  // Inject Admin UI Routes
125
226
  const pages = [
126
227
  { pattern: "", entrypoint: "./pages/index.astro" },
@@ -128,6 +229,7 @@ export function kyroAdmin(options: KyroAdminOptions = {}): AstroIntegration {
128
229
  { pattern: "/register", entrypoint: "./pages/auth/register.astro" },
129
230
  { pattern: "/media", entrypoint: "./pages/media.astro" },
130
231
  { pattern: "/users", entrypoint: "./pages/users/index.astro" },
232
+ { pattern: "/users/new", entrypoint: "./pages/users/new.astro" },
131
233
  { pattern: "/users/[id]", entrypoint: "./pages/users/[id].astro" },
132
234
  { pattern: "/roles", entrypoint: "./pages/roles/index.astro" },
133
235
  { pattern: "/settings", entrypoint: "./pages/settings/index.astro" },
package/src/kyro-cms.d.ts CHANGED
@@ -57,7 +57,7 @@ admin?: {
57
57
  export interface SelectField extends FieldConfig { type: 'select'; options?: { label: string; value: string | number }[] }
58
58
  export interface TextareaField extends FieldConfig { type: 'textarea' }
59
59
  export interface MarkdownField extends FieldConfig { type: 'markdown' }
60
- export interface RichTextField extends FieldConfig { type: 'richText' }
60
+ export interface RichTextField extends FieldConfig { type: 'richtext' }
61
61
  export interface CodeField extends FieldConfig { type: 'code'; language?: string }
62
62
  export interface JSONField extends FieldConfig { type: 'json' }
63
63
  export interface ImageField extends FieldConfig { type: 'image' }
@@ -208,4 +208,9 @@ declare module 'kyro:config' {
208
208
  import type { KyroConfig } from '@kyro-cms/core';
209
209
  const config: KyroConfig;
210
210
  export default config;
211
- }
211
+ }
212
+
213
+ // Injected by Vite's define config during admin integration setup
214
+ declare const __KYRO_ADMIN_CONFIG_FILE__: string;
215
+ declare const __KYRO_ADMIN_PATH__: string;
216
+ declare const __KYRO_API_PATH__: string;
@@ -7,7 +7,7 @@ import { adminPath, apiPath } from "../lib/paths";
7
7
  import { AuthBridge } from "../components/AuthBridge";
8
8
  import { GlobalModal } from "../components/ui/GlobalModal";
9
9
  import { Toaster } from "../components/ui/Toaster";
10
- import { getSiteSettings } from "../lib/globals";
10
+ import { getSiteSettings, getGlobal } from "../lib/globals";
11
11
 
12
12
  interface Props {
13
13
  title: string;
@@ -15,8 +15,25 @@ interface Props {
15
15
 
16
16
  const { title } = Astro.props;
17
17
  const siteSettings = await getSiteSettings({ request: Astro.request });
18
+ const seoSettings = await getGlobal("seo-settings", { request: Astro.request });
19
+
18
20
  const siteName = siteSettings?.siteName || "Kyro CMS";
19
21
  const siteFavicon = siteSettings?.siteFavicon;
22
+ const siteDescription = siteSettings?.siteDescription || "";
23
+
24
+ // SEO Logic
25
+ const seoMode = seoSettings?.seoMode || "simple";
26
+ const includeSiteName = seoSettings?.siteNameInTitle ?? true;
27
+ const titleSeparator = seoSettings?.separator || " - ";
28
+ const defaultTitle = seoSettings?.defaultTitle || title;
29
+ const defaultDescription = seoSettings?.defaultDescription || siteDescription;
30
+ const canonicalUrl = seoMode === "advanced" ? seoSettings?.meta?.canonicalUrl : "";
31
+ const robots = seoMode === "advanced" && seoSettings?.meta?.robots ? seoSettings.meta.robots : "noindex, nofollow";
32
+
33
+ let displayTitle = title === "Dashboard" && defaultTitle !== "Dashboard" ? defaultTitle : title;
34
+ if (includeSiteName) {
35
+ displayTitle = `${displayTitle}${titleSeparator}${siteName}`;
36
+ }
20
37
  ---
21
38
 
22
39
  <!doctype html>
@@ -24,7 +41,10 @@ const siteFavicon = siteSettings?.siteFavicon;
24
41
  <head>
25
42
  <meta charset="UTF-8" />
26
43
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
27
- <title>{title} - {siteName}</title>
44
+ <title>{displayTitle}</title>
45
+ {defaultDescription && <meta name="description" content={defaultDescription} />}
46
+ {robots && <meta name="robots" content={robots} />}
47
+ {canonicalUrl && <link rel="canonical" href={canonicalUrl} />}
28
48
  <link
29
49
  rel="icon"
30
50
  type={siteFavicon?.mimeType || "image/svg+xml"}
@@ -1,12 +1,37 @@
1
1
  ---
2
2
  import "../styles/main.css";
3
3
  import { adminPath, apiPath } from "../lib/paths";
4
+ import { getSiteSettings, getGlobal } from "../lib/globals";
4
5
 
5
6
  interface Props {
6
7
  title: string;
7
8
  }
8
9
 
9
10
  const { title } = Astro.props;
11
+ const siteSettings = await getSiteSettings({ request: Astro.request });
12
+ const seoSettings = await getGlobal("seo-settings", { request: Astro.request });
13
+
14
+ const siteName = siteSettings?.siteName || "Kyro CMS";
15
+ const siteFavicon = siteSettings?.siteFavicon;
16
+ const siteLogo = siteSettings?.siteLogo;
17
+ const logoWidth = siteSettings?.logo?.width;
18
+ const logoHeight = siteSettings?.logo?.height;
19
+ const logoAlt = siteSettings?.logo?.altText || siteName;
20
+ const siteDescription = siteSettings?.siteDescription || "";
21
+
22
+ // SEO Logic
23
+ const seoMode = seoSettings?.seoMode || "simple";
24
+ const includeSiteName = seoSettings?.siteNameInTitle ?? true;
25
+ const titleSeparator = seoSettings?.separator || " - ";
26
+ const defaultTitle = seoSettings?.defaultTitle || title;
27
+ const defaultDescription = seoSettings?.defaultDescription || siteDescription;
28
+ const canonicalUrl = seoMode === "advanced" ? seoSettings?.meta?.canonicalUrl : "";
29
+ const robots = seoMode === "advanced" && seoSettings?.meta?.robots ? seoSettings.meta.robots : "noindex, nofollow";
30
+
31
+ let displayTitle = title === "Login" && defaultTitle !== "Login" ? defaultTitle : title;
32
+ if (includeSiteName) {
33
+ displayTitle = `${displayTitle}${titleSeparator}${siteName}`;
34
+ }
10
35
  ---
11
36
 
12
37
  <!doctype html>
@@ -14,8 +39,15 @@ const { title } = Astro.props;
14
39
  <head>
15
40
  <meta charset="UTF-8" />
16
41
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
17
- <title>{title} - Kyro CMS</title>
18
- <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
42
+ <title>{displayTitle}</title>
43
+ {defaultDescription && <meta name="description" content={defaultDescription} />}
44
+ {robots && <meta name="robots" content={robots} />}
45
+ {canonicalUrl && <link rel="canonical" href={canonicalUrl} />}
46
+ <link
47
+ rel="icon"
48
+ type={siteFavicon?.mimeType || "image/svg+xml"}
49
+ href={siteFavicon?.url || "/favicon.svg"}
50
+ />
19
51
  <script is:inline define:vars={{ adminPath, apiPath }}>
20
52
  (async () => {
21
53
  try {
@@ -49,11 +81,39 @@ const { title } = Astro.props;
49
81
  <div class="w-full max-w-md flex flex-col items-center">
50
82
  <!-- Logo -->
51
83
  <div class="w-full text-center mb-8">
52
- <a href="/" class="inline-block">
53
- <span
54
- class="text-xl font-bold tracking-tighter text-[var(--kyro-text-primary)]"
55
- >KYRO.</span
56
- >
84
+ <a href="/" class="inline-flex items-center justify-center gap-2">
85
+ {
86
+ siteLogo ? (
87
+ <img
88
+ src={siteLogo.url}
89
+ alt={logoAlt}
90
+ style={{
91
+ width: logoWidth ? `${logoWidth}px` : "auto",
92
+ height: logoHeight ? `${logoHeight}px` : "40px",
93
+ objectFit: "contain",
94
+ }}
95
+ class="rounded-lg"
96
+ />
97
+ ) : (
98
+ <>
99
+ <svg class="w-7 h-7 text-[var(--kyro-text-primary)]" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
100
+ <defs>
101
+ <mask id="kyro-cutout-auth">
102
+ <rect width="100" height="100" fill="white" />
103
+ <path d="M 40 89.72 L 40 40 L 89.72 40 A 45 45 0 0 1 40 89.72 Z" stroke="black" stroke-width="8" fill="none" />
104
+ </mask>
105
+ </defs>
106
+ <g mask="url(#kyro-cutout-auth)" fill="currentColor">
107
+ <circle cx="45" cy="45" r="45" />
108
+ <rect x="40" y="40" width="60" height="60" />
109
+ </g>
110
+ </svg>
111
+ <span class="text-xl font-bold tracking-tighter text-[var(--kyro-text-primary)]">
112
+ {siteName}
113
+ </span>
114
+ </>
115
+ )
116
+ }
57
117
  </a>
58
118
  </div>
59
119