@gallop.software/studio 0.1.68 → 0.1.69

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.
@@ -1,5 +1,5 @@
1
1
  import { NextRequest, NextResponse } from 'next/server';
2
- import { F as FileItem } from './types-1m_7EjJU.mjs';
2
+ import { F as FileItem } from './types-CNVLjvIw.mjs';
3
3
 
4
4
  /**
5
5
  * Unified GET handler for all Studio API routes
@@ -1,5 +1,5 @@
1
1
  import { NextRequest, NextResponse } from 'next/server';
2
- import { F as FileItem } from './types-1m_7EjJU.js';
2
+ import { F as FileItem } from './types-CNVLjvIw.js';
3
3
 
4
4
  /**
5
5
  * Unified GET handler for all Studio API routes
package/dist/index.d.mts CHANGED
@@ -1,6 +1,6 @@
1
1
  import * as _emotion_react_jsx_runtime from '@emotion/react/jsx-runtime';
2
- import { I as ImageSize, S as SizeEntry, a as ImageEntry, b as StudioMeta } from './types-1m_7EjJU.mjs';
3
- export { C as CdnStatus, F as FileItem, c as StudioConfig } from './types-1m_7EjJU.mjs';
2
+ import { I as ImageEntry, S as StudioMeta, L as LeanMeta } from './types-CNVLjvIw.mjs';
3
+ export { C as CdnStatus, F as FileItem, a as ImageSize, b as LeanImageEntry, c as LeanImageSize, d as SizeEntry, e as StudioConfig, g as getThumbnailPath, i as isLeanMeta, t as toLeanMeta } from './types-CNVLjvIw.mjs';
4
4
 
5
5
  /**
6
6
  * Floating button that opens the Studio modal.
@@ -9,26 +9,31 @@ export { C as CdnStatus, F as FileItem, c as StudioConfig } from './types-1m_7Ej
9
9
  */
10
10
  declare function StudioButton(): _emotion_react_jsx_runtime.JSX.Element | null;
11
11
 
12
+ type UnifiedMeta = StudioMeta | LeanMeta;
12
13
  /**
13
- * The meta object containing all image metadata.
14
- * This is read from _data/_meta.json in the consuming project.
14
+ * The meta object in verbose format (for backward compatibility)
15
15
  */
16
16
  declare const meta: StudioMeta;
17
17
  /**
18
- * Initialize meta from a JSON object (called during build/runtime)
18
+ * Initialize meta from a JSON object (handles both formats)
19
19
  */
20
- declare function initializeMeta(data: StudioMeta): void;
20
+ declare function initializeMeta(data: UnifiedMeta): void;
21
21
  /**
22
- * Get the resolved URL for an image, handling CDN vs local paths
22
+ * Get the resolved URL for an image
23
+ * Works with both verbose and lean formats
23
24
  */
24
- declare function getImageUrl(imageKey: string, size?: ImageSize): string | undefined;
25
+ declare function getImageUrl(imageKey: string, size?: 'sm' | 'md' | 'lg' | 'small' | 'medium' | 'large' | 'full'): string | undefined;
25
26
  /**
26
- * Get the full image entry for a key
27
+ * Get image entry (verbose format for backward compat)
27
28
  */
28
29
  declare function getStudioMeta(imageKey: string): ImageEntry | undefined;
29
30
  /**
30
- * Get size data for an image
31
+ * Get dimensions for an image
31
32
  */
32
- declare function getImageSize(imageKey: string, size?: ImageSize): SizeEntry | undefined;
33
+ declare function getImageSize(imageKey: string, size?: 'sm' | 'md' | 'lg' | 'small' | 'medium' | 'large' | 'full'): {
34
+ width: number;
35
+ height: number;
36
+ path?: string;
37
+ } | undefined;
33
38
 
34
- export { ImageEntry, ImageSize, SizeEntry, StudioButton, StudioMeta, getImageSize, getImageUrl, getStudioMeta, initializeMeta, meta };
39
+ export { ImageEntry, LeanMeta, StudioButton, StudioMeta, getImageSize, getImageUrl, getStudioMeta, initializeMeta, meta };
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import * as _emotion_react_jsx_runtime from '@emotion/react/jsx-runtime';
2
- import { I as ImageSize, S as SizeEntry, a as ImageEntry, b as StudioMeta } from './types-1m_7EjJU.js';
3
- export { C as CdnStatus, F as FileItem, c as StudioConfig } from './types-1m_7EjJU.js';
2
+ import { I as ImageEntry, S as StudioMeta, L as LeanMeta } from './types-CNVLjvIw.js';
3
+ export { C as CdnStatus, F as FileItem, a as ImageSize, b as LeanImageEntry, c as LeanImageSize, d as SizeEntry, e as StudioConfig, g as getThumbnailPath, i as isLeanMeta, t as toLeanMeta } from './types-CNVLjvIw.js';
4
4
 
5
5
  /**
6
6
  * Floating button that opens the Studio modal.
@@ -9,26 +9,31 @@ export { C as CdnStatus, F as FileItem, c as StudioConfig } from './types-1m_7Ej
9
9
  */
10
10
  declare function StudioButton(): _emotion_react_jsx_runtime.JSX.Element | null;
11
11
 
12
+ type UnifiedMeta = StudioMeta | LeanMeta;
12
13
  /**
13
- * The meta object containing all image metadata.
14
- * This is read from _data/_meta.json in the consuming project.
14
+ * The meta object in verbose format (for backward compatibility)
15
15
  */
16
16
  declare const meta: StudioMeta;
17
17
  /**
18
- * Initialize meta from a JSON object (called during build/runtime)
18
+ * Initialize meta from a JSON object (handles both formats)
19
19
  */
20
- declare function initializeMeta(data: StudioMeta): void;
20
+ declare function initializeMeta(data: UnifiedMeta): void;
21
21
  /**
22
- * Get the resolved URL for an image, handling CDN vs local paths
22
+ * Get the resolved URL for an image
23
+ * Works with both verbose and lean formats
23
24
  */
24
- declare function getImageUrl(imageKey: string, size?: ImageSize): string | undefined;
25
+ declare function getImageUrl(imageKey: string, size?: 'sm' | 'md' | 'lg' | 'small' | 'medium' | 'large' | 'full'): string | undefined;
25
26
  /**
26
- * Get the full image entry for a key
27
+ * Get image entry (verbose format for backward compat)
27
28
  */
28
29
  declare function getStudioMeta(imageKey: string): ImageEntry | undefined;
29
30
  /**
30
- * Get size data for an image
31
+ * Get dimensions for an image
31
32
  */
32
- declare function getImageSize(imageKey: string, size?: ImageSize): SizeEntry | undefined;
33
+ declare function getImageSize(imageKey: string, size?: 'sm' | 'md' | 'lg' | 'small' | 'medium' | 'large' | 'full'): {
34
+ width: number;
35
+ height: number;
36
+ path?: string;
37
+ } | undefined;
33
38
 
34
- export { ImageEntry, ImageSize, SizeEntry, StudioButton, StudioMeta, getImageSize, getImageUrl, getStudioMeta, initializeMeta, meta };
39
+ export { ImageEntry, LeanMeta, StudioButton, StudioMeta, getImageSize, getImageUrl, getStudioMeta, initializeMeta, meta };
package/dist/index.js CHANGED
@@ -175,35 +175,104 @@ function LoadingState() {
175
175
  ] }) });
176
176
  }
177
177
 
178
+ // src/types.ts
179
+ function getThumbnailPath(originalPath, size) {
180
+ const ext = _optionalChain([originalPath, 'access', _ => _.match, 'call', _2 => _2(/\.\w+$/), 'optionalAccess', _3 => _3[0]]) || ".jpg";
181
+ const base = originalPath.replace(/\.\w+$/, "");
182
+ const outputExt = ext.toLowerCase() === ".png" ? ".png" : ".jpg";
183
+ return `/images${base}-${size}${outputExt}`;
184
+ }
185
+ function toLeanMeta(verbose) {
186
+ const lean = {};
187
+ for (const [key, entry] of Object.entries(verbose.images)) {
188
+ const pathKey = _optionalChain([entry, 'access', _4 => _4.original, 'optionalAccess', _5 => _5.path]) || `/${key}`;
189
+ lean[pathKey] = {
190
+ w: _optionalChain([entry, 'access', _6 => _6.original, 'optionalAccess', _7 => _7.width]) || 0,
191
+ h: _optionalChain([entry, 'access', _8 => _8.original, 'optionalAccess', _9 => _9.height]) || 0,
192
+ blur: entry.blurhash || ""
193
+ };
194
+ if (_optionalChain([entry, 'access', _10 => _10.cdn, 'optionalAccess', _11 => _11.synced])) {
195
+ lean[pathKey].s = 1;
196
+ }
197
+ }
198
+ return lean;
199
+ }
200
+ function isLeanMeta(meta2) {
201
+ if (!meta2 || typeof meta2 !== "object") return false;
202
+ return !("images" in meta2);
203
+ }
204
+
178
205
  // src/lib/meta.ts
179
- var _meta = {
206
+ var _verboseMeta = null;
207
+ var _leanMeta = {};
208
+ var meta = {
180
209
  $schema: "https://gallop.software/schemas/studio-meta.json",
181
210
  version: 1,
182
211
  generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
183
212
  images: {}
184
213
  };
185
- var meta = _meta;
186
214
  function initializeMeta(data) {
187
- _meta = data;
188
- Object.assign(meta, data);
215
+ if (isLeanMeta(data)) {
216
+ _leanMeta = data;
217
+ _verboseMeta = null;
218
+ } else {
219
+ _verboseMeta = data;
220
+ _leanMeta = toLeanMeta(data);
221
+ Object.assign(meta, data);
222
+ }
189
223
  }
190
- function getImageUrl(imageKey, size = "medium") {
191
- const image = meta.images[imageKey];
192
- if (!image) return void 0;
193
- const sizeData = image.sizes[size] || image.sizes.full;
194
- if (!sizeData) return void 0;
195
- if (_optionalChain([image, 'access', _ => _.cdn, 'optionalAccess', _2 => _2.synced]) && image.cdn.baseUrl) {
196
- return `${image.cdn.baseUrl}${sizeData.path}`;
224
+ function getImageUrl(imageKey, size) {
225
+ const cdnUrl = process.env.CLOUDFLARE_R2_PUBLIC_URL || process.env.NEXT_PUBLIC_CDN_URL;
226
+ const leanEntry = _leanMeta[imageKey];
227
+ if (leanEntry) {
228
+ const sizeMap = {
229
+ small: "sm",
230
+ medium: "md",
231
+ large: "lg"
232
+ };
233
+ if (!size || size === "full") {
234
+ if (leanEntry.s && cdnUrl) {
235
+ return `${cdnUrl}${imageKey}`;
236
+ }
237
+ return imageKey;
238
+ }
239
+ const normalizedSize = sizeMap[size] || size;
240
+ const thumbPath = getThumbnailPath(imageKey, normalizedSize);
241
+ if (leanEntry.s && cdnUrl) {
242
+ return `${cdnUrl}${thumbPath}`;
243
+ }
244
+ return thumbPath;
245
+ }
246
+ if (_verboseMeta) {
247
+ const entry = _verboseMeta.images[imageKey];
248
+ if (!entry) return void 0;
249
+ const sizeData = entry.sizes[size || "medium"] || entry.sizes.full;
250
+ if (!sizeData) return void 0;
251
+ if (_optionalChain([entry, 'access', _12 => _12.cdn, 'optionalAccess', _13 => _13.synced]) && entry.cdn.baseUrl) {
252
+ return `${entry.cdn.baseUrl}${sizeData.path}`;
253
+ }
254
+ return sizeData.path;
197
255
  }
198
- return sizeData.path;
256
+ return void 0;
199
257
  }
200
258
  function getStudioMeta(imageKey) {
201
- return meta.images[imageKey];
259
+ if (_verboseMeta) {
260
+ return _verboseMeta.images[imageKey];
261
+ }
262
+ return void 0;
202
263
  }
203
- function getImageSize(imageKey, size = "medium") {
204
- const image = meta.images[imageKey];
205
- if (!image) return void 0;
206
- return image.sizes[size] || image.sizes.full;
264
+ function getImageSize(imageKey, size) {
265
+ const leanEntry = _leanMeta[imageKey];
266
+ if (leanEntry) {
267
+ return { width: leanEntry.w, height: leanEntry.h };
268
+ }
269
+ if (_verboseMeta) {
270
+ const entry = _verboseMeta.images[imageKey];
271
+ if (!entry) return void 0;
272
+ const sizeData = entry.sizes[size || "medium"] || entry.sizes.full;
273
+ return sizeData;
274
+ }
275
+ return void 0;
207
276
  }
208
277
 
209
278
 
@@ -212,5 +281,8 @@ function getImageSize(imageKey, size = "medium") {
212
281
 
213
282
 
214
283
 
215
- exports.StudioButton = StudioButton; exports.getImageSize = getImageSize; exports.getImageUrl = getImageUrl; exports.getStudioMeta = getStudioMeta; exports.initializeMeta = initializeMeta; exports.meta = meta;
284
+
285
+
286
+
287
+ exports.StudioButton = StudioButton; exports.getImageSize = getImageSize; exports.getImageUrl = getImageUrl; exports.getStudioMeta = getStudioMeta; exports.getThumbnailPath = getThumbnailPath; exports.initializeMeta = initializeMeta; exports.isLeanMeta = isLeanMeta; exports.meta = meta; exports.toLeanMeta = toLeanMeta;
216
288
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["/Users/chrisb/Sites/studio/dist/index.js","../src/components/StudioButton.tsx","../src/lib/meta.ts"],"names":[],"mappings":"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","file":"/Users/chrisb/Sites/studio/dist/index.js","sourcesContent":[null,"/** @jsxImportSource @emotion/react */\n'use client'\n\nimport { useState, useEffect, lazy, Suspense } from 'react'\nimport { css, keyframes } from '@emotion/react'\nimport { colors, fontStack, fontSize, baseReset } from './tokens'\n\n// Lazy load the full Studio UI to avoid bundling in production\nconst StudioUI = lazy(() => import('./StudioUI'))\n\nconst spin = keyframes`\n to {\n transform: rotate(360deg);\n }\n`\n\nconst styles = {\n button: css`\n position: fixed;\n bottom: 24px;\n right: 24px;\n z-index: 9998;\n width: 52px;\n height: 52px;\n border-radius: 50%;\n background: ${colors.primary};\n color: white;\n box-shadow: 0 4px 12px ${colors.shadowDark}, 0 1px 3px ${colors.shadow};\n display: flex;\n align-items: center;\n justify-content: center;\n border: none;\n cursor: pointer;\n transition: all 0.15s ease;\n font-family: ${fontStack};\n \n &:hover {\n transform: translateY(-2px);\n box-shadow: 0 8px 20px ${colors.shadowDark}, 0 2px 6px ${colors.shadow};\n background: ${colors.primaryHover};\n }\n \n &:active {\n transform: translateY(0);\n }\n `,\n buttonIcon: css`\n width: 24px;\n height: 24px;\n `,\n overlay: css`\n position: fixed;\n top: 0;\n right: 0;\n bottom: 0;\n left: 0;\n z-index: 9999;\n transition: opacity 0.2s ease, visibility 0.2s ease;\n `,\n overlayHidden: css`\n opacity: 0;\n visibility: hidden;\n pointer-events: none;\n `,\n backdrop: css`\n position: absolute;\n top: 0;\n right: 0;\n bottom: 0;\n left: 0;\n background-color: rgba(26, 31, 54, 0.4);\n backdrop-filter: blur(4px);\n `,\n modal: css`\n ${baseReset}\n position: absolute;\n top: 24px;\n right: 24px;\n bottom: 24px;\n left: 24px;\n background-color: ${colors.surface};\n border-radius: 12px;\n box-shadow: 0 30px 60px -12px rgba(50, 50, 93, 0.25), 0 18px 36px -18px rgba(0, 0, 0, 0.3);\n display: flex;\n flex-direction: column;\n overflow: hidden;\n `,\n loading: css`\n display: flex;\n align-items: center;\n justify-content: center;\n height: 100%;\n background: ${colors.background};\n font-family: ${fontStack};\n `,\n loadingContent: css`\n display: flex;\n flex-direction: column;\n align-items: center;\n gap: 16px;\n `,\n spinner: css`\n width: 36px;\n height: 36px;\n border-radius: 50%;\n border: 3px solid ${colors.border};\n border-top-color: ${colors.primary};\n animation: ${spin} 0.8s linear infinite;\n `,\n loadingText: css`\n color: ${colors.textSecondary};\n font-size: ${fontSize.base};\n font-weight: 500;\n margin: 0;\n letter-spacing: -0.01em;\n `,\n}\n\n/**\n * Floating button that opens the Studio modal.\n * Fixed position in bottom-right corner.\n * Only renders in development mode.\n */\nexport function StudioButton() {\n const [mounted, setMounted] = useState(false)\n const [isOpen, setIsOpen] = useState(false)\n const [hasBeenOpened, setHasBeenOpened] = useState(false)\n\n // Only render on client to avoid hydration mismatch\n useEffect(() => {\n setMounted(true)\n }, [])\n\n const handleOpen = () => {\n setIsOpen(true)\n setHasBeenOpened(true)\n }\n\n // Only render in development and on client\n if (!mounted || process.env.NODE_ENV !== 'development') {\n return null\n }\n\n return (\n <>\n {!isOpen && (\n <button\n css={styles.button}\n onClick={handleOpen}\n title=\"Open Studio\"\n aria-label=\"Open Studio media manager\"\n >\n <ImageIcon />\n </button>\n )}\n\n {/* Keep mounted once opened to preserve state */}\n {hasBeenOpened && (\n <div css={[styles.overlay, !isOpen && styles.overlayHidden]}>\n <div css={styles.backdrop} onClick={() => setIsOpen(false)} />\n <div css={styles.modal}>\n <Suspense fallback={<LoadingState />}>\n <StudioUI onClose={() => setIsOpen(false)} isVisible={isOpen} />\n </Suspense>\n </div>\n </div>\n )}\n </>\n )\n}\n\nfunction ImageIcon() {\n return (\n <svg\n css={styles.buttonIcon}\n xmlns=\"http://www.w3.org/2000/svg\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth={2}\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n >\n <rect x=\"3\" y=\"3\" width=\"18\" height=\"18\" rx=\"2\" ry=\"2\" />\n <circle cx=\"8.5\" cy=\"8.5\" r=\"1.5\" />\n <polyline points=\"21 15 16 10 5 21\" />\n </svg>\n )\n}\n\nfunction LoadingState() {\n return (\n <div css={styles.loading}>\n <div css={styles.loadingContent}>\n <div css={styles.spinner} />\n <p css={styles.loadingText}>Loading Studio...</p>\n </div>\n </div>\n )\n}\n","import type { StudioMeta, ImageEntry, ImageSize, SizeEntry } from '../types'\n\n// Default empty meta - will be populated when reading from project\nlet _meta: StudioMeta = {\n $schema: 'https://gallop.software/schemas/studio-meta.json',\n version: 1,\n generatedAt: new Date().toISOString(),\n images: {},\n}\n\n/**\n * The meta object containing all image metadata.\n * This is read from _data/_meta.json in the consuming project.\n */\nexport const meta: StudioMeta = _meta\n\n/**\n * Initialize meta from a JSON object (called during build/runtime)\n */\nexport function initializeMeta(data: StudioMeta): void {\n _meta = data\n Object.assign(meta, data)\n}\n\n/**\n * Get the resolved URL for an image, handling CDN vs local paths\n */\nexport function getImageUrl(\n imageKey: string,\n size: ImageSize = 'medium'\n): string | undefined {\n const image = meta.images[imageKey]\n if (!image) return undefined\n\n const sizeData = image.sizes[size] || image.sizes.full\n if (!sizeData) return undefined\n\n // If synced to CDN, use CDN URL\n if (image.cdn?.synced && image.cdn.baseUrl) {\n return `${image.cdn.baseUrl}${sizeData.path}`\n }\n\n // Otherwise use local path\n return sizeData.path\n}\n\n/**\n * Get the full image entry for a key\n */\nexport function getStudioMeta(imageKey: string): ImageEntry | undefined {\n return meta.images[imageKey]\n}\n\n/**\n * Get size data for an image\n */\nexport function getImageSize(\n imageKey: string,\n size: ImageSize = 'medium'\n): SizeEntry | undefined {\n const image = meta.images[imageKey]\n if (!image) return undefined\n return image.sizes[size] || image.sizes.full\n}\n"]}
1
+ {"version":3,"sources":["/Users/chrisb/Sites/studio/dist/index.js","../src/components/StudioButton.tsx","../src/types.ts","../src/lib/meta.ts"],"names":["meta"],"mappings":"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","file":"/Users/chrisb/Sites/studio/dist/index.js","sourcesContent":[null,"/** @jsxImportSource @emotion/react */\n'use client'\n\nimport { useState, useEffect, lazy, Suspense } from 'react'\nimport { css, keyframes } from '@emotion/react'\nimport { colors, fontStack, fontSize, baseReset } from './tokens'\n\n// Lazy load the full Studio UI to avoid bundling in production\nconst StudioUI = lazy(() => import('./StudioUI'))\n\nconst spin = keyframes`\n to {\n transform: rotate(360deg);\n }\n`\n\nconst styles = {\n button: css`\n position: fixed;\n bottom: 24px;\n right: 24px;\n z-index: 9998;\n width: 52px;\n height: 52px;\n border-radius: 50%;\n background: ${colors.primary};\n color: white;\n box-shadow: 0 4px 12px ${colors.shadowDark}, 0 1px 3px ${colors.shadow};\n display: flex;\n align-items: center;\n justify-content: center;\n border: none;\n cursor: pointer;\n transition: all 0.15s ease;\n font-family: ${fontStack};\n \n &:hover {\n transform: translateY(-2px);\n box-shadow: 0 8px 20px ${colors.shadowDark}, 0 2px 6px ${colors.shadow};\n background: ${colors.primaryHover};\n }\n \n &:active {\n transform: translateY(0);\n }\n `,\n buttonIcon: css`\n width: 24px;\n height: 24px;\n `,\n overlay: css`\n position: fixed;\n top: 0;\n right: 0;\n bottom: 0;\n left: 0;\n z-index: 9999;\n transition: opacity 0.2s ease, visibility 0.2s ease;\n `,\n overlayHidden: css`\n opacity: 0;\n visibility: hidden;\n pointer-events: none;\n `,\n backdrop: css`\n position: absolute;\n top: 0;\n right: 0;\n bottom: 0;\n left: 0;\n background-color: rgba(26, 31, 54, 0.4);\n backdrop-filter: blur(4px);\n `,\n modal: css`\n ${baseReset}\n position: absolute;\n top: 24px;\n right: 24px;\n bottom: 24px;\n left: 24px;\n background-color: ${colors.surface};\n border-radius: 12px;\n box-shadow: 0 30px 60px -12px rgba(50, 50, 93, 0.25), 0 18px 36px -18px rgba(0, 0, 0, 0.3);\n display: flex;\n flex-direction: column;\n overflow: hidden;\n `,\n loading: css`\n display: flex;\n align-items: center;\n justify-content: center;\n height: 100%;\n background: ${colors.background};\n font-family: ${fontStack};\n `,\n loadingContent: css`\n display: flex;\n flex-direction: column;\n align-items: center;\n gap: 16px;\n `,\n spinner: css`\n width: 36px;\n height: 36px;\n border-radius: 50%;\n border: 3px solid ${colors.border};\n border-top-color: ${colors.primary};\n animation: ${spin} 0.8s linear infinite;\n `,\n loadingText: css`\n color: ${colors.textSecondary};\n font-size: ${fontSize.base};\n font-weight: 500;\n margin: 0;\n letter-spacing: -0.01em;\n `,\n}\n\n/**\n * Floating button that opens the Studio modal.\n * Fixed position in bottom-right corner.\n * Only renders in development mode.\n */\nexport function StudioButton() {\n const [mounted, setMounted] = useState(false)\n const [isOpen, setIsOpen] = useState(false)\n const [hasBeenOpened, setHasBeenOpened] = useState(false)\n\n // Only render on client to avoid hydration mismatch\n useEffect(() => {\n setMounted(true)\n }, [])\n\n const handleOpen = () => {\n setIsOpen(true)\n setHasBeenOpened(true)\n }\n\n // Only render in development and on client\n if (!mounted || process.env.NODE_ENV !== 'development') {\n return null\n }\n\n return (\n <>\n {!isOpen && (\n <button\n css={styles.button}\n onClick={handleOpen}\n title=\"Open Studio\"\n aria-label=\"Open Studio media manager\"\n >\n <ImageIcon />\n </button>\n )}\n\n {/* Keep mounted once opened to preserve state */}\n {hasBeenOpened && (\n <div css={[styles.overlay, !isOpen && styles.overlayHidden]}>\n <div css={styles.backdrop} onClick={() => setIsOpen(false)} />\n <div css={styles.modal}>\n <Suspense fallback={<LoadingState />}>\n <StudioUI onClose={() => setIsOpen(false)} isVisible={isOpen} />\n </Suspense>\n </div>\n </div>\n )}\n </>\n )\n}\n\nfunction ImageIcon() {\n return (\n <svg\n css={styles.buttonIcon}\n xmlns=\"http://www.w3.org/2000/svg\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth={2}\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n >\n <rect x=\"3\" y=\"3\" width=\"18\" height=\"18\" rx=\"2\" ry=\"2\" />\n <circle cx=\"8.5\" cy=\"8.5\" r=\"1.5\" />\n <polyline points=\"21 15 16 10 5 21\" />\n </svg>\n )\n}\n\nfunction LoadingState() {\n return (\n <div css={styles.loading}>\n <div css={styles.loadingContent}>\n <div css={styles.spinner} />\n <p css={styles.loadingText}>Loading Studio...</p>\n </div>\n </div>\n )\n}\n","/**\n * Image size variants (verbose format used by handlers)\n */\nexport type ImageSize = 'small' | 'medium' | 'large' | 'full'\n\n/**\n * Image size variants (lean format for consumers)\n */\nexport type LeanImageSize = 'sm' | 'md' | 'lg'\n\n/**\n * Size entry with path and dimensions\n */\nexport interface SizeEntry {\n path: string\n width: number\n height: number\n}\n\n/**\n * CDN sync status\n */\nexport interface CdnStatus {\n synced: boolean\n baseUrl: string\n syncedAt: string\n}\n\n/**\n * Image entry in meta (verbose format - used by Studio handlers)\n */\nexport interface ImageEntry {\n original: {\n path: string\n width: number\n height: number\n fileSize: number\n }\n sizes: {\n full: SizeEntry\n large: SizeEntry\n medium: SizeEntry\n small: SizeEntry\n [key: string]: SizeEntry\n }\n blurhash: string\n dominantColor: string\n cdn: CdnStatus | null\n}\n\n/**\n * Studio meta schema (verbose format - used by Studio handlers)\n */\nexport interface StudioMeta {\n $schema: string\n version: number\n generatedAt: string\n images: Record<string, ImageEntry>\n}\n\n/**\n * Lean image entry - minimal metadata for consumers\n * ~80 bytes per image vs ~500 bytes in verbose format\n */\nexport interface LeanImageEntry {\n /** Original width */\n w: number\n /** Original height */\n h: number\n /** Blurhash for placeholder */\n blur: string\n /** Synced to CDN (present and 1 if synced, omit if not) */\n s?: 1\n}\n\n/**\n * Lean meta schema - flat structure with path as key\n */\nexport type LeanMeta = Record<string, LeanImageEntry>\n\n/**\n * File/folder item for browser\n */\nexport interface FileItem {\n name: string\n path: string\n type: 'file' | 'folder'\n size?: number\n dimensions?: { width: number; height: number }\n cdnSynced?: boolean\n fileCount?: number\n totalSize?: number\n thumbnail?: string\n hasThumbnail?: boolean\n}\n\n/**\n * Studio configuration\n */\nexport interface StudioConfig {\n r2AccountId?: string\n r2AccessKeyId?: string\n r2SecretAccessKey?: string\n r2BucketName?: string\n r2PublicUrl?: string\n thumbnailSizes?: {\n small: number\n medium: number\n large: number\n }\n}\n\n/**\n * Helper to derive thumbnail path from original path\n */\nexport function getThumbnailPath(originalPath: string, size: LeanImageSize): string {\n const ext = originalPath.match(/\\.\\w+$/)?.[0] || '.jpg'\n const base = originalPath.replace(/\\.\\w+$/, '')\n const outputExt = ext.toLowerCase() === '.png' ? '.png' : '.jpg'\n return `/images${base}-${size}${outputExt}`\n}\n\n/**\n * Convert verbose StudioMeta to LeanMeta\n */\nexport function toLeanMeta(verbose: StudioMeta): LeanMeta {\n const lean: LeanMeta = {}\n \n for (const [key, entry] of Object.entries(verbose.images)) {\n const pathKey = entry.original?.path || `/${key}`\n lean[pathKey] = {\n w: entry.original?.width || 0,\n h: entry.original?.height || 0,\n blur: entry.blurhash || '',\n }\n if (entry.cdn?.synced) {\n lean[pathKey].s = 1\n }\n }\n \n return lean\n}\n\n/**\n * Check if meta is in lean format (no 'images' wrapper)\n */\nexport function isLeanMeta(meta: unknown): meta is LeanMeta {\n if (!meta || typeof meta !== 'object') return false\n // Lean format doesn't have 'images' property\n return !('images' in meta)\n}\n","import type { StudioMeta, ImageEntry, LeanMeta, LeanImageEntry } from '../types'\nimport { getThumbnailPath, toLeanMeta, isLeanMeta } from '../types'\n\n// Unified meta - can be either format\ntype UnifiedMeta = StudioMeta | LeanMeta\n\n// Store both formats\nlet _verboseMeta: StudioMeta | null = null\nlet _leanMeta: LeanMeta = {}\n\n/**\n * The meta object in verbose format (for backward compatibility)\n */\nexport const meta: StudioMeta = {\n $schema: 'https://gallop.software/schemas/studio-meta.json',\n version: 1,\n generatedAt: new Date().toISOString(),\n images: {},\n}\n\n/**\n * Get lean meta for efficient access\n */\nexport function getLeanMeta(): LeanMeta {\n return _leanMeta\n}\n\n/**\n * Initialize meta from a JSON object (handles both formats)\n */\nexport function initializeMeta(data: UnifiedMeta): void {\n if (isLeanMeta(data)) {\n // Already lean format\n _leanMeta = data\n _verboseMeta = null\n } else {\n // Verbose format - convert to lean\n _verboseMeta = data\n _leanMeta = toLeanMeta(data)\n // Also update the exported meta object for backward compat\n Object.assign(meta, data)\n }\n}\n\n/**\n * Get the resolved URL for an image\n * Works with both verbose and lean formats\n */\nexport function getImageUrl(\n imageKey: string,\n size?: 'sm' | 'md' | 'lg' | 'small' | 'medium' | 'large' | 'full'\n): string | undefined {\n const cdnUrl = process.env.CLOUDFLARE_R2_PUBLIC_URL || process.env.NEXT_PUBLIC_CDN_URL\n \n // Try lean meta first\n const leanEntry = _leanMeta[imageKey]\n if (leanEntry) {\n // Map old size names to new\n const sizeMap: Record<string, 'sm' | 'md' | 'lg'> = {\n small: 'sm',\n medium: 'md', \n large: 'lg',\n }\n \n if (!size || size === 'full') {\n // Return original path\n if (leanEntry.s && cdnUrl) {\n return `${cdnUrl}${imageKey}`\n }\n return imageKey\n }\n \n const normalizedSize = sizeMap[size] || size as 'sm' | 'md' | 'lg'\n const thumbPath = getThumbnailPath(imageKey, normalizedSize)\n \n if (leanEntry.s && cdnUrl) {\n return `${cdnUrl}${thumbPath}`\n }\n return thumbPath\n }\n \n // Fall back to verbose meta\n if (_verboseMeta) {\n const entry = _verboseMeta.images[imageKey]\n if (!entry) return undefined\n \n const sizeData = entry.sizes[size || 'medium'] || entry.sizes.full\n if (!sizeData) return undefined\n \n if (entry.cdn?.synced && entry.cdn.baseUrl) {\n return `${entry.cdn.baseUrl}${sizeData.path}`\n }\n return sizeData.path\n }\n \n return undefined\n}\n\n/**\n * Get image entry (verbose format for backward compat)\n */\nexport function getStudioMeta(imageKey: string): ImageEntry | undefined {\n if (_verboseMeta) {\n return _verboseMeta.images[imageKey]\n }\n return undefined\n}\n\n/**\n * Get lean entry\n */\nexport function getLeanEntry(imageKey: string): LeanImageEntry | undefined {\n return _leanMeta[imageKey]\n}\n\n/**\n * Get dimensions for an image\n */\nexport function getImageSize(\n imageKey: string,\n size?: 'sm' | 'md' | 'lg' | 'small' | 'medium' | 'large' | 'full'\n): { width: number; height: number; path?: string } | undefined {\n // Try lean meta first\n const leanEntry = _leanMeta[imageKey]\n if (leanEntry) {\n // For lean, we only have original dimensions\n return { width: leanEntry.w, height: leanEntry.h }\n }\n \n // Fall back to verbose meta\n if (_verboseMeta) {\n const entry = _verboseMeta.images[imageKey]\n if (!entry) return undefined\n const sizeData = entry.sizes[size || 'medium'] || entry.sizes.full\n return sizeData\n }\n \n return undefined\n}\n"]}
package/dist/index.mjs CHANGED
@@ -175,42 +175,114 @@ function LoadingState() {
175
175
  ] }) });
176
176
  }
177
177
 
178
+ // src/types.ts
179
+ function getThumbnailPath(originalPath, size) {
180
+ const ext = originalPath.match(/\.\w+$/)?.[0] || ".jpg";
181
+ const base = originalPath.replace(/\.\w+$/, "");
182
+ const outputExt = ext.toLowerCase() === ".png" ? ".png" : ".jpg";
183
+ return `/images${base}-${size}${outputExt}`;
184
+ }
185
+ function toLeanMeta(verbose) {
186
+ const lean = {};
187
+ for (const [key, entry] of Object.entries(verbose.images)) {
188
+ const pathKey = entry.original?.path || `/${key}`;
189
+ lean[pathKey] = {
190
+ w: entry.original?.width || 0,
191
+ h: entry.original?.height || 0,
192
+ blur: entry.blurhash || ""
193
+ };
194
+ if (entry.cdn?.synced) {
195
+ lean[pathKey].s = 1;
196
+ }
197
+ }
198
+ return lean;
199
+ }
200
+ function isLeanMeta(meta2) {
201
+ if (!meta2 || typeof meta2 !== "object") return false;
202
+ return !("images" in meta2);
203
+ }
204
+
178
205
  // src/lib/meta.ts
179
- var _meta = {
206
+ var _verboseMeta = null;
207
+ var _leanMeta = {};
208
+ var meta = {
180
209
  $schema: "https://gallop.software/schemas/studio-meta.json",
181
210
  version: 1,
182
211
  generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
183
212
  images: {}
184
213
  };
185
- var meta = _meta;
186
214
  function initializeMeta(data) {
187
- _meta = data;
188
- Object.assign(meta, data);
215
+ if (isLeanMeta(data)) {
216
+ _leanMeta = data;
217
+ _verboseMeta = null;
218
+ } else {
219
+ _verboseMeta = data;
220
+ _leanMeta = toLeanMeta(data);
221
+ Object.assign(meta, data);
222
+ }
189
223
  }
190
- function getImageUrl(imageKey, size = "medium") {
191
- const image = meta.images[imageKey];
192
- if (!image) return void 0;
193
- const sizeData = image.sizes[size] || image.sizes.full;
194
- if (!sizeData) return void 0;
195
- if (image.cdn?.synced && image.cdn.baseUrl) {
196
- return `${image.cdn.baseUrl}${sizeData.path}`;
224
+ function getImageUrl(imageKey, size) {
225
+ const cdnUrl = process.env.CLOUDFLARE_R2_PUBLIC_URL || process.env.NEXT_PUBLIC_CDN_URL;
226
+ const leanEntry = _leanMeta[imageKey];
227
+ if (leanEntry) {
228
+ const sizeMap = {
229
+ small: "sm",
230
+ medium: "md",
231
+ large: "lg"
232
+ };
233
+ if (!size || size === "full") {
234
+ if (leanEntry.s && cdnUrl) {
235
+ return `${cdnUrl}${imageKey}`;
236
+ }
237
+ return imageKey;
238
+ }
239
+ const normalizedSize = sizeMap[size] || size;
240
+ const thumbPath = getThumbnailPath(imageKey, normalizedSize);
241
+ if (leanEntry.s && cdnUrl) {
242
+ return `${cdnUrl}${thumbPath}`;
243
+ }
244
+ return thumbPath;
245
+ }
246
+ if (_verboseMeta) {
247
+ const entry = _verboseMeta.images[imageKey];
248
+ if (!entry) return void 0;
249
+ const sizeData = entry.sizes[size || "medium"] || entry.sizes.full;
250
+ if (!sizeData) return void 0;
251
+ if (entry.cdn?.synced && entry.cdn.baseUrl) {
252
+ return `${entry.cdn.baseUrl}${sizeData.path}`;
253
+ }
254
+ return sizeData.path;
197
255
  }
198
- return sizeData.path;
256
+ return void 0;
199
257
  }
200
258
  function getStudioMeta(imageKey) {
201
- return meta.images[imageKey];
259
+ if (_verboseMeta) {
260
+ return _verboseMeta.images[imageKey];
261
+ }
262
+ return void 0;
202
263
  }
203
- function getImageSize(imageKey, size = "medium") {
204
- const image = meta.images[imageKey];
205
- if (!image) return void 0;
206
- return image.sizes[size] || image.sizes.full;
264
+ function getImageSize(imageKey, size) {
265
+ const leanEntry = _leanMeta[imageKey];
266
+ if (leanEntry) {
267
+ return { width: leanEntry.w, height: leanEntry.h };
268
+ }
269
+ if (_verboseMeta) {
270
+ const entry = _verboseMeta.images[imageKey];
271
+ if (!entry) return void 0;
272
+ const sizeData = entry.sizes[size || "medium"] || entry.sizes.full;
273
+ return sizeData;
274
+ }
275
+ return void 0;
207
276
  }
208
277
  export {
209
278
  StudioButton,
210
279
  getImageSize,
211
280
  getImageUrl,
212
281
  getStudioMeta,
282
+ getThumbnailPath,
213
283
  initializeMeta,
214
- meta
284
+ isLeanMeta,
285
+ meta,
286
+ toLeanMeta
215
287
  };
216
288
  //# sourceMappingURL=index.mjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/components/StudioButton.tsx","../src/lib/meta.ts"],"sourcesContent":["/** @jsxImportSource @emotion/react */\n'use client'\n\nimport { useState, useEffect, lazy, Suspense } from 'react'\nimport { css, keyframes } from '@emotion/react'\nimport { colors, fontStack, fontSize, baseReset } from './tokens'\n\n// Lazy load the full Studio UI to avoid bundling in production\nconst StudioUI = lazy(() => import('./StudioUI'))\n\nconst spin = keyframes`\n to {\n transform: rotate(360deg);\n }\n`\n\nconst styles = {\n button: css`\n position: fixed;\n bottom: 24px;\n right: 24px;\n z-index: 9998;\n width: 52px;\n height: 52px;\n border-radius: 50%;\n background: ${colors.primary};\n color: white;\n box-shadow: 0 4px 12px ${colors.shadowDark}, 0 1px 3px ${colors.shadow};\n display: flex;\n align-items: center;\n justify-content: center;\n border: none;\n cursor: pointer;\n transition: all 0.15s ease;\n font-family: ${fontStack};\n \n &:hover {\n transform: translateY(-2px);\n box-shadow: 0 8px 20px ${colors.shadowDark}, 0 2px 6px ${colors.shadow};\n background: ${colors.primaryHover};\n }\n \n &:active {\n transform: translateY(0);\n }\n `,\n buttonIcon: css`\n width: 24px;\n height: 24px;\n `,\n overlay: css`\n position: fixed;\n top: 0;\n right: 0;\n bottom: 0;\n left: 0;\n z-index: 9999;\n transition: opacity 0.2s ease, visibility 0.2s ease;\n `,\n overlayHidden: css`\n opacity: 0;\n visibility: hidden;\n pointer-events: none;\n `,\n backdrop: css`\n position: absolute;\n top: 0;\n right: 0;\n bottom: 0;\n left: 0;\n background-color: rgba(26, 31, 54, 0.4);\n backdrop-filter: blur(4px);\n `,\n modal: css`\n ${baseReset}\n position: absolute;\n top: 24px;\n right: 24px;\n bottom: 24px;\n left: 24px;\n background-color: ${colors.surface};\n border-radius: 12px;\n box-shadow: 0 30px 60px -12px rgba(50, 50, 93, 0.25), 0 18px 36px -18px rgba(0, 0, 0, 0.3);\n display: flex;\n flex-direction: column;\n overflow: hidden;\n `,\n loading: css`\n display: flex;\n align-items: center;\n justify-content: center;\n height: 100%;\n background: ${colors.background};\n font-family: ${fontStack};\n `,\n loadingContent: css`\n display: flex;\n flex-direction: column;\n align-items: center;\n gap: 16px;\n `,\n spinner: css`\n width: 36px;\n height: 36px;\n border-radius: 50%;\n border: 3px solid ${colors.border};\n border-top-color: ${colors.primary};\n animation: ${spin} 0.8s linear infinite;\n `,\n loadingText: css`\n color: ${colors.textSecondary};\n font-size: ${fontSize.base};\n font-weight: 500;\n margin: 0;\n letter-spacing: -0.01em;\n `,\n}\n\n/**\n * Floating button that opens the Studio modal.\n * Fixed position in bottom-right corner.\n * Only renders in development mode.\n */\nexport function StudioButton() {\n const [mounted, setMounted] = useState(false)\n const [isOpen, setIsOpen] = useState(false)\n const [hasBeenOpened, setHasBeenOpened] = useState(false)\n\n // Only render on client to avoid hydration mismatch\n useEffect(() => {\n setMounted(true)\n }, [])\n\n const handleOpen = () => {\n setIsOpen(true)\n setHasBeenOpened(true)\n }\n\n // Only render in development and on client\n if (!mounted || process.env.NODE_ENV !== 'development') {\n return null\n }\n\n return (\n <>\n {!isOpen && (\n <button\n css={styles.button}\n onClick={handleOpen}\n title=\"Open Studio\"\n aria-label=\"Open Studio media manager\"\n >\n <ImageIcon />\n </button>\n )}\n\n {/* Keep mounted once opened to preserve state */}\n {hasBeenOpened && (\n <div css={[styles.overlay, !isOpen && styles.overlayHidden]}>\n <div css={styles.backdrop} onClick={() => setIsOpen(false)} />\n <div css={styles.modal}>\n <Suspense fallback={<LoadingState />}>\n <StudioUI onClose={() => setIsOpen(false)} isVisible={isOpen} />\n </Suspense>\n </div>\n </div>\n )}\n </>\n )\n}\n\nfunction ImageIcon() {\n return (\n <svg\n css={styles.buttonIcon}\n xmlns=\"http://www.w3.org/2000/svg\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth={2}\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n >\n <rect x=\"3\" y=\"3\" width=\"18\" height=\"18\" rx=\"2\" ry=\"2\" />\n <circle cx=\"8.5\" cy=\"8.5\" r=\"1.5\" />\n <polyline points=\"21 15 16 10 5 21\" />\n </svg>\n )\n}\n\nfunction LoadingState() {\n return (\n <div css={styles.loading}>\n <div css={styles.loadingContent}>\n <div css={styles.spinner} />\n <p css={styles.loadingText}>Loading Studio...</p>\n </div>\n </div>\n )\n}\n","import type { StudioMeta, ImageEntry, ImageSize, SizeEntry } from '../types'\n\n// Default empty meta - will be populated when reading from project\nlet _meta: StudioMeta = {\n $schema: 'https://gallop.software/schemas/studio-meta.json',\n version: 1,\n generatedAt: new Date().toISOString(),\n images: {},\n}\n\n/**\n * The meta object containing all image metadata.\n * This is read from _data/_meta.json in the consuming project.\n */\nexport const meta: StudioMeta = _meta\n\n/**\n * Initialize meta from a JSON object (called during build/runtime)\n */\nexport function initializeMeta(data: StudioMeta): void {\n _meta = data\n Object.assign(meta, data)\n}\n\n/**\n * Get the resolved URL for an image, handling CDN vs local paths\n */\nexport function getImageUrl(\n imageKey: string,\n size: ImageSize = 'medium'\n): string | undefined {\n const image = meta.images[imageKey]\n if (!image) return undefined\n\n const sizeData = image.sizes[size] || image.sizes.full\n if (!sizeData) return undefined\n\n // If synced to CDN, use CDN URL\n if (image.cdn?.synced && image.cdn.baseUrl) {\n return `${image.cdn.baseUrl}${sizeData.path}`\n }\n\n // Otherwise use local path\n return sizeData.path\n}\n\n/**\n * Get the full image entry for a key\n */\nexport function getStudioMeta(imageKey: string): ImageEntry | undefined {\n return meta.images[imageKey]\n}\n\n/**\n * Get size data for an image\n */\nexport function getImageSize(\n imageKey: string,\n size: ImageSize = 'medium'\n): SizeEntry | undefined {\n const image = meta.images[imageKey]\n if (!image) return undefined\n return image.sizes[size] || image.sizes.full\n}\n"],"mappings":";;;;;;;;;AAGA,SAAS,UAAU,WAAW,MAAM,gBAAgB;AACpD,SAAS,KAAK,iBAAiB;AA4I3B,mBAQM,KAMF,YAdJ;AAxIJ,IAAM,WAAW,KAAK,MAAM,OAAO,yBAAY,CAAC;AAEhD,IAAM,OAAO;AAAA;AAAA;AAAA;AAAA;AAMb,IAAM,SAAS;AAAA,EACb,QAAQ;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,kBAQQ,OAAO,OAAO;AAAA;AAAA,6BAEH,OAAO,UAAU,eAAe,OAAO,MAAM;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,mBAOvD,SAAS;AAAA;AAAA;AAAA;AAAA,+BAIG,OAAO,UAAU,eAAe,OAAO,MAAM;AAAA,oBACxD,OAAO,YAAY;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOrC,YAAY;AAAA;AAAA;AAAA;AAAA,EAIZ,SAAS;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAST,eAAe;AAAA;AAAA;AAAA;AAAA;AAAA,EAKf,UAAU;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASV,OAAO;AAAA,MACH,SAAS;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,wBAMS,OAAO,OAAO;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOpC,SAAS;AAAA;AAAA;AAAA;AAAA;AAAA,kBAKO,OAAO,UAAU;AAAA,mBAChB,SAAS;AAAA;AAAA,EAE1B,gBAAgB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAMhB,SAAS;AAAA;AAAA;AAAA;AAAA,wBAIa,OAAO,MAAM;AAAA,wBACb,OAAO,OAAO;AAAA,iBACrB,IAAI;AAAA;AAAA,EAEnB,aAAa;AAAA,aACF,OAAO,aAAa;AAAA,iBAChB,SAAS,IAAI;AAAA;AAAA;AAAA;AAAA;AAK9B;AAOO,SAAS,eAAe;AAC7B,QAAM,CAAC,SAAS,UAAU,IAAI,SAAS,KAAK;AAC5C,QAAM,CAAC,QAAQ,SAAS,IAAI,SAAS,KAAK;AAC1C,QAAM,CAAC,eAAe,gBAAgB,IAAI,SAAS,KAAK;AAGxD,YAAU,MAAM;AACd,eAAW,IAAI;AAAA,EACjB,GAAG,CAAC,CAAC;AAEL,QAAM,aAAa,MAAM;AACvB,cAAU,IAAI;AACd,qBAAiB,IAAI;AAAA,EACvB;AAGA,MAAI,CAAC,WAAW,QAAQ,IAAI,aAAa,eAAe;AACtD,WAAO;AAAA,EACT;AAEA,SACE,iCACG;AAAA,KAAC,UACA;AAAA,MAAC;AAAA;AAAA,QACC,KAAK,OAAO;AAAA,QACZ,SAAS;AAAA,QACT,OAAM;AAAA,QACN,cAAW;AAAA,QAEX,8BAAC,aAAU;AAAA;AAAA,IACb;AAAA,IAID,iBACC,qBAAC,SAAI,KAAK,CAAC,OAAO,SAAS,CAAC,UAAU,OAAO,aAAa,GACxD;AAAA,0BAAC,SAAI,KAAK,OAAO,UAAU,SAAS,MAAM,UAAU,KAAK,GAAG;AAAA,MAC5D,oBAAC,SAAI,KAAK,OAAO,OACf,8BAAC,YAAS,UAAU,oBAAC,gBAAa,GAChC,8BAAC,YAAS,SAAS,MAAM,UAAU,KAAK,GAAG,WAAW,QAAQ,GAChE,GACF;AAAA,OACF;AAAA,KAEJ;AAEJ;AAEA,SAAS,YAAY;AACnB,SACE;AAAA,IAAC;AAAA;AAAA,MACC,KAAK,OAAO;AAAA,MACZ,OAAM;AAAA,MACN,SAAQ;AAAA,MACR,MAAK;AAAA,MACL,QAAO;AAAA,MACP,aAAa;AAAA,MACb,eAAc;AAAA,MACd,gBAAe;AAAA,MAEf;AAAA,4BAAC,UAAK,GAAE,KAAI,GAAE,KAAI,OAAM,MAAK,QAAO,MAAK,IAAG,KAAI,IAAG,KAAI;AAAA,QACvD,oBAAC,YAAO,IAAG,OAAM,IAAG,OAAM,GAAE,OAAM;AAAA,QAClC,oBAAC,cAAS,QAAO,oBAAmB;AAAA;AAAA;AAAA,EACtC;AAEJ;AAEA,SAAS,eAAe;AACtB,SACE,oBAAC,SAAI,KAAK,OAAO,SACf,+BAAC,SAAI,KAAK,OAAO,gBACf;AAAA,wBAAC,SAAI,KAAK,OAAO,SAAS;AAAA,IAC1B,oBAAC,OAAE,KAAK,OAAO,aAAa,+BAAiB;AAAA,KAC/C,GACF;AAEJ;;;ACpMA,IAAI,QAAoB;AAAA,EACtB,SAAS;AAAA,EACT,SAAS;AAAA,EACT,cAAa,oBAAI,KAAK,GAAE,YAAY;AAAA,EACpC,QAAQ,CAAC;AACX;AAMO,IAAM,OAAmB;AAKzB,SAAS,eAAe,MAAwB;AACrD,UAAQ;AACR,SAAO,OAAO,MAAM,IAAI;AAC1B;AAKO,SAAS,YACd,UACA,OAAkB,UACE;AACpB,QAAM,QAAQ,KAAK,OAAO,QAAQ;AAClC,MAAI,CAAC,MAAO,QAAO;AAEnB,QAAM,WAAW,MAAM,MAAM,IAAI,KAAK,MAAM,MAAM;AAClD,MAAI,CAAC,SAAU,QAAO;AAGtB,MAAI,MAAM,KAAK,UAAU,MAAM,IAAI,SAAS;AAC1C,WAAO,GAAG,MAAM,IAAI,OAAO,GAAG,SAAS,IAAI;AAAA,EAC7C;AAGA,SAAO,SAAS;AAClB;AAKO,SAAS,cAAc,UAA0C;AACtE,SAAO,KAAK,OAAO,QAAQ;AAC7B;AAKO,SAAS,aACd,UACA,OAAkB,UACK;AACvB,QAAM,QAAQ,KAAK,OAAO,QAAQ;AAClC,MAAI,CAAC,MAAO,QAAO;AACnB,SAAO,MAAM,MAAM,IAAI,KAAK,MAAM,MAAM;AAC1C;","names":[]}
1
+ {"version":3,"sources":["../src/components/StudioButton.tsx","../src/types.ts","../src/lib/meta.ts"],"sourcesContent":["/** @jsxImportSource @emotion/react */\n'use client'\n\nimport { useState, useEffect, lazy, Suspense } from 'react'\nimport { css, keyframes } from '@emotion/react'\nimport { colors, fontStack, fontSize, baseReset } from './tokens'\n\n// Lazy load the full Studio UI to avoid bundling in production\nconst StudioUI = lazy(() => import('./StudioUI'))\n\nconst spin = keyframes`\n to {\n transform: rotate(360deg);\n }\n`\n\nconst styles = {\n button: css`\n position: fixed;\n bottom: 24px;\n right: 24px;\n z-index: 9998;\n width: 52px;\n height: 52px;\n border-radius: 50%;\n background: ${colors.primary};\n color: white;\n box-shadow: 0 4px 12px ${colors.shadowDark}, 0 1px 3px ${colors.shadow};\n display: flex;\n align-items: center;\n justify-content: center;\n border: none;\n cursor: pointer;\n transition: all 0.15s ease;\n font-family: ${fontStack};\n \n &:hover {\n transform: translateY(-2px);\n box-shadow: 0 8px 20px ${colors.shadowDark}, 0 2px 6px ${colors.shadow};\n background: ${colors.primaryHover};\n }\n \n &:active {\n transform: translateY(0);\n }\n `,\n buttonIcon: css`\n width: 24px;\n height: 24px;\n `,\n overlay: css`\n position: fixed;\n top: 0;\n right: 0;\n bottom: 0;\n left: 0;\n z-index: 9999;\n transition: opacity 0.2s ease, visibility 0.2s ease;\n `,\n overlayHidden: css`\n opacity: 0;\n visibility: hidden;\n pointer-events: none;\n `,\n backdrop: css`\n position: absolute;\n top: 0;\n right: 0;\n bottom: 0;\n left: 0;\n background-color: rgba(26, 31, 54, 0.4);\n backdrop-filter: blur(4px);\n `,\n modal: css`\n ${baseReset}\n position: absolute;\n top: 24px;\n right: 24px;\n bottom: 24px;\n left: 24px;\n background-color: ${colors.surface};\n border-radius: 12px;\n box-shadow: 0 30px 60px -12px rgba(50, 50, 93, 0.25), 0 18px 36px -18px rgba(0, 0, 0, 0.3);\n display: flex;\n flex-direction: column;\n overflow: hidden;\n `,\n loading: css`\n display: flex;\n align-items: center;\n justify-content: center;\n height: 100%;\n background: ${colors.background};\n font-family: ${fontStack};\n `,\n loadingContent: css`\n display: flex;\n flex-direction: column;\n align-items: center;\n gap: 16px;\n `,\n spinner: css`\n width: 36px;\n height: 36px;\n border-radius: 50%;\n border: 3px solid ${colors.border};\n border-top-color: ${colors.primary};\n animation: ${spin} 0.8s linear infinite;\n `,\n loadingText: css`\n color: ${colors.textSecondary};\n font-size: ${fontSize.base};\n font-weight: 500;\n margin: 0;\n letter-spacing: -0.01em;\n `,\n}\n\n/**\n * Floating button that opens the Studio modal.\n * Fixed position in bottom-right corner.\n * Only renders in development mode.\n */\nexport function StudioButton() {\n const [mounted, setMounted] = useState(false)\n const [isOpen, setIsOpen] = useState(false)\n const [hasBeenOpened, setHasBeenOpened] = useState(false)\n\n // Only render on client to avoid hydration mismatch\n useEffect(() => {\n setMounted(true)\n }, [])\n\n const handleOpen = () => {\n setIsOpen(true)\n setHasBeenOpened(true)\n }\n\n // Only render in development and on client\n if (!mounted || process.env.NODE_ENV !== 'development') {\n return null\n }\n\n return (\n <>\n {!isOpen && (\n <button\n css={styles.button}\n onClick={handleOpen}\n title=\"Open Studio\"\n aria-label=\"Open Studio media manager\"\n >\n <ImageIcon />\n </button>\n )}\n\n {/* Keep mounted once opened to preserve state */}\n {hasBeenOpened && (\n <div css={[styles.overlay, !isOpen && styles.overlayHidden]}>\n <div css={styles.backdrop} onClick={() => setIsOpen(false)} />\n <div css={styles.modal}>\n <Suspense fallback={<LoadingState />}>\n <StudioUI onClose={() => setIsOpen(false)} isVisible={isOpen} />\n </Suspense>\n </div>\n </div>\n )}\n </>\n )\n}\n\nfunction ImageIcon() {\n return (\n <svg\n css={styles.buttonIcon}\n xmlns=\"http://www.w3.org/2000/svg\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth={2}\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n >\n <rect x=\"3\" y=\"3\" width=\"18\" height=\"18\" rx=\"2\" ry=\"2\" />\n <circle cx=\"8.5\" cy=\"8.5\" r=\"1.5\" />\n <polyline points=\"21 15 16 10 5 21\" />\n </svg>\n )\n}\n\nfunction LoadingState() {\n return (\n <div css={styles.loading}>\n <div css={styles.loadingContent}>\n <div css={styles.spinner} />\n <p css={styles.loadingText}>Loading Studio...</p>\n </div>\n </div>\n )\n}\n","/**\n * Image size variants (verbose format used by handlers)\n */\nexport type ImageSize = 'small' | 'medium' | 'large' | 'full'\n\n/**\n * Image size variants (lean format for consumers)\n */\nexport type LeanImageSize = 'sm' | 'md' | 'lg'\n\n/**\n * Size entry with path and dimensions\n */\nexport interface SizeEntry {\n path: string\n width: number\n height: number\n}\n\n/**\n * CDN sync status\n */\nexport interface CdnStatus {\n synced: boolean\n baseUrl: string\n syncedAt: string\n}\n\n/**\n * Image entry in meta (verbose format - used by Studio handlers)\n */\nexport interface ImageEntry {\n original: {\n path: string\n width: number\n height: number\n fileSize: number\n }\n sizes: {\n full: SizeEntry\n large: SizeEntry\n medium: SizeEntry\n small: SizeEntry\n [key: string]: SizeEntry\n }\n blurhash: string\n dominantColor: string\n cdn: CdnStatus | null\n}\n\n/**\n * Studio meta schema (verbose format - used by Studio handlers)\n */\nexport interface StudioMeta {\n $schema: string\n version: number\n generatedAt: string\n images: Record<string, ImageEntry>\n}\n\n/**\n * Lean image entry - minimal metadata for consumers\n * ~80 bytes per image vs ~500 bytes in verbose format\n */\nexport interface LeanImageEntry {\n /** Original width */\n w: number\n /** Original height */\n h: number\n /** Blurhash for placeholder */\n blur: string\n /** Synced to CDN (present and 1 if synced, omit if not) */\n s?: 1\n}\n\n/**\n * Lean meta schema - flat structure with path as key\n */\nexport type LeanMeta = Record<string, LeanImageEntry>\n\n/**\n * File/folder item for browser\n */\nexport interface FileItem {\n name: string\n path: string\n type: 'file' | 'folder'\n size?: number\n dimensions?: { width: number; height: number }\n cdnSynced?: boolean\n fileCount?: number\n totalSize?: number\n thumbnail?: string\n hasThumbnail?: boolean\n}\n\n/**\n * Studio configuration\n */\nexport interface StudioConfig {\n r2AccountId?: string\n r2AccessKeyId?: string\n r2SecretAccessKey?: string\n r2BucketName?: string\n r2PublicUrl?: string\n thumbnailSizes?: {\n small: number\n medium: number\n large: number\n }\n}\n\n/**\n * Helper to derive thumbnail path from original path\n */\nexport function getThumbnailPath(originalPath: string, size: LeanImageSize): string {\n const ext = originalPath.match(/\\.\\w+$/)?.[0] || '.jpg'\n const base = originalPath.replace(/\\.\\w+$/, '')\n const outputExt = ext.toLowerCase() === '.png' ? '.png' : '.jpg'\n return `/images${base}-${size}${outputExt}`\n}\n\n/**\n * Convert verbose StudioMeta to LeanMeta\n */\nexport function toLeanMeta(verbose: StudioMeta): LeanMeta {\n const lean: LeanMeta = {}\n \n for (const [key, entry] of Object.entries(verbose.images)) {\n const pathKey = entry.original?.path || `/${key}`\n lean[pathKey] = {\n w: entry.original?.width || 0,\n h: entry.original?.height || 0,\n blur: entry.blurhash || '',\n }\n if (entry.cdn?.synced) {\n lean[pathKey].s = 1\n }\n }\n \n return lean\n}\n\n/**\n * Check if meta is in lean format (no 'images' wrapper)\n */\nexport function isLeanMeta(meta: unknown): meta is LeanMeta {\n if (!meta || typeof meta !== 'object') return false\n // Lean format doesn't have 'images' property\n return !('images' in meta)\n}\n","import type { StudioMeta, ImageEntry, LeanMeta, LeanImageEntry } from '../types'\nimport { getThumbnailPath, toLeanMeta, isLeanMeta } from '../types'\n\n// Unified meta - can be either format\ntype UnifiedMeta = StudioMeta | LeanMeta\n\n// Store both formats\nlet _verboseMeta: StudioMeta | null = null\nlet _leanMeta: LeanMeta = {}\n\n/**\n * The meta object in verbose format (for backward compatibility)\n */\nexport const meta: StudioMeta = {\n $schema: 'https://gallop.software/schemas/studio-meta.json',\n version: 1,\n generatedAt: new Date().toISOString(),\n images: {},\n}\n\n/**\n * Get lean meta for efficient access\n */\nexport function getLeanMeta(): LeanMeta {\n return _leanMeta\n}\n\n/**\n * Initialize meta from a JSON object (handles both formats)\n */\nexport function initializeMeta(data: UnifiedMeta): void {\n if (isLeanMeta(data)) {\n // Already lean format\n _leanMeta = data\n _verboseMeta = null\n } else {\n // Verbose format - convert to lean\n _verboseMeta = data\n _leanMeta = toLeanMeta(data)\n // Also update the exported meta object for backward compat\n Object.assign(meta, data)\n }\n}\n\n/**\n * Get the resolved URL for an image\n * Works with both verbose and lean formats\n */\nexport function getImageUrl(\n imageKey: string,\n size?: 'sm' | 'md' | 'lg' | 'small' | 'medium' | 'large' | 'full'\n): string | undefined {\n const cdnUrl = process.env.CLOUDFLARE_R2_PUBLIC_URL || process.env.NEXT_PUBLIC_CDN_URL\n \n // Try lean meta first\n const leanEntry = _leanMeta[imageKey]\n if (leanEntry) {\n // Map old size names to new\n const sizeMap: Record<string, 'sm' | 'md' | 'lg'> = {\n small: 'sm',\n medium: 'md', \n large: 'lg',\n }\n \n if (!size || size === 'full') {\n // Return original path\n if (leanEntry.s && cdnUrl) {\n return `${cdnUrl}${imageKey}`\n }\n return imageKey\n }\n \n const normalizedSize = sizeMap[size] || size as 'sm' | 'md' | 'lg'\n const thumbPath = getThumbnailPath(imageKey, normalizedSize)\n \n if (leanEntry.s && cdnUrl) {\n return `${cdnUrl}${thumbPath}`\n }\n return thumbPath\n }\n \n // Fall back to verbose meta\n if (_verboseMeta) {\n const entry = _verboseMeta.images[imageKey]\n if (!entry) return undefined\n \n const sizeData = entry.sizes[size || 'medium'] || entry.sizes.full\n if (!sizeData) return undefined\n \n if (entry.cdn?.synced && entry.cdn.baseUrl) {\n return `${entry.cdn.baseUrl}${sizeData.path}`\n }\n return sizeData.path\n }\n \n return undefined\n}\n\n/**\n * Get image entry (verbose format for backward compat)\n */\nexport function getStudioMeta(imageKey: string): ImageEntry | undefined {\n if (_verboseMeta) {\n return _verboseMeta.images[imageKey]\n }\n return undefined\n}\n\n/**\n * Get lean entry\n */\nexport function getLeanEntry(imageKey: string): LeanImageEntry | undefined {\n return _leanMeta[imageKey]\n}\n\n/**\n * Get dimensions for an image\n */\nexport function getImageSize(\n imageKey: string,\n size?: 'sm' | 'md' | 'lg' | 'small' | 'medium' | 'large' | 'full'\n): { width: number; height: number; path?: string } | undefined {\n // Try lean meta first\n const leanEntry = _leanMeta[imageKey]\n if (leanEntry) {\n // For lean, we only have original dimensions\n return { width: leanEntry.w, height: leanEntry.h }\n }\n \n // Fall back to verbose meta\n if (_verboseMeta) {\n const entry = _verboseMeta.images[imageKey]\n if (!entry) return undefined\n const sizeData = entry.sizes[size || 'medium'] || entry.sizes.full\n return sizeData\n }\n \n return undefined\n}\n"],"mappings":";;;;;;;;;AAGA,SAAS,UAAU,WAAW,MAAM,gBAAgB;AACpD,SAAS,KAAK,iBAAiB;AA4I3B,mBAQM,KAMF,YAdJ;AAxIJ,IAAM,WAAW,KAAK,MAAM,OAAO,yBAAY,CAAC;AAEhD,IAAM,OAAO;AAAA;AAAA;AAAA;AAAA;AAMb,IAAM,SAAS;AAAA,EACb,QAAQ;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,kBAQQ,OAAO,OAAO;AAAA;AAAA,6BAEH,OAAO,UAAU,eAAe,OAAO,MAAM;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,mBAOvD,SAAS;AAAA;AAAA;AAAA;AAAA,+BAIG,OAAO,UAAU,eAAe,OAAO,MAAM;AAAA,oBACxD,OAAO,YAAY;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOrC,YAAY;AAAA;AAAA;AAAA;AAAA,EAIZ,SAAS;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAST,eAAe;AAAA;AAAA;AAAA;AAAA;AAAA,EAKf,UAAU;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASV,OAAO;AAAA,MACH,SAAS;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,wBAMS,OAAO,OAAO;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOpC,SAAS;AAAA;AAAA;AAAA;AAAA;AAAA,kBAKO,OAAO,UAAU;AAAA,mBAChB,SAAS;AAAA;AAAA,EAE1B,gBAAgB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAMhB,SAAS;AAAA;AAAA;AAAA;AAAA,wBAIa,OAAO,MAAM;AAAA,wBACb,OAAO,OAAO;AAAA,iBACrB,IAAI;AAAA;AAAA,EAEnB,aAAa;AAAA,aACF,OAAO,aAAa;AAAA,iBAChB,SAAS,IAAI;AAAA;AAAA;AAAA;AAAA;AAK9B;AAOO,SAAS,eAAe;AAC7B,QAAM,CAAC,SAAS,UAAU,IAAI,SAAS,KAAK;AAC5C,QAAM,CAAC,QAAQ,SAAS,IAAI,SAAS,KAAK;AAC1C,QAAM,CAAC,eAAe,gBAAgB,IAAI,SAAS,KAAK;AAGxD,YAAU,MAAM;AACd,eAAW,IAAI;AAAA,EACjB,GAAG,CAAC,CAAC;AAEL,QAAM,aAAa,MAAM;AACvB,cAAU,IAAI;AACd,qBAAiB,IAAI;AAAA,EACvB;AAGA,MAAI,CAAC,WAAW,QAAQ,IAAI,aAAa,eAAe;AACtD,WAAO;AAAA,EACT;AAEA,SACE,iCACG;AAAA,KAAC,UACA;AAAA,MAAC;AAAA;AAAA,QACC,KAAK,OAAO;AAAA,QACZ,SAAS;AAAA,QACT,OAAM;AAAA,QACN,cAAW;AAAA,QAEX,8BAAC,aAAU;AAAA;AAAA,IACb;AAAA,IAID,iBACC,qBAAC,SAAI,KAAK,CAAC,OAAO,SAAS,CAAC,UAAU,OAAO,aAAa,GACxD;AAAA,0BAAC,SAAI,KAAK,OAAO,UAAU,SAAS,MAAM,UAAU,KAAK,GAAG;AAAA,MAC5D,oBAAC,SAAI,KAAK,OAAO,OACf,8BAAC,YAAS,UAAU,oBAAC,gBAAa,GAChC,8BAAC,YAAS,SAAS,MAAM,UAAU,KAAK,GAAG,WAAW,QAAQ,GAChE,GACF;AAAA,OACF;AAAA,KAEJ;AAEJ;AAEA,SAAS,YAAY;AACnB,SACE;AAAA,IAAC;AAAA;AAAA,MACC,KAAK,OAAO;AAAA,MACZ,OAAM;AAAA,MACN,SAAQ;AAAA,MACR,MAAK;AAAA,MACL,QAAO;AAAA,MACP,aAAa;AAAA,MACb,eAAc;AAAA,MACd,gBAAe;AAAA,MAEf;AAAA,4BAAC,UAAK,GAAE,KAAI,GAAE,KAAI,OAAM,MAAK,QAAO,MAAK,IAAG,KAAI,IAAG,KAAI;AAAA,QACvD,oBAAC,YAAO,IAAG,OAAM,IAAG,OAAM,GAAE,OAAM;AAAA,QAClC,oBAAC,cAAS,QAAO,oBAAmB;AAAA;AAAA;AAAA,EACtC;AAEJ;AAEA,SAAS,eAAe;AACtB,SACE,oBAAC,SAAI,KAAK,OAAO,SACf,+BAAC,SAAI,KAAK,OAAO,gBACf;AAAA,wBAAC,SAAI,KAAK,OAAO,SAAS;AAAA,IAC1B,oBAAC,OAAE,KAAK,OAAO,aAAa,+BAAiB;AAAA,KAC/C,GACF;AAEJ;;;ACpFO,SAAS,iBAAiB,cAAsB,MAA6B;AAClF,QAAM,MAAM,aAAa,MAAM,QAAQ,IAAI,CAAC,KAAK;AACjD,QAAM,OAAO,aAAa,QAAQ,UAAU,EAAE;AAC9C,QAAM,YAAY,IAAI,YAAY,MAAM,SAAS,SAAS;AAC1D,SAAO,UAAU,IAAI,IAAI,IAAI,GAAG,SAAS;AAC3C;AAKO,SAAS,WAAW,SAA+B;AACxD,QAAM,OAAiB,CAAC;AAExB,aAAW,CAAC,KAAK,KAAK,KAAK,OAAO,QAAQ,QAAQ,MAAM,GAAG;AACzD,UAAM,UAAU,MAAM,UAAU,QAAQ,IAAI,GAAG;AAC/C,SAAK,OAAO,IAAI;AAAA,MACd,GAAG,MAAM,UAAU,SAAS;AAAA,MAC5B,GAAG,MAAM,UAAU,UAAU;AAAA,MAC7B,MAAM,MAAM,YAAY;AAAA,IAC1B;AACA,QAAI,MAAM,KAAK,QAAQ;AACrB,WAAK,OAAO,EAAE,IAAI;AAAA,IACpB;AAAA,EACF;AAEA,SAAO;AACT;AAKO,SAAS,WAAWA,OAAiC;AAC1D,MAAI,CAACA,SAAQ,OAAOA,UAAS,SAAU,QAAO;AAE9C,SAAO,EAAE,YAAYA;AACvB;;;AC/IA,IAAI,eAAkC;AACtC,IAAI,YAAsB,CAAC;AAKpB,IAAM,OAAmB;AAAA,EAC9B,SAAS;AAAA,EACT,SAAS;AAAA,EACT,cAAa,oBAAI,KAAK,GAAE,YAAY;AAAA,EACpC,QAAQ,CAAC;AACX;AAYO,SAAS,eAAe,MAAyB;AACtD,MAAI,WAAW,IAAI,GAAG;AAEpB,gBAAY;AACZ,mBAAe;AAAA,EACjB,OAAO;AAEL,mBAAe;AACf,gBAAY,WAAW,IAAI;AAE3B,WAAO,OAAO,MAAM,IAAI;AAAA,EAC1B;AACF;AAMO,SAAS,YACd,UACA,MACoB;AACpB,QAAM,SAAS,QAAQ,IAAI,4BAA4B,QAAQ,IAAI;AAGnE,QAAM,YAAY,UAAU,QAAQ;AACpC,MAAI,WAAW;AAEb,UAAM,UAA8C;AAAA,MAClD,OAAO;AAAA,MACP,QAAQ;AAAA,MACR,OAAO;AAAA,IACT;AAEA,QAAI,CAAC,QAAQ,SAAS,QAAQ;AAE5B,UAAI,UAAU,KAAK,QAAQ;AACzB,eAAO,GAAG,MAAM,GAAG,QAAQ;AAAA,MAC7B;AACA,aAAO;AAAA,IACT;AAEA,UAAM,iBAAiB,QAAQ,IAAI,KAAK;AACxC,UAAM,YAAY,iBAAiB,UAAU,cAAc;AAE3D,QAAI,UAAU,KAAK,QAAQ;AACzB,aAAO,GAAG,MAAM,GAAG,SAAS;AAAA,IAC9B;AACA,WAAO;AAAA,EACT;AAGA,MAAI,cAAc;AAChB,UAAM,QAAQ,aAAa,OAAO,QAAQ;AAC1C,QAAI,CAAC,MAAO,QAAO;AAEnB,UAAM,WAAW,MAAM,MAAM,QAAQ,QAAQ,KAAK,MAAM,MAAM;AAC9D,QAAI,CAAC,SAAU,QAAO;AAEtB,QAAI,MAAM,KAAK,UAAU,MAAM,IAAI,SAAS;AAC1C,aAAO,GAAG,MAAM,IAAI,OAAO,GAAG,SAAS,IAAI;AAAA,IAC7C;AACA,WAAO,SAAS;AAAA,EAClB;AAEA,SAAO;AACT;AAKO,SAAS,cAAc,UAA0C;AACtE,MAAI,cAAc;AAChB,WAAO,aAAa,OAAO,QAAQ;AAAA,EACrC;AACA,SAAO;AACT;AAYO,SAAS,aACd,UACA,MAC8D;AAE9D,QAAM,YAAY,UAAU,QAAQ;AACpC,MAAI,WAAW;AAEb,WAAO,EAAE,OAAO,UAAU,GAAG,QAAQ,UAAU,EAAE;AAAA,EACnD;AAGA,MAAI,cAAc;AAChB,UAAM,QAAQ,aAAa,OAAO,QAAQ;AAC1C,QAAI,CAAC,MAAO,QAAO;AACnB,UAAM,WAAW,MAAM,MAAM,QAAQ,QAAQ,KAAK,MAAM,MAAM;AAC9D,WAAO;AAAA,EACT;AAEA,SAAO;AACT;","names":["meta"]}
@@ -0,0 +1,119 @@
1
+ /**
2
+ * Image size variants (verbose format used by handlers)
3
+ */
4
+ type ImageSize = 'small' | 'medium' | 'large' | 'full';
5
+ /**
6
+ * Image size variants (lean format for consumers)
7
+ */
8
+ type LeanImageSize = 'sm' | 'md' | 'lg';
9
+ /**
10
+ * Size entry with path and dimensions
11
+ */
12
+ interface SizeEntry {
13
+ path: string;
14
+ width: number;
15
+ height: number;
16
+ }
17
+ /**
18
+ * CDN sync status
19
+ */
20
+ interface CdnStatus {
21
+ synced: boolean;
22
+ baseUrl: string;
23
+ syncedAt: string;
24
+ }
25
+ /**
26
+ * Image entry in meta (verbose format - used by Studio handlers)
27
+ */
28
+ interface ImageEntry {
29
+ original: {
30
+ path: string;
31
+ width: number;
32
+ height: number;
33
+ fileSize: number;
34
+ };
35
+ sizes: {
36
+ full: SizeEntry;
37
+ large: SizeEntry;
38
+ medium: SizeEntry;
39
+ small: SizeEntry;
40
+ [key: string]: SizeEntry;
41
+ };
42
+ blurhash: string;
43
+ dominantColor: string;
44
+ cdn: CdnStatus | null;
45
+ }
46
+ /**
47
+ * Studio meta schema (verbose format - used by Studio handlers)
48
+ */
49
+ interface StudioMeta {
50
+ $schema: string;
51
+ version: number;
52
+ generatedAt: string;
53
+ images: Record<string, ImageEntry>;
54
+ }
55
+ /**
56
+ * Lean image entry - minimal metadata for consumers
57
+ * ~80 bytes per image vs ~500 bytes in verbose format
58
+ */
59
+ interface LeanImageEntry {
60
+ /** Original width */
61
+ w: number;
62
+ /** Original height */
63
+ h: number;
64
+ /** Blurhash for placeholder */
65
+ blur: string;
66
+ /** Synced to CDN (present and 1 if synced, omit if not) */
67
+ s?: 1;
68
+ }
69
+ /**
70
+ * Lean meta schema - flat structure with path as key
71
+ */
72
+ type LeanMeta = Record<string, LeanImageEntry>;
73
+ /**
74
+ * File/folder item for browser
75
+ */
76
+ interface FileItem {
77
+ name: string;
78
+ path: string;
79
+ type: 'file' | 'folder';
80
+ size?: number;
81
+ dimensions?: {
82
+ width: number;
83
+ height: number;
84
+ };
85
+ cdnSynced?: boolean;
86
+ fileCount?: number;
87
+ totalSize?: number;
88
+ thumbnail?: string;
89
+ hasThumbnail?: boolean;
90
+ }
91
+ /**
92
+ * Studio configuration
93
+ */
94
+ interface StudioConfig {
95
+ r2AccountId?: string;
96
+ r2AccessKeyId?: string;
97
+ r2SecretAccessKey?: string;
98
+ r2BucketName?: string;
99
+ r2PublicUrl?: string;
100
+ thumbnailSizes?: {
101
+ small: number;
102
+ medium: number;
103
+ large: number;
104
+ };
105
+ }
106
+ /**
107
+ * Helper to derive thumbnail path from original path
108
+ */
109
+ declare function getThumbnailPath(originalPath: string, size: LeanImageSize): string;
110
+ /**
111
+ * Convert verbose StudioMeta to LeanMeta
112
+ */
113
+ declare function toLeanMeta(verbose: StudioMeta): LeanMeta;
114
+ /**
115
+ * Check if meta is in lean format (no 'images' wrapper)
116
+ */
117
+ declare function isLeanMeta(meta: unknown): meta is LeanMeta;
118
+
119
+ export { type CdnStatus as C, type FileItem as F, type ImageEntry as I, type LeanMeta as L, type StudioMeta as S, type ImageSize as a, type LeanImageEntry as b, type LeanImageSize as c, type SizeEntry as d, type StudioConfig as e, getThumbnailPath as g, isLeanMeta as i, toLeanMeta as t };
@@ -0,0 +1,119 @@
1
+ /**
2
+ * Image size variants (verbose format used by handlers)
3
+ */
4
+ type ImageSize = 'small' | 'medium' | 'large' | 'full';
5
+ /**
6
+ * Image size variants (lean format for consumers)
7
+ */
8
+ type LeanImageSize = 'sm' | 'md' | 'lg';
9
+ /**
10
+ * Size entry with path and dimensions
11
+ */
12
+ interface SizeEntry {
13
+ path: string;
14
+ width: number;
15
+ height: number;
16
+ }
17
+ /**
18
+ * CDN sync status
19
+ */
20
+ interface CdnStatus {
21
+ synced: boolean;
22
+ baseUrl: string;
23
+ syncedAt: string;
24
+ }
25
+ /**
26
+ * Image entry in meta (verbose format - used by Studio handlers)
27
+ */
28
+ interface ImageEntry {
29
+ original: {
30
+ path: string;
31
+ width: number;
32
+ height: number;
33
+ fileSize: number;
34
+ };
35
+ sizes: {
36
+ full: SizeEntry;
37
+ large: SizeEntry;
38
+ medium: SizeEntry;
39
+ small: SizeEntry;
40
+ [key: string]: SizeEntry;
41
+ };
42
+ blurhash: string;
43
+ dominantColor: string;
44
+ cdn: CdnStatus | null;
45
+ }
46
+ /**
47
+ * Studio meta schema (verbose format - used by Studio handlers)
48
+ */
49
+ interface StudioMeta {
50
+ $schema: string;
51
+ version: number;
52
+ generatedAt: string;
53
+ images: Record<string, ImageEntry>;
54
+ }
55
+ /**
56
+ * Lean image entry - minimal metadata for consumers
57
+ * ~80 bytes per image vs ~500 bytes in verbose format
58
+ */
59
+ interface LeanImageEntry {
60
+ /** Original width */
61
+ w: number;
62
+ /** Original height */
63
+ h: number;
64
+ /** Blurhash for placeholder */
65
+ blur: string;
66
+ /** Synced to CDN (present and 1 if synced, omit if not) */
67
+ s?: 1;
68
+ }
69
+ /**
70
+ * Lean meta schema - flat structure with path as key
71
+ */
72
+ type LeanMeta = Record<string, LeanImageEntry>;
73
+ /**
74
+ * File/folder item for browser
75
+ */
76
+ interface FileItem {
77
+ name: string;
78
+ path: string;
79
+ type: 'file' | 'folder';
80
+ size?: number;
81
+ dimensions?: {
82
+ width: number;
83
+ height: number;
84
+ };
85
+ cdnSynced?: boolean;
86
+ fileCount?: number;
87
+ totalSize?: number;
88
+ thumbnail?: string;
89
+ hasThumbnail?: boolean;
90
+ }
91
+ /**
92
+ * Studio configuration
93
+ */
94
+ interface StudioConfig {
95
+ r2AccountId?: string;
96
+ r2AccessKeyId?: string;
97
+ r2SecretAccessKey?: string;
98
+ r2BucketName?: string;
99
+ r2PublicUrl?: string;
100
+ thumbnailSizes?: {
101
+ small: number;
102
+ medium: number;
103
+ large: number;
104
+ };
105
+ }
106
+ /**
107
+ * Helper to derive thumbnail path from original path
108
+ */
109
+ declare function getThumbnailPath(originalPath: string, size: LeanImageSize): string;
110
+ /**
111
+ * Convert verbose StudioMeta to LeanMeta
112
+ */
113
+ declare function toLeanMeta(verbose: StudioMeta): LeanMeta;
114
+ /**
115
+ * Check if meta is in lean format (no 'images' wrapper)
116
+ */
117
+ declare function isLeanMeta(meta: unknown): meta is LeanMeta;
118
+
119
+ export { type CdnStatus as C, type FileItem as F, type ImageEntry as I, type LeanMeta as L, type StudioMeta as S, type ImageSize as a, type LeanImageEntry as b, type LeanImageSize as c, type SizeEntry as d, type StudioConfig as e, getThumbnailPath as g, isLeanMeta as i, toLeanMeta as t };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gallop.software/studio",
3
- "version": "0.1.68",
3
+ "version": "0.1.69",
4
4
  "description": "Media manager for Gallop templates - upload, process, and sync images to CDN",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",
@@ -1,79 +0,0 @@
1
- /**
2
- * Image size variants
3
- */
4
- type ImageSize = 'small' | 'medium' | 'large' | 'full';
5
- /**
6
- * Size entry with path and dimensions
7
- */
8
- interface SizeEntry {
9
- path: string;
10
- width: number;
11
- height: number;
12
- }
13
- /**
14
- * CDN sync status
15
- */
16
- interface CdnStatus {
17
- synced: boolean;
18
- baseUrl: string;
19
- syncedAt: string;
20
- }
21
- /**
22
- * Image entry in meta
23
- */
24
- interface ImageEntry {
25
- original: {
26
- path: string;
27
- width: number;
28
- height: number;
29
- fileSize: number;
30
- };
31
- sizes: Record<ImageSize, SizeEntry>;
32
- blurhash: string;
33
- dominantColor: string;
34
- cdn: CdnStatus | null;
35
- }
36
- /**
37
- * Studio meta schema
38
- */
39
- interface StudioMeta {
40
- $schema: string;
41
- version: number;
42
- generatedAt: string;
43
- images: Record<string, ImageEntry>;
44
- }
45
- /**
46
- * File/folder item for browser
47
- */
48
- interface FileItem {
49
- name: string;
50
- path: string;
51
- type: 'file' | 'folder';
52
- size?: number;
53
- dimensions?: {
54
- width: number;
55
- height: number;
56
- };
57
- cdnSynced?: boolean;
58
- fileCount?: number;
59
- totalSize?: number;
60
- thumbnail?: string;
61
- hasThumbnail?: boolean;
62
- }
63
- /**
64
- * Studio configuration
65
- */
66
- interface StudioConfig {
67
- r2AccountId?: string;
68
- r2AccessKeyId?: string;
69
- r2SecretAccessKey?: string;
70
- r2BucketName?: string;
71
- r2PublicUrl?: string;
72
- thumbnailSizes?: {
73
- small: number;
74
- medium: number;
75
- large: number;
76
- };
77
- }
78
-
79
- export type { CdnStatus as C, FileItem as F, ImageSize as I, SizeEntry as S, ImageEntry as a, StudioMeta as b, StudioConfig as c };
@@ -1,79 +0,0 @@
1
- /**
2
- * Image size variants
3
- */
4
- type ImageSize = 'small' | 'medium' | 'large' | 'full';
5
- /**
6
- * Size entry with path and dimensions
7
- */
8
- interface SizeEntry {
9
- path: string;
10
- width: number;
11
- height: number;
12
- }
13
- /**
14
- * CDN sync status
15
- */
16
- interface CdnStatus {
17
- synced: boolean;
18
- baseUrl: string;
19
- syncedAt: string;
20
- }
21
- /**
22
- * Image entry in meta
23
- */
24
- interface ImageEntry {
25
- original: {
26
- path: string;
27
- width: number;
28
- height: number;
29
- fileSize: number;
30
- };
31
- sizes: Record<ImageSize, SizeEntry>;
32
- blurhash: string;
33
- dominantColor: string;
34
- cdn: CdnStatus | null;
35
- }
36
- /**
37
- * Studio meta schema
38
- */
39
- interface StudioMeta {
40
- $schema: string;
41
- version: number;
42
- generatedAt: string;
43
- images: Record<string, ImageEntry>;
44
- }
45
- /**
46
- * File/folder item for browser
47
- */
48
- interface FileItem {
49
- name: string;
50
- path: string;
51
- type: 'file' | 'folder';
52
- size?: number;
53
- dimensions?: {
54
- width: number;
55
- height: number;
56
- };
57
- cdnSynced?: boolean;
58
- fileCount?: number;
59
- totalSize?: number;
60
- thumbnail?: string;
61
- hasThumbnail?: boolean;
62
- }
63
- /**
64
- * Studio configuration
65
- */
66
- interface StudioConfig {
67
- r2AccountId?: string;
68
- r2AccessKeyId?: string;
69
- r2SecretAccessKey?: string;
70
- r2BucketName?: string;
71
- r2PublicUrl?: string;
72
- thumbnailSizes?: {
73
- small: number;
74
- medium: number;
75
- large: number;
76
- };
77
- }
78
-
79
- export type { CdnStatus as C, FileItem as F, ImageSize as I, SizeEntry as S, ImageEntry as a, StudioMeta as b, StudioConfig as c };