@gallop.software/studio 0.1.69 → 0.1.70
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/handlers.d.mts +1 -1
- package/dist/handlers.d.ts +1 -1
- package/dist/handlers.js +19 -4
- package/dist/handlers.js.map +1 -1
- package/dist/handlers.mjs +17 -2
- package/dist/handlers.mjs.map +1 -1
- package/dist/index.d.mts +12 -17
- package/dist/index.d.ts +12 -17
- package/dist/index.js +18 -90
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +18 -90
- package/dist/index.mjs.map +1 -1
- package/dist/types-1m_7EjJU.d.mts +79 -0
- package/dist/types-1m_7EjJU.d.ts +79 -0
- package/package.json +1 -1
- package/dist/types-CNVLjvIw.d.mts +0 -119
- package/dist/types-CNVLjvIw.d.ts +0 -119
package/dist/index.js
CHANGED
|
@@ -175,104 +175,35 @@ 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
|
-
|
|
205
178
|
// src/lib/meta.ts
|
|
206
|
-
var
|
|
207
|
-
var _leanMeta = {};
|
|
208
|
-
var meta = {
|
|
179
|
+
var _meta = {
|
|
209
180
|
$schema: "https://gallop.software/schemas/studio-meta.json",
|
|
210
181
|
version: 1,
|
|
211
182
|
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
212
183
|
images: {}
|
|
213
184
|
};
|
|
185
|
+
var meta = _meta;
|
|
214
186
|
function initializeMeta(data) {
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
_verboseMeta = null;
|
|
218
|
-
} else {
|
|
219
|
-
_verboseMeta = data;
|
|
220
|
-
_leanMeta = toLeanMeta(data);
|
|
221
|
-
Object.assign(meta, data);
|
|
222
|
-
}
|
|
187
|
+
_meta = data;
|
|
188
|
+
Object.assign(meta, data);
|
|
223
189
|
}
|
|
224
|
-
function getImageUrl(imageKey, size) {
|
|
225
|
-
const
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
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;
|
|
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}`;
|
|
255
197
|
}
|
|
256
|
-
return
|
|
198
|
+
return sizeData.path;
|
|
257
199
|
}
|
|
258
200
|
function getStudioMeta(imageKey) {
|
|
259
|
-
|
|
260
|
-
return _verboseMeta.images[imageKey];
|
|
261
|
-
}
|
|
262
|
-
return void 0;
|
|
201
|
+
return meta.images[imageKey];
|
|
263
202
|
}
|
|
264
|
-
function getImageSize(imageKey, size) {
|
|
265
|
-
const
|
|
266
|
-
if (
|
|
267
|
-
|
|
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;
|
|
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;
|
|
276
207
|
}
|
|
277
208
|
|
|
278
209
|
|
|
@@ -281,8 +212,5 @@ function getImageSize(imageKey, size) {
|
|
|
281
212
|
|
|
282
213
|
|
|
283
214
|
|
|
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;
|
|
215
|
+
exports.StudioButton = StudioButton; exports.getImageSize = getImageSize; exports.getImageUrl = getImageUrl; exports.getStudioMeta = getStudioMeta; exports.initializeMeta = initializeMeta; exports.meta = meta;
|
|
288
216
|
//# 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/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"]}
|
|
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"]}
|
package/dist/index.mjs
CHANGED
|
@@ -175,114 +175,42 @@ 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
|
-
|
|
205
178
|
// src/lib/meta.ts
|
|
206
|
-
var
|
|
207
|
-
var _leanMeta = {};
|
|
208
|
-
var meta = {
|
|
179
|
+
var _meta = {
|
|
209
180
|
$schema: "https://gallop.software/schemas/studio-meta.json",
|
|
210
181
|
version: 1,
|
|
211
182
|
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
212
183
|
images: {}
|
|
213
184
|
};
|
|
185
|
+
var meta = _meta;
|
|
214
186
|
function initializeMeta(data) {
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
_verboseMeta = null;
|
|
218
|
-
} else {
|
|
219
|
-
_verboseMeta = data;
|
|
220
|
-
_leanMeta = toLeanMeta(data);
|
|
221
|
-
Object.assign(meta, data);
|
|
222
|
-
}
|
|
187
|
+
_meta = data;
|
|
188
|
+
Object.assign(meta, data);
|
|
223
189
|
}
|
|
224
|
-
function getImageUrl(imageKey, size) {
|
|
225
|
-
const
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
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;
|
|
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}`;
|
|
255
197
|
}
|
|
256
|
-
return
|
|
198
|
+
return sizeData.path;
|
|
257
199
|
}
|
|
258
200
|
function getStudioMeta(imageKey) {
|
|
259
|
-
|
|
260
|
-
return _verboseMeta.images[imageKey];
|
|
261
|
-
}
|
|
262
|
-
return void 0;
|
|
201
|
+
return meta.images[imageKey];
|
|
263
202
|
}
|
|
264
|
-
function getImageSize(imageKey, size) {
|
|
265
|
-
const
|
|
266
|
-
if (
|
|
267
|
-
|
|
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;
|
|
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;
|
|
276
207
|
}
|
|
277
208
|
export {
|
|
278
209
|
StudioButton,
|
|
279
210
|
getImageSize,
|
|
280
211
|
getImageUrl,
|
|
281
212
|
getStudioMeta,
|
|
282
|
-
getThumbnailPath,
|
|
283
213
|
initializeMeta,
|
|
284
|
-
|
|
285
|
-
meta,
|
|
286
|
-
toLeanMeta
|
|
214
|
+
meta
|
|
287
215
|
};
|
|
288
216
|
//# sourceMappingURL=index.mjs.map
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
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":"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","names":["meta"]}
|
|
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":"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","names":[]}
|
|
@@ -0,0 +1,79 @@
|
|
|
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 };
|
|
@@ -0,0 +1,79 @@
|
|
|
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 };
|
package/package.json
CHANGED