@realiizlabs/admin 0.11.2 → 0.13.1

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 (47) hide show
  1. package/dist/Sidebar-CNuphww6.d.cts +60 -0
  2. package/dist/Sidebar-CNuphww6.d.ts +60 -0
  3. package/dist/auth/index.d.cts +5 -127
  4. package/dist/auth/index.d.ts +5 -127
  5. package/dist/auth-ui/index.js +124 -1
  6. package/dist/auth-ui/index.js.map +1 -1
  7. package/dist/forms/index.js +287 -1
  8. package/dist/forms/index.js.map +1 -1
  9. package/dist/forms-ui/index.js +355 -6
  10. package/dist/forms-ui/index.js.map +1 -1
  11. package/dist/git/index.cjs.map +1 -1
  12. package/dist/git/index.d.cts +3 -77
  13. package/dist/git/index.d.ts +3 -77
  14. package/dist/git/index.js.map +1 -1
  15. package/dist/media/index.cjs +33 -0
  16. package/dist/media/index.cjs.map +1 -1
  17. package/dist/media/index.d.cts +2 -0
  18. package/dist/media/index.d.ts +2 -0
  19. package/dist/media/index.js +30 -1
  20. package/dist/media/index.js.map +1 -1
  21. package/dist/members-nhbRSYyr.d.cts +106 -0
  22. package/dist/members-nhbRSYyr.d.ts +106 -0
  23. package/dist/roles-BjWuj2_v.d.cts +25 -0
  24. package/dist/roles-Dnwj-DAm.d.ts +25 -0
  25. package/dist/shell/index.d.cts +3 -58
  26. package/dist/shell/index.d.ts +3 -58
  27. package/dist/studio/index.cjs +1150 -0
  28. package/dist/studio/index.cjs.map +1 -0
  29. package/dist/studio/index.d.cts +287 -0
  30. package/dist/studio/index.d.ts +287 -0
  31. package/dist/studio/index.js +1131 -0
  32. package/dist/studio/index.js.map +1 -0
  33. package/dist/studio-ui/index.cjs +3931 -0
  34. package/dist/studio-ui/index.cjs.map +1 -0
  35. package/dist/studio-ui/index.d.cts +375 -0
  36. package/dist/studio-ui/index.d.ts +375 -0
  37. package/dist/studio-ui/index.js +3900 -0
  38. package/dist/studio-ui/index.js.map +1 -0
  39. package/dist/types-BP5G1myE.d.cts +78 -0
  40. package/dist/types-BP5G1myE.d.ts +78 -0
  41. package/dist/validate-C84Ll1cD.d.cts +41 -0
  42. package/dist/validate-C84Ll1cD.d.ts +41 -0
  43. package/package.json +20 -2
  44. package/dist/chunk-2GSYBARR.js +0 -126
  45. package/dist/chunk-2GSYBARR.js.map +0 -1
  46. package/dist/chunk-JLR5K6RP.js +0 -289
  47. package/dist/chunk-JLR5K6RP.js.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/media/errors.ts","../../src/media/resize.ts","../../src/media/paths.ts"],"names":[],"mappings":"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","file":"index.cjs","sourcesContent":["/** A picture problem the person can act on. The message is the sentence shown under the control. */\nexport class MediaError extends Error {\n constructor(message: string) {\n super(message);\n this.name = \"MediaError\";\n }\n}\n\nexport const MESSAGES = {\n type: \"That file type isn’t supported — use a JPG, PNG or WebP.\",\n size: \"That picture is too big to send — pick one under 10MB.\",\n encode: \"This browser can’t prepare pictures — try Chrome, Edge or Safari 16+.\",\n} as const;\n","/**\n * resizeImage — shrink a picture in the browser to web size, as WebP.\n *\n * Phone photos are 3–8MB; a Vercel server action body is capped at 4.5MB and the\n * GitHub API wants base64. So the shrink happens here, before anything leaves the\n * browser: decode → fit inside maxWidth → draw to a canvas → encode WebP, lowering\n * quality in steps until the result is under maxBytes. GIFs lose their animation\n * (a still frame is encoded) — acceptable for a blog.\n *\n * No network, no dependencies. Throws MediaError with a plain sentence for the\n * three things that can go wrong: wrong type, too big to even try, or a browser\n * that cannot encode WebP (toBlob returns null or a non-WebP type).\n */\n\nimport { MediaError, MESSAGES } from \"./errors\";\n\nexport const ACCEPTED_TYPES = [\"image/jpeg\", \"image/png\", \"image/webp\", \"image/gif\"] as const;\nexport const MAX_INPUT_BYTES = 10 * 1024 * 1024;\n\nexport interface ResizeOptions {\n /** Longest allowed width in pixels. Default 1600. */\n maxWidth?: number;\n /** Starting WebP quality 0–1. Default 0.82. */\n quality?: number;\n /** Target ceiling for the encoded size. Default 600KB. */\n maxBytes?: number;\n}\n\nexport interface ResizedImage {\n blob: Blob;\n width: number;\n height: number;\n}\n\n/** Pure: fit (width × height) inside maxWidth, never scaling up, whole pixels. */\nexport function fitWithin(width: number, height: number, maxWidth: number): { width: number; height: number } {\n if (width <= maxWidth) return { width, height };\n const ratio = maxWidth / width;\n return { width: Math.round(width * ratio), height: Math.max(1, Math.round(height * ratio)) };\n}\n\n/** The type/size guards, separated so they can run (and be tested) without a canvas. */\nexport function checkInput(file: { type: string; size: number }): void {\n if (!(ACCEPTED_TYPES as readonly string[]).includes(file.type)) throw new MediaError(MESSAGES.type);\n if (file.size > MAX_INPUT_BYTES) throw new MediaError(MESSAGES.size);\n}\n\nexport async function resizeImage(file: File, opts: ResizeOptions = {}): Promise<ResizedImage> {\n checkInput(file);\n const { maxWidth = 1600, quality = 0.82, maxBytes = 600 * 1024 } = opts;\n\n const bitmap = await decode(file);\n const { width, height } = fitWithin(bitmap.width, bitmap.height, maxWidth);\n const canvas = makeCanvas(width, height);\n const ctx = canvas.getContext(\"2d\");\n if (!ctx) throw new MediaError(MESSAGES.encode);\n ctx.drawImage(bitmap, 0, 0, width, height);\n bitmap.close?.();\n\n let q = quality;\n let blob = await encode(canvas, q);\n while (blob.size > maxBytes && q - 0.08 >= 0.5) {\n q -= 0.08;\n blob = await encode(canvas, q);\n }\n return { blob, width, height };\n}\n\nasync function decode(file: File): Promise<ImageBitmap> {\n try {\n return await createImageBitmap(file);\n } catch {\n throw new MediaError(MESSAGES.encode);\n }\n}\n\ntype AnyCanvas = HTMLCanvasElement | OffscreenCanvas;\n\nfunction makeCanvas(width: number, height: number): AnyCanvas {\n if (typeof OffscreenCanvas !== \"undefined\") return new OffscreenCanvas(width, height);\n const c = document.createElement(\"canvas\");\n c.width = width;\n c.height = height;\n return c;\n}\n\nasync function encode(canvas: AnyCanvas, quality: number): Promise<Blob> {\n const blob =\n \"convertToBlob\" in canvas\n ? await canvas.convertToBlob({ type: \"image/webp\", quality }).catch(() => null)\n : await new Promise<Blob | null>((resolve) => canvas.toBlob(resolve, \"image/webp\", quality));\n if (!blob || blob.type !== \"image/webp\") throw new MediaError(MESSAGES.encode);\n return blob;\n}\n","/**\n * Where a picture lives, decided from the post's slug so the path is known before\n * anything is committed: hero → /blog/{slug}.webp, social → /blog/{slug}-social.webp,\n * body → /blog/{slug}/{name}.webp. The host commits the file under public/ + path.\n */\n\nexport type ImageKind = \"hero\" | \"social\" | \"body\";\n\nexport interface PathOptions {\n /** URL prefix the pictures are served from. Default \"/blog\". */\n publicDir?: string;\n}\n\n/** Lower-case letters, digits and single hyphens; empty in → \"image\". */\nexport function slugifyName(name: string): string {\n const base = name.replace(/\\.[a-z0-9]+$/i, \"\");\n const s = base\n .normalize(\"NFKD\")\n .replace(/[̀-ͯ]/g, \"\")\n .toLowerCase()\n .replace(/[^a-z0-9]+/g, \"-\")\n .replace(/^-+|-+$/g, \"\");\n return s || \"image\";\n}\n\nexport function imagePathFor(kind: ImageKind, slug: string, originalName = \"\", opts: PathOptions = {}): string {\n const dir = (opts.publicDir ?? \"/blog\").replace(/\\/$/, \"\");\n switch (kind) {\n case \"hero\":\n return `${dir}/${slug}.webp`;\n case \"social\":\n return `${dir}/${slug}-social.webp`;\n case \"body\":\n return `${dir}/${slug}/${slugifyName(originalName)}.webp`;\n }\n}\n"]}
1
+ {"version":3,"sources":["../../src/media/errors.ts","../../src/media/resize.ts","../../src/media/paths.ts","../../src/media/validate.ts"],"names":[],"mappings":"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","file":"index.cjs","sourcesContent":["/** A picture problem the person can act on. The message is the sentence shown under the control. */\nexport class MediaError extends Error {\n constructor(message: string) {\n super(message);\n this.name = \"MediaError\";\n }\n}\n\nexport const MESSAGES = {\n type: \"That file type isn’t supported — use a JPG, PNG or WebP.\",\n size: \"That picture is too big to send — pick one under 10MB.\",\n encode: \"This browser can’t prepare pictures — try Chrome, Edge or Safari 16+.\",\n} as const;\n","/**\n * resizeImage — shrink a picture in the browser to web size, as WebP.\n *\n * Phone photos are 3–8MB; a Vercel server action body is capped at 4.5MB and the\n * GitHub API wants base64. So the shrink happens here, before anything leaves the\n * browser: decode → fit inside maxWidth → draw to a canvas → encode WebP, lowering\n * quality in steps until the result is under maxBytes. GIFs lose their animation\n * (a still frame is encoded) — acceptable for a blog.\n *\n * No network, no dependencies. Throws MediaError with a plain sentence for the\n * three things that can go wrong: wrong type, too big to even try, or a browser\n * that cannot encode WebP (toBlob returns null or a non-WebP type).\n */\n\nimport { MediaError, MESSAGES } from \"./errors\";\n\nexport const ACCEPTED_TYPES = [\"image/jpeg\", \"image/png\", \"image/webp\", \"image/gif\"] as const;\nexport const MAX_INPUT_BYTES = 10 * 1024 * 1024;\n\nexport interface ResizeOptions {\n /** Longest allowed width in pixels. Default 1600. */\n maxWidth?: number;\n /** Starting WebP quality 0–1. Default 0.82. */\n quality?: number;\n /** Target ceiling for the encoded size. Default 600KB. */\n maxBytes?: number;\n}\n\nexport interface ResizedImage {\n blob: Blob;\n width: number;\n height: number;\n}\n\n/** Pure: fit (width × height) inside maxWidth, never scaling up, whole pixels. */\nexport function fitWithin(width: number, height: number, maxWidth: number): { width: number; height: number } {\n if (width <= maxWidth) return { width, height };\n const ratio = maxWidth / width;\n return { width: Math.round(width * ratio), height: Math.max(1, Math.round(height * ratio)) };\n}\n\n/** The type/size guards, separated so they can run (and be tested) without a canvas. */\nexport function checkInput(file: { type: string; size: number }): void {\n if (!(ACCEPTED_TYPES as readonly string[]).includes(file.type)) throw new MediaError(MESSAGES.type);\n if (file.size > MAX_INPUT_BYTES) throw new MediaError(MESSAGES.size);\n}\n\nexport async function resizeImage(file: File, opts: ResizeOptions = {}): Promise<ResizedImage> {\n checkInput(file);\n const { maxWidth = 1600, quality = 0.82, maxBytes = 600 * 1024 } = opts;\n\n const bitmap = await decode(file);\n const { width, height } = fitWithin(bitmap.width, bitmap.height, maxWidth);\n const canvas = makeCanvas(width, height);\n const ctx = canvas.getContext(\"2d\");\n if (!ctx) throw new MediaError(MESSAGES.encode);\n ctx.drawImage(bitmap, 0, 0, width, height);\n bitmap.close?.();\n\n let q = quality;\n let blob = await encode(canvas, q);\n while (blob.size > maxBytes && q - 0.08 >= 0.5) {\n q -= 0.08;\n blob = await encode(canvas, q);\n }\n return { blob, width, height };\n}\n\nasync function decode(file: File): Promise<ImageBitmap> {\n try {\n return await createImageBitmap(file);\n } catch {\n throw new MediaError(MESSAGES.encode);\n }\n}\n\ntype AnyCanvas = HTMLCanvasElement | OffscreenCanvas;\n\nfunction makeCanvas(width: number, height: number): AnyCanvas {\n if (typeof OffscreenCanvas !== \"undefined\") return new OffscreenCanvas(width, height);\n const c = document.createElement(\"canvas\");\n c.width = width;\n c.height = height;\n return c;\n}\n\nasync function encode(canvas: AnyCanvas, quality: number): Promise<Blob> {\n const blob =\n \"convertToBlob\" in canvas\n ? await canvas.convertToBlob({ type: \"image/webp\", quality }).catch(() => null)\n : await new Promise<Blob | null>((resolve) => canvas.toBlob(resolve, \"image/webp\", quality));\n if (!blob || blob.type !== \"image/webp\") throw new MediaError(MESSAGES.encode);\n return blob;\n}\n","/**\n * Where a picture lives, decided from the post's slug so the path is known before\n * anything is committed: hero → /blog/{slug}.webp, social → /blog/{slug}-social.webp,\n * body → /blog/{slug}/{name}.webp. The host commits the file under public/ + path.\n */\n\nexport type ImageKind = \"hero\" | \"social\" | \"body\";\n\nexport interface PathOptions {\n /** URL prefix the pictures are served from. Default \"/blog\". */\n publicDir?: string;\n}\n\n/** Lower-case letters, digits and single hyphens; empty in → \"image\". */\nexport function slugifyName(name: string): string {\n const base = name.replace(/\\.[a-z0-9]+$/i, \"\");\n const s = base\n .normalize(\"NFKD\")\n .replace(/[̀-ͯ]/g, \"\")\n .toLowerCase()\n .replace(/[^a-z0-9]+/g, \"-\")\n .replace(/^-+|-+$/g, \"\");\n return s || \"image\";\n}\n\nexport function imagePathFor(kind: ImageKind, slug: string, originalName = \"\", opts: PathOptions = {}): string {\n const dir = (opts.publicDir ?? \"/blog\").replace(/\\/$/, \"\");\n switch (kind) {\n case \"hero\":\n return `${dir}/${slug}.webp`;\n case \"social\":\n return `${dir}/${slug}-social.webp`;\n case \"body\":\n return `${dir}/${slug}/${slugifyName(originalName)}.webp`;\n }\n}\n","/**\n * Server-side checks on the pictures a save carries. Pure.\n *\n * The browser already shrank them, but anyone can POST to a server action, so\n * the path, type and size are re-checked before anything is committed: only\n * WebP under the type's public folder, ≤ 800 KB each, ≤ 3.5 MB per save.\n * Frontmatter paths are URL paths (/blog/x.webp); the repo file lives under public/.\n */\n\nexport interface ImagePayload {\n /** URL path as written in the frontmatter or body, e.g. /blog/my-post.webp. */\n path: string;\n /** Base64 of the WebP bytes. */\n base64: string;\n}\n\nexport interface ImageLimits {\n maxEach: number;\n maxTotal: number;\n /** URL prefix pictures must live under. */\n prefix: string;\n}\n\nexport const IMAGE_LIMITS: ImageLimits = { maxEach: 800 * 1024, maxTotal: 3.5 * 1024 * 1024, prefix: \"/blog/\" };\n\nexport const IMAGE_ERRORS = {\n path: \"That image path isn’t allowed.\",\n size: \"That picture is too large to send — try a smaller one.\",\n} as const;\n\nexport type ValidatedImages =\n | { ok: true; files: { path: string; content: string; encoding: \"base64\" }[] }\n | { ok: false; error: string };\n\nconst SAFE_PATH = /^[a-z0-9][a-z0-9/_-]*\\.webp$/;\n\n/** Bytes a base64 string decodes to, without decoding it. */\nexport function decodedSize(b64: string): number {\n const clean = b64.replace(/\\s+/g, \"\");\n const pad = clean.endsWith(\"==\") ? 2 : clean.endsWith(\"=\") ? 1 : 0;\n return Math.floor((clean.length * 3) / 4) - pad;\n}\n\nexport function validateImages(images: ImagePayload[], limits: ImageLimits = IMAGE_LIMITS): ValidatedImages {\n let total = 0;\n const files: { path: string; content: string; encoding: \"base64\" }[] = [];\n for (const img of images) {\n if (typeof img?.path !== \"string\" || typeof img?.base64 !== \"string\") return { ok: false, error: IMAGE_ERRORS.path };\n if (!img.path.startsWith(limits.prefix) || img.path.includes(\"..\")) return { ok: false, error: IMAGE_ERRORS.path };\n const rel = img.path.slice(1);\n if (!SAFE_PATH.test(rel)) return { ok: false, error: IMAGE_ERRORS.path };\n const size = decodedSize(img.base64);\n if (size === 0 || size > limits.maxEach) return { ok: false, error: IMAGE_ERRORS.size };\n total += size;\n if (total > limits.maxTotal) return { ok: false, error: IMAGE_ERRORS.size };\n files.push({ path: `public/${rel}`, content: img.base64.replace(/\\s+/g, \"\"), encoding: \"base64\" });\n }\n return { ok: true, files };\n}\n"]}
@@ -1,3 +1,5 @@
1
+ export { I as IMAGE_ERRORS, a as IMAGE_LIMITS, b as ImageLimits, c as ImagePayload, V as ValidatedImages, d as decodedSize, v as validateImages } from '../validate-C84Ll1cD.cjs';
2
+
1
3
  /**
2
4
  * resizeImage — shrink a picture in the browser to web size, as WebP.
3
5
  *
@@ -1,3 +1,5 @@
1
+ export { I as IMAGE_ERRORS, a as IMAGE_LIMITS, b as ImageLimits, c as ImagePayload, V as ValidatedImages, d as decodedSize, v as validateImages } from '../validate-C84Ll1cD.js';
2
+
1
3
  /**
2
4
  * resizeImage — shrink a picture in the browser to web size, as WebP.
3
5
  *
@@ -80,6 +80,35 @@ function imagePathFor(kind, slug, originalName = "", opts = {}) {
80
80
  }
81
81
  }
82
82
 
83
- export { ACCEPTED_TYPES, MAX_INPUT_BYTES, MESSAGES as MEDIA_MESSAGES, MediaError, checkInput, fitWithin, imagePathFor, resizeImage, slugifyName };
83
+ // src/media/validate.ts
84
+ var IMAGE_LIMITS = { maxEach: 800 * 1024, maxTotal: 3.5 * 1024 * 1024, prefix: "/blog/" };
85
+ var IMAGE_ERRORS = {
86
+ path: "That image path isn\u2019t allowed.",
87
+ size: "That picture is too large to send \u2014 try a smaller one."
88
+ };
89
+ var SAFE_PATH = /^[a-z0-9][a-z0-9/_-]*\.webp$/;
90
+ function decodedSize(b64) {
91
+ const clean = b64.replace(/\s+/g, "");
92
+ const pad = clean.endsWith("==") ? 2 : clean.endsWith("=") ? 1 : 0;
93
+ return Math.floor(clean.length * 3 / 4) - pad;
94
+ }
95
+ function validateImages(images, limits = IMAGE_LIMITS) {
96
+ let total = 0;
97
+ const files = [];
98
+ for (const img of images) {
99
+ if (typeof img?.path !== "string" || typeof img?.base64 !== "string") return { ok: false, error: IMAGE_ERRORS.path };
100
+ if (!img.path.startsWith(limits.prefix) || img.path.includes("..")) return { ok: false, error: IMAGE_ERRORS.path };
101
+ const rel = img.path.slice(1);
102
+ if (!SAFE_PATH.test(rel)) return { ok: false, error: IMAGE_ERRORS.path };
103
+ const size = decodedSize(img.base64);
104
+ if (size === 0 || size > limits.maxEach) return { ok: false, error: IMAGE_ERRORS.size };
105
+ total += size;
106
+ if (total > limits.maxTotal) return { ok: false, error: IMAGE_ERRORS.size };
107
+ files.push({ path: `public/${rel}`, content: img.base64.replace(/\s+/g, ""), encoding: "base64" });
108
+ }
109
+ return { ok: true, files };
110
+ }
111
+
112
+ export { ACCEPTED_TYPES, IMAGE_ERRORS, IMAGE_LIMITS, MAX_INPUT_BYTES, MESSAGES as MEDIA_MESSAGES, MediaError, checkInput, decodedSize, fitWithin, imagePathFor, resizeImage, slugifyName, validateImages };
84
113
  //# sourceMappingURL=index.js.map
85
114
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/media/errors.ts","../../src/media/resize.ts","../../src/media/paths.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["/** A picture problem the person can act on. The message is the sentence shown under the control. */\nexport class MediaError extends Error {\n constructor(message: string) {\n super(message);\n this.name = \"MediaError\";\n }\n}\n\nexport const MESSAGES = {\n type: \"That file type isn’t supported — use a JPG, PNG or WebP.\",\n size: \"That picture is too big to send — pick one under 10MB.\",\n encode: \"This browser can’t prepare pictures — try Chrome, Edge or Safari 16+.\",\n} as const;\n","/**\n * resizeImage — shrink a picture in the browser to web size, as WebP.\n *\n * Phone photos are 3–8MB; a Vercel server action body is capped at 4.5MB and the\n * GitHub API wants base64. So the shrink happens here, before anything leaves the\n * browser: decode → fit inside maxWidth → draw to a canvas → encode WebP, lowering\n * quality in steps until the result is under maxBytes. GIFs lose their animation\n * (a still frame is encoded) — acceptable for a blog.\n *\n * No network, no dependencies. Throws MediaError with a plain sentence for the\n * three things that can go wrong: wrong type, too big to even try, or a browser\n * that cannot encode WebP (toBlob returns null or a non-WebP type).\n */\n\nimport { MediaError, MESSAGES } from \"./errors\";\n\nexport const ACCEPTED_TYPES = [\"image/jpeg\", \"image/png\", \"image/webp\", \"image/gif\"] as const;\nexport const MAX_INPUT_BYTES = 10 * 1024 * 1024;\n\nexport interface ResizeOptions {\n /** Longest allowed width in pixels. Default 1600. */\n maxWidth?: number;\n /** Starting WebP quality 0–1. Default 0.82. */\n quality?: number;\n /** Target ceiling for the encoded size. Default 600KB. */\n maxBytes?: number;\n}\n\nexport interface ResizedImage {\n blob: Blob;\n width: number;\n height: number;\n}\n\n/** Pure: fit (width × height) inside maxWidth, never scaling up, whole pixels. */\nexport function fitWithin(width: number, height: number, maxWidth: number): { width: number; height: number } {\n if (width <= maxWidth) return { width, height };\n const ratio = maxWidth / width;\n return { width: Math.round(width * ratio), height: Math.max(1, Math.round(height * ratio)) };\n}\n\n/** The type/size guards, separated so they can run (and be tested) without a canvas. */\nexport function checkInput(file: { type: string; size: number }): void {\n if (!(ACCEPTED_TYPES as readonly string[]).includes(file.type)) throw new MediaError(MESSAGES.type);\n if (file.size > MAX_INPUT_BYTES) throw new MediaError(MESSAGES.size);\n}\n\nexport async function resizeImage(file: File, opts: ResizeOptions = {}): Promise<ResizedImage> {\n checkInput(file);\n const { maxWidth = 1600, quality = 0.82, maxBytes = 600 * 1024 } = opts;\n\n const bitmap = await decode(file);\n const { width, height } = fitWithin(bitmap.width, bitmap.height, maxWidth);\n const canvas = makeCanvas(width, height);\n const ctx = canvas.getContext(\"2d\");\n if (!ctx) throw new MediaError(MESSAGES.encode);\n ctx.drawImage(bitmap, 0, 0, width, height);\n bitmap.close?.();\n\n let q = quality;\n let blob = await encode(canvas, q);\n while (blob.size > maxBytes && q - 0.08 >= 0.5) {\n q -= 0.08;\n blob = await encode(canvas, q);\n }\n return { blob, width, height };\n}\n\nasync function decode(file: File): Promise<ImageBitmap> {\n try {\n return await createImageBitmap(file);\n } catch {\n throw new MediaError(MESSAGES.encode);\n }\n}\n\ntype AnyCanvas = HTMLCanvasElement | OffscreenCanvas;\n\nfunction makeCanvas(width: number, height: number): AnyCanvas {\n if (typeof OffscreenCanvas !== \"undefined\") return new OffscreenCanvas(width, height);\n const c = document.createElement(\"canvas\");\n c.width = width;\n c.height = height;\n return c;\n}\n\nasync function encode(canvas: AnyCanvas, quality: number): Promise<Blob> {\n const blob =\n \"convertToBlob\" in canvas\n ? await canvas.convertToBlob({ type: \"image/webp\", quality }).catch(() => null)\n : await new Promise<Blob | null>((resolve) => canvas.toBlob(resolve, \"image/webp\", quality));\n if (!blob || blob.type !== \"image/webp\") throw new MediaError(MESSAGES.encode);\n return blob;\n}\n","/**\n * Where a picture lives, decided from the post's slug so the path is known before\n * anything is committed: hero → /blog/{slug}.webp, social → /blog/{slug}-social.webp,\n * body → /blog/{slug}/{name}.webp. The host commits the file under public/ + path.\n */\n\nexport type ImageKind = \"hero\" | \"social\" | \"body\";\n\nexport interface PathOptions {\n /** URL prefix the pictures are served from. Default \"/blog\". */\n publicDir?: string;\n}\n\n/** Lower-case letters, digits and single hyphens; empty in → \"image\". */\nexport function slugifyName(name: string): string {\n const base = name.replace(/\\.[a-z0-9]+$/i, \"\");\n const s = base\n .normalize(\"NFKD\")\n .replace(/[̀-ͯ]/g, \"\")\n .toLowerCase()\n .replace(/[^a-z0-9]+/g, \"-\")\n .replace(/^-+|-+$/g, \"\");\n return s || \"image\";\n}\n\nexport function imagePathFor(kind: ImageKind, slug: string, originalName = \"\", opts: PathOptions = {}): string {\n const dir = (opts.publicDir ?? \"/blog\").replace(/\\/$/, \"\");\n switch (kind) {\n case \"hero\":\n return `${dir}/${slug}.webp`;\n case \"social\":\n return `${dir}/${slug}-social.webp`;\n case \"body\":\n return `${dir}/${slug}/${slugifyName(originalName)}.webp`;\n }\n}\n"]}
1
+ {"version":3,"sources":["../../src/media/errors.ts","../../src/media/resize.ts","../../src/media/paths.ts","../../src/media/validate.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["/** A picture problem the person can act on. The message is the sentence shown under the control. */\nexport class MediaError extends Error {\n constructor(message: string) {\n super(message);\n this.name = \"MediaError\";\n }\n}\n\nexport const MESSAGES = {\n type: \"That file type isn’t supported — use a JPG, PNG or WebP.\",\n size: \"That picture is too big to send — pick one under 10MB.\",\n encode: \"This browser can’t prepare pictures — try Chrome, Edge or Safari 16+.\",\n} as const;\n","/**\n * resizeImage — shrink a picture in the browser to web size, as WebP.\n *\n * Phone photos are 3–8MB; a Vercel server action body is capped at 4.5MB and the\n * GitHub API wants base64. So the shrink happens here, before anything leaves the\n * browser: decode → fit inside maxWidth → draw to a canvas → encode WebP, lowering\n * quality in steps until the result is under maxBytes. GIFs lose their animation\n * (a still frame is encoded) — acceptable for a blog.\n *\n * No network, no dependencies. Throws MediaError with a plain sentence for the\n * three things that can go wrong: wrong type, too big to even try, or a browser\n * that cannot encode WebP (toBlob returns null or a non-WebP type).\n */\n\nimport { MediaError, MESSAGES } from \"./errors\";\n\nexport const ACCEPTED_TYPES = [\"image/jpeg\", \"image/png\", \"image/webp\", \"image/gif\"] as const;\nexport const MAX_INPUT_BYTES = 10 * 1024 * 1024;\n\nexport interface ResizeOptions {\n /** Longest allowed width in pixels. Default 1600. */\n maxWidth?: number;\n /** Starting WebP quality 0–1. Default 0.82. */\n quality?: number;\n /** Target ceiling for the encoded size. Default 600KB. */\n maxBytes?: number;\n}\n\nexport interface ResizedImage {\n blob: Blob;\n width: number;\n height: number;\n}\n\n/** Pure: fit (width × height) inside maxWidth, never scaling up, whole pixels. */\nexport function fitWithin(width: number, height: number, maxWidth: number): { width: number; height: number } {\n if (width <= maxWidth) return { width, height };\n const ratio = maxWidth / width;\n return { width: Math.round(width * ratio), height: Math.max(1, Math.round(height * ratio)) };\n}\n\n/** The type/size guards, separated so they can run (and be tested) without a canvas. */\nexport function checkInput(file: { type: string; size: number }): void {\n if (!(ACCEPTED_TYPES as readonly string[]).includes(file.type)) throw new MediaError(MESSAGES.type);\n if (file.size > MAX_INPUT_BYTES) throw new MediaError(MESSAGES.size);\n}\n\nexport async function resizeImage(file: File, opts: ResizeOptions = {}): Promise<ResizedImage> {\n checkInput(file);\n const { maxWidth = 1600, quality = 0.82, maxBytes = 600 * 1024 } = opts;\n\n const bitmap = await decode(file);\n const { width, height } = fitWithin(bitmap.width, bitmap.height, maxWidth);\n const canvas = makeCanvas(width, height);\n const ctx = canvas.getContext(\"2d\");\n if (!ctx) throw new MediaError(MESSAGES.encode);\n ctx.drawImage(bitmap, 0, 0, width, height);\n bitmap.close?.();\n\n let q = quality;\n let blob = await encode(canvas, q);\n while (blob.size > maxBytes && q - 0.08 >= 0.5) {\n q -= 0.08;\n blob = await encode(canvas, q);\n }\n return { blob, width, height };\n}\n\nasync function decode(file: File): Promise<ImageBitmap> {\n try {\n return await createImageBitmap(file);\n } catch {\n throw new MediaError(MESSAGES.encode);\n }\n}\n\ntype AnyCanvas = HTMLCanvasElement | OffscreenCanvas;\n\nfunction makeCanvas(width: number, height: number): AnyCanvas {\n if (typeof OffscreenCanvas !== \"undefined\") return new OffscreenCanvas(width, height);\n const c = document.createElement(\"canvas\");\n c.width = width;\n c.height = height;\n return c;\n}\n\nasync function encode(canvas: AnyCanvas, quality: number): Promise<Blob> {\n const blob =\n \"convertToBlob\" in canvas\n ? await canvas.convertToBlob({ type: \"image/webp\", quality }).catch(() => null)\n : await new Promise<Blob | null>((resolve) => canvas.toBlob(resolve, \"image/webp\", quality));\n if (!blob || blob.type !== \"image/webp\") throw new MediaError(MESSAGES.encode);\n return blob;\n}\n","/**\n * Where a picture lives, decided from the post's slug so the path is known before\n * anything is committed: hero → /blog/{slug}.webp, social → /blog/{slug}-social.webp,\n * body → /blog/{slug}/{name}.webp. The host commits the file under public/ + path.\n */\n\nexport type ImageKind = \"hero\" | \"social\" | \"body\";\n\nexport interface PathOptions {\n /** URL prefix the pictures are served from. Default \"/blog\". */\n publicDir?: string;\n}\n\n/** Lower-case letters, digits and single hyphens; empty in → \"image\". */\nexport function slugifyName(name: string): string {\n const base = name.replace(/\\.[a-z0-9]+$/i, \"\");\n const s = base\n .normalize(\"NFKD\")\n .replace(/[̀-ͯ]/g, \"\")\n .toLowerCase()\n .replace(/[^a-z0-9]+/g, \"-\")\n .replace(/^-+|-+$/g, \"\");\n return s || \"image\";\n}\n\nexport function imagePathFor(kind: ImageKind, slug: string, originalName = \"\", opts: PathOptions = {}): string {\n const dir = (opts.publicDir ?? \"/blog\").replace(/\\/$/, \"\");\n switch (kind) {\n case \"hero\":\n return `${dir}/${slug}.webp`;\n case \"social\":\n return `${dir}/${slug}-social.webp`;\n case \"body\":\n return `${dir}/${slug}/${slugifyName(originalName)}.webp`;\n }\n}\n","/**\n * Server-side checks on the pictures a save carries. Pure.\n *\n * The browser already shrank them, but anyone can POST to a server action, so\n * the path, type and size are re-checked before anything is committed: only\n * WebP under the type's public folder, ≤ 800 KB each, ≤ 3.5 MB per save.\n * Frontmatter paths are URL paths (/blog/x.webp); the repo file lives under public/.\n */\n\nexport interface ImagePayload {\n /** URL path as written in the frontmatter or body, e.g. /blog/my-post.webp. */\n path: string;\n /** Base64 of the WebP bytes. */\n base64: string;\n}\n\nexport interface ImageLimits {\n maxEach: number;\n maxTotal: number;\n /** URL prefix pictures must live under. */\n prefix: string;\n}\n\nexport const IMAGE_LIMITS: ImageLimits = { maxEach: 800 * 1024, maxTotal: 3.5 * 1024 * 1024, prefix: \"/blog/\" };\n\nexport const IMAGE_ERRORS = {\n path: \"That image path isn’t allowed.\",\n size: \"That picture is too large to send — try a smaller one.\",\n} as const;\n\nexport type ValidatedImages =\n | { ok: true; files: { path: string; content: string; encoding: \"base64\" }[] }\n | { ok: false; error: string };\n\nconst SAFE_PATH = /^[a-z0-9][a-z0-9/_-]*\\.webp$/;\n\n/** Bytes a base64 string decodes to, without decoding it. */\nexport function decodedSize(b64: string): number {\n const clean = b64.replace(/\\s+/g, \"\");\n const pad = clean.endsWith(\"==\") ? 2 : clean.endsWith(\"=\") ? 1 : 0;\n return Math.floor((clean.length * 3) / 4) - pad;\n}\n\nexport function validateImages(images: ImagePayload[], limits: ImageLimits = IMAGE_LIMITS): ValidatedImages {\n let total = 0;\n const files: { path: string; content: string; encoding: \"base64\" }[] = [];\n for (const img of images) {\n if (typeof img?.path !== \"string\" || typeof img?.base64 !== \"string\") return { ok: false, error: IMAGE_ERRORS.path };\n if (!img.path.startsWith(limits.prefix) || img.path.includes(\"..\")) return { ok: false, error: IMAGE_ERRORS.path };\n const rel = img.path.slice(1);\n if (!SAFE_PATH.test(rel)) return { ok: false, error: IMAGE_ERRORS.path };\n const size = decodedSize(img.base64);\n if (size === 0 || size > limits.maxEach) return { ok: false, error: IMAGE_ERRORS.size };\n total += size;\n if (total > limits.maxTotal) return { ok: false, error: IMAGE_ERRORS.size };\n files.push({ path: `public/${rel}`, content: img.base64.replace(/\\s+/g, \"\"), encoding: \"base64\" });\n }\n return { ok: true, files };\n}\n"]}
@@ -0,0 +1,106 @@
1
+ import { SupabaseClient } from '@supabase/supabase-js';
2
+
3
+ /**
4
+ * Types for the identity layer. All config arrives as arguments — never env.
5
+ */
6
+ /**
7
+ * Either cookie-store shape a host can hand us:
8
+ * - @supabase/ssr's CookieMethodsServer: { getAll, setAll }
9
+ * - Next's `await cookies()` store: { getAll, set }
10
+ * `setAll` is preferred when present; otherwise each cookie goes through `set`.
11
+ */
12
+ interface CookieStore {
13
+ getAll(): {
14
+ name: string;
15
+ value: string;
16
+ }[] | Promise<{
17
+ name: string;
18
+ value: string;
19
+ }[]>;
20
+ setAll?(cookies: {
21
+ name: string;
22
+ value: string;
23
+ options?: Record<string, unknown>;
24
+ }[]): void | Promise<void>;
25
+ set?(name: string, value: string, options?: Record<string, unknown>): unknown;
26
+ }
27
+ /** Enough to read the signed-in user with the anon key + their cookies. */
28
+ interface IdentityContext {
29
+ url: string;
30
+ anonKey: string;
31
+ cookies: CookieStore;
32
+ }
33
+ /** Server-only. The service-role key bypasses RLS; the module checks roles in code first. */
34
+ interface ServiceContext {
35
+ url: string;
36
+ serviceRoleKey: string;
37
+ }
38
+ type SiteRole = "staff" | "owner" | "editor";
39
+ declare const ROLE_RANK: Record<SiteRole, number>;
40
+ interface IdentityUser {
41
+ id: string;
42
+ email: string | null;
43
+ }
44
+ interface SiteMembership {
45
+ userId: string;
46
+ siteId: string;
47
+ role: "owner" | "editor";
48
+ }
49
+
50
+ /**
51
+ * createIdentityClient — a Supabase client bound to the request's cookies.
52
+ *
53
+ * Built per request from arguments. A Next 16 route does:
54
+ *
55
+ * const store = await cookies();
56
+ * const client = createIdentityClient({ url, anonKey, cookies: store });
57
+ *
58
+ * No `next` import here. The CookieStore interface accepts either shape a
59
+ * host might hand us: @supabase/ssr's { getAll, setAll }, or Next's
60
+ * cookies() store, which has { getAll, set } — the session cookies are
61
+ * written through whichever exists. (Observed 2026-09-08: with only setAll
62
+ * supported, Next's store silently wrote nothing and every sign-in bounced.)
63
+ */
64
+
65
+ type IdentityClient = SupabaseClient;
66
+ declare function createIdentityClient(ctx: IdentityContext): IdentityClient;
67
+
68
+ /**
69
+ * Per-site branding — the Business tab. Reads and writes public.sites through
70
+ * the signed-in user's client, so RLS decides who may (members read; owners
71
+ * and staff write). Nothing here needs the service role.
72
+ */
73
+
74
+ interface SiteBranding {
75
+ name: string;
76
+ logoUrl: string | null;
77
+ faviconUrl: string | null;
78
+ businessEmail: string | null;
79
+ businessPhone: string | null;
80
+ }
81
+ type BrandingPatch = Partial<SiteBranding> & {
82
+ name: string;
83
+ };
84
+ declare function getSiteBranding(client: IdentityClient, siteId: string): Promise<SiteBranding | null>;
85
+ /** Upsert the row. RLS refuses anyone but the site's owners or staff. */
86
+ declare function updateSiteBranding(client: IdentityClient, siteId: string, patch: BrandingPatch): Promise<void>;
87
+
88
+ /**
89
+ * The Team roster — public.site_members(site_id), a SECURITY DEFINER function
90
+ * that returns rows only to that site's owners or staff (editors get a 42501).
91
+ * Read with the signed-in user's client; writes are in service.ts.
92
+ */
93
+
94
+ interface SiteMember {
95
+ userId: string;
96
+ email: string;
97
+ name: string | null;
98
+ avatarUrl: string | null;
99
+ role: "owner" | "editor";
100
+ /** "invited" until the person has signed in once. */
101
+ status: "invited" | "active";
102
+ invitedAt: string;
103
+ }
104
+ declare function listSiteMembers(client: IdentityClient, siteId: string): Promise<SiteMember[]>;
105
+
106
+ export { type BrandingPatch as B, type CookieStore as C, type IdentityClient as I, ROLE_RANK as R, type ServiceContext as S, type IdentityUser as a, type IdentityContext as b, type SiteBranding as c, type SiteMember as d, type SiteMembership as e, type SiteRole as f, createIdentityClient as g, getSiteBranding as h, listSiteMembers as l, updateSiteBranding as u };
@@ -0,0 +1,106 @@
1
+ import { SupabaseClient } from '@supabase/supabase-js';
2
+
3
+ /**
4
+ * Types for the identity layer. All config arrives as arguments — never env.
5
+ */
6
+ /**
7
+ * Either cookie-store shape a host can hand us:
8
+ * - @supabase/ssr's CookieMethodsServer: { getAll, setAll }
9
+ * - Next's `await cookies()` store: { getAll, set }
10
+ * `setAll` is preferred when present; otherwise each cookie goes through `set`.
11
+ */
12
+ interface CookieStore {
13
+ getAll(): {
14
+ name: string;
15
+ value: string;
16
+ }[] | Promise<{
17
+ name: string;
18
+ value: string;
19
+ }[]>;
20
+ setAll?(cookies: {
21
+ name: string;
22
+ value: string;
23
+ options?: Record<string, unknown>;
24
+ }[]): void | Promise<void>;
25
+ set?(name: string, value: string, options?: Record<string, unknown>): unknown;
26
+ }
27
+ /** Enough to read the signed-in user with the anon key + their cookies. */
28
+ interface IdentityContext {
29
+ url: string;
30
+ anonKey: string;
31
+ cookies: CookieStore;
32
+ }
33
+ /** Server-only. The service-role key bypasses RLS; the module checks roles in code first. */
34
+ interface ServiceContext {
35
+ url: string;
36
+ serviceRoleKey: string;
37
+ }
38
+ type SiteRole = "staff" | "owner" | "editor";
39
+ declare const ROLE_RANK: Record<SiteRole, number>;
40
+ interface IdentityUser {
41
+ id: string;
42
+ email: string | null;
43
+ }
44
+ interface SiteMembership {
45
+ userId: string;
46
+ siteId: string;
47
+ role: "owner" | "editor";
48
+ }
49
+
50
+ /**
51
+ * createIdentityClient — a Supabase client bound to the request's cookies.
52
+ *
53
+ * Built per request from arguments. A Next 16 route does:
54
+ *
55
+ * const store = await cookies();
56
+ * const client = createIdentityClient({ url, anonKey, cookies: store });
57
+ *
58
+ * No `next` import here. The CookieStore interface accepts either shape a
59
+ * host might hand us: @supabase/ssr's { getAll, setAll }, or Next's
60
+ * cookies() store, which has { getAll, set } — the session cookies are
61
+ * written through whichever exists. (Observed 2026-09-08: with only setAll
62
+ * supported, Next's store silently wrote nothing and every sign-in bounced.)
63
+ */
64
+
65
+ type IdentityClient = SupabaseClient;
66
+ declare function createIdentityClient(ctx: IdentityContext): IdentityClient;
67
+
68
+ /**
69
+ * Per-site branding — the Business tab. Reads and writes public.sites through
70
+ * the signed-in user's client, so RLS decides who may (members read; owners
71
+ * and staff write). Nothing here needs the service role.
72
+ */
73
+
74
+ interface SiteBranding {
75
+ name: string;
76
+ logoUrl: string | null;
77
+ faviconUrl: string | null;
78
+ businessEmail: string | null;
79
+ businessPhone: string | null;
80
+ }
81
+ type BrandingPatch = Partial<SiteBranding> & {
82
+ name: string;
83
+ };
84
+ declare function getSiteBranding(client: IdentityClient, siteId: string): Promise<SiteBranding | null>;
85
+ /** Upsert the row. RLS refuses anyone but the site's owners or staff. */
86
+ declare function updateSiteBranding(client: IdentityClient, siteId: string, patch: BrandingPatch): Promise<void>;
87
+
88
+ /**
89
+ * The Team roster — public.site_members(site_id), a SECURITY DEFINER function
90
+ * that returns rows only to that site's owners or staff (editors get a 42501).
91
+ * Read with the signed-in user's client; writes are in service.ts.
92
+ */
93
+
94
+ interface SiteMember {
95
+ userId: string;
96
+ email: string;
97
+ name: string | null;
98
+ avatarUrl: string | null;
99
+ role: "owner" | "editor";
100
+ /** "invited" until the person has signed in once. */
101
+ status: "invited" | "active";
102
+ invitedAt: string;
103
+ }
104
+ declare function listSiteMembers(client: IdentityClient, siteId: string): Promise<SiteMember[]>;
105
+
106
+ export { type BrandingPatch as B, type CookieStore as C, type IdentityClient as I, ROLE_RANK as R, type ServiceContext as S, type IdentityUser as a, type IdentityContext as b, type SiteBranding as c, type SiteMember as d, type SiteMembership as e, type SiteRole as f, createIdentityClient as g, getSiteBranding as h, listSiteMembers as l, updateSiteBranding as u };
@@ -0,0 +1,25 @@
1
+ import { f as SiteRole, I as IdentityClient, a as IdentityUser } from './members-nhbRSYyr.cjs';
2
+
3
+ /**
4
+ * Role resolution and the request gate.
5
+ *
6
+ * Two selects on the caller's OWN rows — both permitted by RLS with the anon
7
+ * key, so this never needs the service role. Staff wins over any site role.
8
+ */
9
+
10
+ declare function getSiteRole(client: IdentityClient, siteId: string, userId?: string): Promise<SiteRole | null>;
11
+ declare function roleAtLeast(actual: SiteRole | null, minimum: SiteRole): boolean;
12
+ interface RequireOptions {
13
+ /** Default "editor" — any member of the site. */
14
+ minimumRole?: SiteRole;
15
+ }
16
+ /**
17
+ * The gate. Throws UnauthenticatedError (→ sign in) or ForbiddenError (→ 403);
18
+ * otherwise returns the user and their effective role for this site.
19
+ */
20
+ declare function requireSiteUser(client: IdentityClient, siteId: string, opts?: RequireOptions): Promise<{
21
+ user: IdentityUser;
22
+ role: SiteRole;
23
+ }>;
24
+
25
+ export { type RequireOptions as R, roleAtLeast as a, getSiteRole as g, requireSiteUser as r };
@@ -0,0 +1,25 @@
1
+ import { f as SiteRole, I as IdentityClient, a as IdentityUser } from './members-nhbRSYyr.js';
2
+
3
+ /**
4
+ * Role resolution and the request gate.
5
+ *
6
+ * Two selects on the caller's OWN rows — both permitted by RLS with the anon
7
+ * key, so this never needs the service role. Staff wins over any site role.
8
+ */
9
+
10
+ declare function getSiteRole(client: IdentityClient, siteId: string, userId?: string): Promise<SiteRole | null>;
11
+ declare function roleAtLeast(actual: SiteRole | null, minimum: SiteRole): boolean;
12
+ interface RequireOptions {
13
+ /** Default "editor" — any member of the site. */
14
+ minimumRole?: SiteRole;
15
+ }
16
+ /**
17
+ * The gate. Throws UnauthenticatedError (→ sign in) or ForbiddenError (→ 403);
18
+ * otherwise returns the user and their effective role for this site.
19
+ */
20
+ declare function requireSiteUser(client: IdentityClient, siteId: string, opts?: RequireOptions): Promise<{
21
+ user: IdentityUser;
22
+ role: SiteRole;
23
+ }>;
24
+
25
+ export { type RequireOptions as R, roleAtLeast as a, getSiteRole as g, requireSiteUser as r };
@@ -1,62 +1,7 @@
1
1
  import * as react from 'react';
2
2
  import { ReactNode, ButtonHTMLAttributes, AnchorHTMLAttributes } from 'react';
3
-
4
- /**
5
- * Light / dark, remembered per browser. The attribute lives on the .rz-admin
6
- * root (not <html>) so the host site's own theme is never touched.
7
- *
8
- * No flash of the wrong theme: the server renders defaultTheme, and the effect
9
- * only flips when a stored value differs — a single client-side change, never
10
- * a mismatch between server HTML and first client render.
11
- */
12
- type Theme = "light" | "dark";
13
- declare function useTheme(defaultTheme?: Theme): [Theme, (t: Theme) => void];
14
- declare function ThemeToggle({ theme, onChange }: {
15
- theme: Theme;
16
- onChange: (t: Theme) => void;
17
- }): react.JSX.Element;
18
-
19
- interface ShellUser {
20
- email: string;
21
- name?: string | null;
22
- /** Profile photo; initials show when absent. */
23
- avatarUrl?: string | null;
24
- }
25
- declare function firstNameOf(user: ShellUser): string;
26
- declare function initialsOf(user: ShellUser): string;
27
- declare function AccountMenu({ user, role, theme, onTheme, signOutPath, accountHref, helpHref }: {
28
- user: ShellUser;
29
- role: string;
30
- theme: Theme;
31
- onTheme: (t: Theme) => void;
32
- signOutPath: string;
33
- /** The Account page (My Profile · Change Password · Team · Business). Omit to hide the item. */
34
- accountHref?: string;
35
- /** The Help centre. Omit to hide the item. */
36
- helpHref?: string;
37
- }): react.JSX.Element;
38
-
39
- /**
40
- * Sidebar — items from the content-type registry (via navFromContentTypes),
41
- * active by href prefix, collapsible with the state remembered per browser.
42
- * Plain anchors: the package does not know which router the host uses.
43
- */
44
- interface NavItem {
45
- id: string;
46
- label: string;
47
- href: string;
48
- /** Icon id; unknown ids fall back to a document icon. Defaults to `id`. */
49
- icon?: string;
50
- /** Shown greyed out and not navigable — a section that is coming but not built yet. */
51
- disabled?: boolean;
52
- }
53
- declare function isActive(item: NavItem, activeHref: string, basePath: string): boolean;
54
- declare function Sidebar({ items, pinned, activeHref, basePath }: {
55
- items: NavItem[];
56
- pinned?: NavItem[];
57
- activeHref: string;
58
- basePath: string;
59
- }): react.JSX.Element;
3
+ import { S as ShellUser, N as NavItem, T as Theme } from '../Sidebar-CNuphww6.cjs';
4
+ export { A as AccountMenu, a as Sidebar, b as ThemeToggle, f as firstNameOf, i as initialsOf, c as isActive, u as useTheme } from '../Sidebar-CNuphww6.cjs';
60
5
 
61
6
  interface AdminShellProps {
62
7
  siteName: string;
@@ -213,4 +158,4 @@ declare function Tabs({ options, value, onChange, label }: {
213
158
  label?: string;
214
159
  }): react.JSX.Element;
215
160
 
216
- export { AccountMenu, AdminShell, type AdminShellProps, Button, type ButtonSize, type ButtonVariant, Card, EmptyState, ForwardArrow, Icon, type IconName, LinkButton, type NavItem, OutboundArrow, PageHeader, type PillTone, SHELL_CSS, type ShellUser, Sidebar, StatusPill, type StepState, Steps, type TabOption, Tabs, type Theme, ThemeToggle, TopBar, buttonClass, buttonStyle, firstNameOf, iconFor, initialsOf, isActive, useTheme };
161
+ export { AdminShell, type AdminShellProps, Button, type ButtonSize, type ButtonVariant, Card, EmptyState, ForwardArrow, Icon, type IconName, LinkButton, NavItem, OutboundArrow, PageHeader, type PillTone, SHELL_CSS, ShellUser, StatusPill, type StepState, Steps, type TabOption, Tabs, Theme, TopBar, buttonClass, buttonStyle, iconFor };
@@ -1,62 +1,7 @@
1
1
  import * as react from 'react';
2
2
  import { ReactNode, ButtonHTMLAttributes, AnchorHTMLAttributes } from 'react';
3
-
4
- /**
5
- * Light / dark, remembered per browser. The attribute lives on the .rz-admin
6
- * root (not <html>) so the host site's own theme is never touched.
7
- *
8
- * No flash of the wrong theme: the server renders defaultTheme, and the effect
9
- * only flips when a stored value differs — a single client-side change, never
10
- * a mismatch between server HTML and first client render.
11
- */
12
- type Theme = "light" | "dark";
13
- declare function useTheme(defaultTheme?: Theme): [Theme, (t: Theme) => void];
14
- declare function ThemeToggle({ theme, onChange }: {
15
- theme: Theme;
16
- onChange: (t: Theme) => void;
17
- }): react.JSX.Element;
18
-
19
- interface ShellUser {
20
- email: string;
21
- name?: string | null;
22
- /** Profile photo; initials show when absent. */
23
- avatarUrl?: string | null;
24
- }
25
- declare function firstNameOf(user: ShellUser): string;
26
- declare function initialsOf(user: ShellUser): string;
27
- declare function AccountMenu({ user, role, theme, onTheme, signOutPath, accountHref, helpHref }: {
28
- user: ShellUser;
29
- role: string;
30
- theme: Theme;
31
- onTheme: (t: Theme) => void;
32
- signOutPath: string;
33
- /** The Account page (My Profile · Change Password · Team · Business). Omit to hide the item. */
34
- accountHref?: string;
35
- /** The Help centre. Omit to hide the item. */
36
- helpHref?: string;
37
- }): react.JSX.Element;
38
-
39
- /**
40
- * Sidebar — items from the content-type registry (via navFromContentTypes),
41
- * active by href prefix, collapsible with the state remembered per browser.
42
- * Plain anchors: the package does not know which router the host uses.
43
- */
44
- interface NavItem {
45
- id: string;
46
- label: string;
47
- href: string;
48
- /** Icon id; unknown ids fall back to a document icon. Defaults to `id`. */
49
- icon?: string;
50
- /** Shown greyed out and not navigable — a section that is coming but not built yet. */
51
- disabled?: boolean;
52
- }
53
- declare function isActive(item: NavItem, activeHref: string, basePath: string): boolean;
54
- declare function Sidebar({ items, pinned, activeHref, basePath }: {
55
- items: NavItem[];
56
- pinned?: NavItem[];
57
- activeHref: string;
58
- basePath: string;
59
- }): react.JSX.Element;
3
+ import { S as ShellUser, N as NavItem, T as Theme } from '../Sidebar-CNuphww6.js';
4
+ export { A as AccountMenu, a as Sidebar, b as ThemeToggle, f as firstNameOf, i as initialsOf, c as isActive, u as useTheme } from '../Sidebar-CNuphww6.js';
60
5
 
61
6
  interface AdminShellProps {
62
7
  siteName: string;
@@ -213,4 +158,4 @@ declare function Tabs({ options, value, onChange, label }: {
213
158
  label?: string;
214
159
  }): react.JSX.Element;
215
160
 
216
- export { AccountMenu, AdminShell, type AdminShellProps, Button, type ButtonSize, type ButtonVariant, Card, EmptyState, ForwardArrow, Icon, type IconName, LinkButton, type NavItem, OutboundArrow, PageHeader, type PillTone, SHELL_CSS, type ShellUser, Sidebar, StatusPill, type StepState, Steps, type TabOption, Tabs, type Theme, ThemeToggle, TopBar, buttonClass, buttonStyle, firstNameOf, iconFor, initialsOf, isActive, useTheme };
161
+ export { AdminShell, type AdminShellProps, Button, type ButtonSize, type ButtonVariant, Card, EmptyState, ForwardArrow, Icon, type IconName, LinkButton, NavItem, OutboundArrow, PageHeader, type PillTone, SHELL_CSS, ShellUser, StatusPill, type StepState, Steps, type TabOption, Tabs, Theme, TopBar, buttonClass, buttonStyle, iconFor };