@drawnagency/primitives 0.1.64 → 0.1.66

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/dist/auth/errors.d.ts +12 -0
  2. package/dist/auth/errors.d.ts.map +1 -1
  3. package/dist/auth/index.d.ts +1 -1
  4. package/dist/auth/index.d.ts.map +1 -1
  5. package/dist/auth/index.js +3 -1
  6. package/dist/{chunk-FECCYOPJ.js → chunk-DQTKWKSK.js} +1 -1
  7. package/dist/{chunk-UNWNT52N.js → chunk-EM5DFHDZ.js} +8 -1
  8. package/dist/{chunk-QYQ3DQHI.js → chunk-VEIDANEU.js} +1 -1
  9. package/dist/{chunk-CBWK3KPV.js → chunk-VU3XR362.js} +4 -0
  10. package/dist/{chunk-VMDKWUMT.js → chunk-XNGG5AGI.js} +74 -17
  11. package/dist/components/primitives/ResolvedMedia.d.ts +3 -1
  12. package/dist/components/primitives/ResolvedMedia.d.ts.map +1 -1
  13. package/dist/components/primitives/index.js +3 -3
  14. package/dist/components/sections/Media/MediaBlock.d.ts.map +1 -1
  15. package/dist/components/sections/brand-guide.js +3 -3
  16. package/dist/components/sections/register-schemas.js +4 -4
  17. package/dist/components/shell/EditorLoading.d.ts +13 -0
  18. package/dist/components/shell/EditorLoading.d.ts.map +1 -0
  19. package/dist/components/shell/EditorShell.d.ts.map +1 -1
  20. package/dist/components/shell/SiteSettingsDisplay.d.ts.map +1 -1
  21. package/dist/hooks/useResolvedMedia.d.ts +2 -0
  22. package/dist/hooks/useResolvedMedia.d.ts.map +1 -1
  23. package/dist/index.js +4 -2
  24. package/dist/media/index.js +1 -1
  25. package/dist/media/resolve.d.ts.map +1 -1
  26. package/package.json +1 -1
  27. package/src/auth/errors.ts +16 -0
  28. package/src/auth/index.ts +1 -1
  29. package/src/components/primitives/ResolvedMedia.tsx +51 -7
  30. package/src/components/sections/Media/MediaBlock.tsx +7 -0
  31. package/src/components/shared/PasswordInput.tsx +3 -3
  32. package/src/components/shell/EditorLoading.tsx +37 -0
  33. package/src/components/shell/EditorShell.tsx +2 -1
  34. package/src/components/shell/SiteSettingsDisplay.tsx +28 -2
  35. package/src/hooks/useResolvedMedia.ts +9 -3
  36. package/src/media/resolve.ts +4 -0
@@ -5,4 +5,16 @@
5
5
  export declare class LastOwnerError extends Error {
6
6
  constructor(message?: string);
7
7
  }
8
+ /**
9
+ * Thrown when an authenticated operation cannot proceed because the underlying
10
+ * Supabase session is missing, expired, or its refresh token is stale/used.
11
+ * This is distinct from a never-authenticated request: the caller passed the
12
+ * middleware gate (e.g. via a still-valid app session cookie) but the Supabase
13
+ * session needed for the operation is dead. API routes should translate this to
14
+ * HTTP 401 so the UI prompts the user to request a fresh link, rather than
15
+ * surfacing an opaque 500.
16
+ */
17
+ export declare class SessionExpiredError extends Error {
18
+ constructor(message?: string);
19
+ }
8
20
  //# sourceMappingURL=errors.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../../src/auth/errors.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,qBAAa,cAAe,SAAQ,KAAK;gBAC3B,OAAO,SAAiC;CAIrD"}
1
+ {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../../src/auth/errors.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,qBAAa,cAAe,SAAQ,KAAK;gBAC3B,OAAO,SAAiC;CAIrD;AAED;;;;;;;;GAQG;AACH,qBAAa,mBAAoB,SAAQ,KAAK;gBAChC,OAAO,SAAyD;CAI7E"}
@@ -3,5 +3,5 @@ export type { UiCapabilities } from "./capabilities";
3
3
  export { deriveUiCapabilities } from "./capabilities";
4
4
  export { requireSessionSecret, isSameOriginRequest, safeNextPath } from "./security";
5
5
  export { signSessionToken, verifySessionToken, signAudienceToken, verifyAudienceToken, setSessionCookie, SESSION_COOKIE, AUDIENCE_COOKIE, SESSION_MAX_AGE_SECONDS, AUDIENCE_MAX_AGE_SECONDS } from "./cookies";
6
- export { LastOwnerError } from "./errors";
6
+ export { LastOwnerError, SessionExpiredError } from "./errors";
7
7
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/auth/index.ts"],"names":[],"mappings":"AAAA,YAAY,EACV,IAAI,EACJ,OAAO,EACP,QAAQ,EACR,QAAQ,EACR,YAAY,EACZ,YAAY,EACZ,iBAAiB,EACjB,mBAAmB,EACnB,YAAY,EACZ,UAAU,EACV,WAAW,GACZ,MAAM,SAAS,CAAC;AAEjB,YAAY,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AACrD,OAAO,EAAE,oBAAoB,EAAE,MAAM,gBAAgB,CAAC;AAEtD,OAAO,EAAE,oBAAoB,EAAE,mBAAmB,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AACrF,OAAO,EAAE,gBAAgB,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,mBAAmB,EAAE,gBAAgB,EAAE,cAAc,EAAE,eAAe,EAAE,uBAAuB,EAAE,wBAAwB,EAAE,MAAM,WAAW,CAAC;AAC/M,OAAO,EAAE,cAAc,EAAE,MAAM,UAAU,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/auth/index.ts"],"names":[],"mappings":"AAAA,YAAY,EACV,IAAI,EACJ,OAAO,EACP,QAAQ,EACR,QAAQ,EACR,YAAY,EACZ,YAAY,EACZ,iBAAiB,EACjB,mBAAmB,EACnB,YAAY,EACZ,UAAU,EACV,WAAW,GACZ,MAAM,SAAS,CAAC;AAEjB,YAAY,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AACrD,OAAO,EAAE,oBAAoB,EAAE,MAAM,gBAAgB,CAAC;AAEtD,OAAO,EAAE,oBAAoB,EAAE,mBAAmB,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AACrF,OAAO,EAAE,gBAAgB,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,mBAAmB,EAAE,gBAAgB,EAAE,cAAc,EAAE,eAAe,EAAE,uBAAuB,EAAE,wBAAwB,EAAE,MAAM,WAAW,CAAC;AAC/M,OAAO,EAAE,cAAc,EAAE,mBAAmB,EAAE,MAAM,UAAU,CAAC"}
@@ -4,13 +4,14 @@ import {
4
4
  LastOwnerError,
5
5
  SESSION_COOKIE,
6
6
  SESSION_MAX_AGE_SECONDS,
7
+ SessionExpiredError,
7
8
  deriveUiCapabilities,
8
9
  setSessionCookie,
9
10
  signAudienceToken,
10
11
  signSessionToken,
11
12
  verifyAudienceToken,
12
13
  verifySessionToken
13
- } from "../chunk-UNWNT52N.js";
14
+ } from "../chunk-EM5DFHDZ.js";
14
15
  import {
15
16
  isSameOriginRequest,
16
17
  requireSessionSecret,
@@ -24,6 +25,7 @@ export {
24
25
  LastOwnerError,
25
26
  SESSION_COOKIE,
26
27
  SESSION_MAX_AGE_SECONDS,
28
+ SessionExpiredError,
27
29
  deriveUiCapabilities,
28
30
  isSameOriginRequest,
29
31
  requireSessionSecret,
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  EditablePlainText
3
- } from "./chunk-VMDKWUMT.js";
3
+ } from "./chunk-XNGG5AGI.js";
4
4
  import {
5
5
  usePages
6
6
  } from "./chunk-4E66N6UV.js";
@@ -74,6 +74,12 @@ var LastOwnerError = class extends Error {
74
74
  this.name = "LastOwnerError";
75
75
  }
76
76
  };
77
+ var SessionExpiredError = class extends Error {
78
+ constructor(message = "Your session has expired. Please request a new link.") {
79
+ super(message);
80
+ this.name = "SessionExpiredError";
81
+ }
82
+ };
77
83
 
78
84
  export {
79
85
  deriveUiCapabilities,
@@ -86,5 +92,6 @@ export {
86
92
  signAudienceToken,
87
93
  verifyAudienceToken,
88
94
  setSessionCookie,
89
- LastOwnerError
95
+ LastOwnerError,
96
+ SessionExpiredError
90
97
  };
@@ -9,7 +9,7 @@ import {
9
9
  MediaBlock,
10
10
  containerGridClass,
11
11
  useEditableCollection
12
- } from "./chunk-VMDKWUMT.js";
12
+ } from "./chunk-XNGG5AGI.js";
13
13
  import {
14
14
  AddIcon,
15
15
  DeleteIcon,
@@ -200,11 +200,15 @@ function resolveManifestReferences(content, manifest, sizes, provider) {
200
200
  if ("srcSet" in resolved && resolved.srcSet) {
201
201
  patch.srcset = resolved.srcSet;
202
202
  }
203
+ patch.width = resolved.width;
204
+ patch.height = resolved.height;
203
205
  } else if (resolved && resolved.tag === "video") {
204
206
  patch.src = resolved.src;
205
207
  if (resolved.poster) {
206
208
  patch.poster = resolved.poster;
207
209
  }
210
+ patch.width = resolved.width;
211
+ patch.height = resolved.height;
208
212
  }
209
213
  return { ...obj, ...patch };
210
214
  }
@@ -7,7 +7,7 @@ import {
7
7
  } from "./chunk-I5737FJY.js";
8
8
  import {
9
9
  getMediaProvider
10
- } from "./chunk-CBWK3KPV.js";
10
+ } from "./chunk-VU3XR362.js";
11
11
  import {
12
12
  cn
13
13
  } from "./chunk-7EOSDFS3.js";
@@ -126,6 +126,7 @@ function EditablePlainText({
126
126
  }
127
127
 
128
128
  // src/components/primitives/ResolvedMedia.tsx
129
+ import { useState as useState2 } from "react";
129
130
  import { ImageIcon } from "lucide-react";
130
131
 
131
132
  // src/hooks/useResolvedMedia.ts
@@ -152,9 +153,11 @@ function useResolvedMedia(imageId) {
152
153
  const item = manifestItem ?? pendingItem;
153
154
  const alt = item?.alt ?? "";
154
155
  const kind = item?.kind ?? "image";
156
+ const width = item?.width;
157
+ const height = item?.height;
155
158
  const localUrl = pendingLocalUrls[imageId];
156
159
  if (localUrl) {
157
- return { src: localUrl, srcset: void 0, poster: void 0, alt, kind };
160
+ return { src: localUrl, srcset: void 0, poster: void 0, alt, kind, width, height };
158
161
  }
159
162
  if (manifestItem) {
160
163
  const provider = getMediaProvider();
@@ -165,24 +168,28 @@ function useResolvedMedia(imageId) {
165
168
  srcset: "srcSet" in resolved ? resolved.srcSet : void 0,
166
169
  poster: void 0,
167
170
  alt,
168
- kind
171
+ kind,
172
+ width: resolved.width,
173
+ height: resolved.height
169
174
  };
170
175
  }
171
176
  if (resolved && resolved.tag === "video") {
172
- return { src: resolved.src, srcset: void 0, poster: resolved.poster, alt, kind };
177
+ return { src: resolved.src, srcset: void 0, poster: resolved.poster, alt, kind, width: resolved.width, height: resolved.height };
173
178
  }
174
179
  }
175
- return { src: void 0, srcset: void 0, poster: void 0, alt, kind };
180
+ return { src: void 0, srcset: void 0, poster: void 0, alt, kind, width, height };
176
181
  }, [imageId, ctx]);
177
182
  }
178
183
 
179
184
  // src/components/primitives/ResolvedMedia.tsx
180
- import { jsx, jsxs } from "react/jsx-runtime";
185
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
181
186
  function ResolvedMedia({
182
187
  imageId,
183
188
  src: propSrc,
184
189
  srcset: propSrcset,
185
190
  alt: propAlt,
191
+ width: propWidth,
192
+ height: propHeight,
186
193
  className,
187
194
  imgClassName,
188
195
  invertFrom
@@ -193,22 +200,66 @@ function ResolvedMedia({
193
200
  const poster = resolved.poster;
194
201
  const alt = propAlt ?? resolved.alt;
195
202
  const kind = resolved.kind;
203
+ const width = propWidth ?? resolved.width;
204
+ const height = propHeight ?? resolved.height;
205
+ const [loaded, setLoaded] = useState2(false);
196
206
  const invertClass = invertFrom === "light" ? "invert dark:invert-0" : invertFrom === "dark" ? "dark:invert" : void 0;
197
- const mediaClass = cn("h-full w-full", imgClassName, invertClass);
198
- return /* @__PURE__ */ jsx("div", { className, children: src ? kind === "video" ? /* @__PURE__ */ jsx("video", { src, poster, muted: true, playsInline: true, loop: true, autoPlay: true, className: mediaClass }) : /* @__PURE__ */ jsx("img", { src, srcSet: srcset, alt, className: mediaClass }) : /* @__PURE__ */ jsxs("div", { className: "flex aspect-video h-full w-full flex-col items-center justify-center gap-1 rounded bg-base-accent text-base-contrast-light/50", children: [
207
+ const mediaClass = cn("relative h-full w-full", imgClassName, invertClass);
208
+ return /* @__PURE__ */ jsx("div", { className: cn("relative", className), children: src ? /* @__PURE__ */ jsxs(Fragment, { children: [
209
+ /* @__PURE__ */ jsx(
210
+ "div",
211
+ {
212
+ "aria-hidden": "true",
213
+ "data-testid": "media-placeholder",
214
+ className: cn(
215
+ "pointer-events-none absolute inset-0 bg-base-accent transition-opacity duration-300",
216
+ loaded ? "opacity-0" : "opacity-100"
217
+ )
218
+ }
219
+ ),
220
+ kind === "video" ? /* @__PURE__ */ jsx(
221
+ "video",
222
+ {
223
+ src,
224
+ poster,
225
+ width,
226
+ height,
227
+ muted: true,
228
+ playsInline: true,
229
+ loop: true,
230
+ autoPlay: true,
231
+ className: mediaClass,
232
+ onLoadedData: () => setLoaded(true)
233
+ }
234
+ ) : /* @__PURE__ */ jsx(
235
+ "img",
236
+ {
237
+ src,
238
+ srcSet: srcset,
239
+ alt,
240
+ width,
241
+ height,
242
+ loading: "lazy",
243
+ decoding: "async",
244
+ className: mediaClass,
245
+ onLoad: () => setLoaded(true),
246
+ onError: () => setLoaded(true)
247
+ }
248
+ )
249
+ ] }) : /* @__PURE__ */ jsxs("div", { className: "flex aspect-video h-full w-full flex-col items-center justify-center gap-1 rounded bg-base-accent text-base-contrast-light/50", children: [
199
250
  /* @__PURE__ */ jsx(ImageIcon, { size: 24 }),
200
251
  /* @__PURE__ */ jsx("span", { className: "text-sm", children: "No Image" })
201
252
  ] }) });
202
253
  }
203
254
 
204
255
  // src/components/primitives/ImageDropZone.tsx
205
- import { useState as useState2 } from "react";
206
- import { Fragment, jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
256
+ import { useState as useState3 } from "react";
257
+ import { Fragment as Fragment2, jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
207
258
  var ACCEPTED = /^(image|video)\//;
208
259
  function ImageDropZone({ onImageSelected, className, children }) {
209
260
  const mediaLibrary = useMediaLibrary();
210
- const [dragging, setDragging] = useState2(false);
211
- if (!mediaLibrary) return /* @__PURE__ */ jsx2(Fragment, { children });
261
+ const [dragging, setDragging] = useState3(false);
262
+ if (!mediaLibrary) return /* @__PURE__ */ jsx2(Fragment2, { children });
212
263
  const maxFileSize = mediaLibrary.siteConfig?.media.maxFileSize;
213
264
  const hasFiles = (e) => e.dataTransfer.types.includes("Files");
214
265
  const handleDragOver = (e) => {
@@ -265,6 +316,9 @@ function MediaBlock({ refValue, options, link, isEditMode, onRefChange }) {
265
316
  const captionStr = refValue.caption ? Array.isArray(refValue.caption) ? refValue.caption.join("\n") : refValue.caption : "";
266
317
  const replace = onRefChange ? (imageId) => onRefChange({ ...refValue, imageId }) : void 0;
267
318
  const baked = refValue;
319
+ const bakedWidth = typeof baked.width === "number" ? baked.width : void 0;
320
+ const bakedHeight = typeof baked.height === "number" ? baked.height : void 0;
321
+ const aspectRatio = bakedWidth && bakedHeight && !options.square ? `${bakedWidth} / ${bakedHeight}` : void 0;
268
322
  const resolved = /* @__PURE__ */ jsx3(
269
323
  ResolvedMedia,
270
324
  {
@@ -272,6 +326,8 @@ function MediaBlock({ refValue, options, link, isEditMode, onRefChange }) {
272
326
  src: baked.src,
273
327
  srcset: baked.srcset,
274
328
  alt: baked.alt,
329
+ width: bakedWidth,
330
+ height: bakedHeight,
275
331
  className: "h-full w-full",
276
332
  imgClassName: fitClass,
277
333
  invertFrom: refValue.invertFrom
@@ -293,6 +349,7 @@ function MediaBlock({ refValue, options, link, isEditMode, onRefChange }) {
293
349
  options.square && "aspect-square",
294
350
  onImageClick && "cursor-pointer"
295
351
  ),
352
+ style: aspectRatio ? { aspectRatio } : void 0,
296
353
  onClick: onImageClick,
297
354
  onPointerDown: onImageClick ? (e) => e.stopPropagation() : void 0,
298
355
  children: media
@@ -313,7 +370,7 @@ function MediaBlock({ refValue, options, link, isEditMode, onRefChange }) {
313
370
  }
314
371
 
315
372
  // src/components/primitives/EditableGrid.tsx
316
- import { useRef as useRef4, useEffect as useEffect4, useState as useState4 } from "react";
373
+ import { useRef as useRef4, useEffect as useEffect4, useState as useState5 } from "react";
317
374
  import { ImageIcon as ImageIcon2 } from "lucide-react";
318
375
 
319
376
  // src/lib/container-grid.ts
@@ -344,7 +401,7 @@ var spacerHiddenClass = {
344
401
  };
345
402
 
346
403
  // src/components/primitives/useEditableCollection.ts
347
- import { useState as useState3, useCallback as useCallback3, useRef as useRef3, useEffect as useEffect3 } from "react";
404
+ import { useState as useState4, useCallback as useCallback3, useRef as useRef3, useEffect as useEffect3 } from "react";
348
405
  var nextId = 0;
349
406
  function uid() {
350
407
  return typeof crypto !== "undefined" && typeof crypto.randomUUID === "function" ? crypto.randomUUID() : `_id_${nextId++}_${Date.now()}`;
@@ -357,7 +414,7 @@ function useEditableCollection({
357
414
  onChange,
358
415
  createItem
359
416
  }) {
360
- const [wrappedItems, setWrappedItems] = useState3(
417
+ const [wrappedItems, setWrappedItems] = useState4(
361
418
  () => wrapItems(items)
362
419
  );
363
420
  const prevItemsRef = useRef3(items);
@@ -437,8 +494,8 @@ function EditableGrid({
437
494
  onChange,
438
495
  createItem
439
496
  });
440
- const [dragState, setDragState] = useState4({ sourceId: null, closestEdge: null });
441
- const [isDragging, setIsDragging] = useState4(false);
497
+ const [dragState, setDragState] = useState5({ sourceId: null, closestEdge: null });
498
+ const [isDragging, setIsDragging] = useState5(false);
442
499
  useEffect4(() => {
443
500
  let cleanup;
444
501
  let cancelled = false;
@@ -3,10 +3,12 @@ interface ResolvedMediaProps {
3
3
  src?: string;
4
4
  srcset?: string;
5
5
  alt?: string;
6
+ width?: number;
7
+ height?: number;
6
8
  className?: string;
7
9
  imgClassName?: string;
8
10
  invertFrom?: string;
9
11
  }
10
- export declare function ResolvedMedia({ imageId, src: propSrc, srcset: propSrcset, alt: propAlt, className, imgClassName, invertFrom, }: ResolvedMediaProps): import("react/jsx-runtime").JSX.Element;
12
+ export declare function ResolvedMedia({ imageId, src: propSrc, srcset: propSrcset, alt: propAlt, width: propWidth, height: propHeight, className, imgClassName, invertFrom, }: ResolvedMediaProps): import("react/jsx-runtime").JSX.Element;
11
13
  export {};
12
14
  //# sourceMappingURL=ResolvedMedia.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"ResolvedMedia.d.ts","sourceRoot":"","sources":["../../../src/components/primitives/ResolvedMedia.tsx"],"names":[],"mappings":"AAIA,UAAU,kBAAkB;IAC1B,OAAO,EAAE,MAAM,GAAG,SAAS,CAAC;IAC5B,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,wBAAgB,aAAa,CAAC,EAC5B,OAAO,EACP,GAAG,EAAE,OAAO,EACZ,MAAM,EAAE,UAAU,EAClB,GAAG,EAAE,OAAO,EACZ,SAAS,EACT,YAAY,EACZ,UAAU,GACX,EAAE,kBAAkB,2CA+BpB"}
1
+ {"version":3,"file":"ResolvedMedia.d.ts","sourceRoot":"","sources":["../../../src/components/primitives/ResolvedMedia.tsx"],"names":[],"mappings":"AAKA,UAAU,kBAAkB;IAC1B,OAAO,EAAE,MAAM,GAAG,SAAS,CAAC;IAC5B,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,wBAAgB,aAAa,CAAC,EAC5B,OAAO,EACP,GAAG,EAAE,OAAO,EACZ,MAAM,EAAE,UAAU,EAClB,GAAG,EAAE,OAAO,EACZ,KAAK,EAAE,SAAS,EAChB,MAAM,EAAE,UAAU,EAClB,SAAS,EACT,YAAY,EACZ,UAAU,GACX,EAAE,kBAAkB,2CAsEpB"}
@@ -2,7 +2,7 @@ import {
2
2
  EditableRichText,
3
3
  LinkPopover,
4
4
  defineHeadingSection
5
- } from "../../chunk-FECCYOPJ.js";
5
+ } from "../../chunk-DQTKWKSK.js";
6
6
  import {
7
7
  EditableGrid,
8
8
  EditablePlainText,
@@ -10,14 +10,14 @@ import {
10
10
  MediaBlock,
11
11
  ResolvedMedia,
12
12
  useEditableCollection
13
- } from "../../chunk-VMDKWUMT.js";
13
+ } from "../../chunk-XNGG5AGI.js";
14
14
  import "../../chunk-4E66N6UV.js";
15
15
  import {
16
16
  DeleteIcon,
17
17
  DragHandle,
18
18
  IconButton
19
19
  } from "../../chunk-I5737FJY.js";
20
- import "../../chunk-CBWK3KPV.js";
20
+ import "../../chunk-VU3XR362.js";
21
21
  import "../../chunk-53DKVAB6.js";
22
22
  import "../../chunk-ZGFAYZWB.js";
23
23
  import {
@@ -1 +1 @@
1
- {"version":3,"file":"MediaBlock.d.ts","sourceRoot":"","sources":["../../../../src/components/sections/Media/MediaBlock.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,yBAAyB,CAAC;AACpE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAOvD,MAAM,WAAW,eAAe;IAC9B,QAAQ,EAAE,oBAAoB,CAAC;IAC/B,OAAO,EAAE;QAAE,MAAM,CAAC,EAAE,OAAO,CAAC;QAAC,WAAW,CAAC,EAAE,OAAO,CAAC;QAAC,MAAM,CAAC,EAAE,OAAO,CAAC;QAAC,SAAS,CAAC,EAAE,OAAO,GAAG,SAAS,CAAA;KAAE,CAAC;IACxG,4EAA4E;IAC5E,IAAI,CAAC,EAAE,SAAS,CAAC;IACjB,UAAU,EAAE,OAAO,CAAC;IACpB,WAAW,CAAC,EAAE,CAAC,GAAG,EAAE,oBAAoB,KAAK,IAAI,CAAC;CACnD;AAED,wBAAgB,UAAU,CAAC,EAAE,QAAQ,EAAE,OAAO,EAAE,IAAI,EAAE,UAAU,EAAE,WAAW,EAAE,EAAE,eAAe,2CAoF/F"}
1
+ {"version":3,"file":"MediaBlock.d.ts","sourceRoot":"","sources":["../../../../src/components/sections/Media/MediaBlock.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,yBAAyB,CAAC;AACpE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAOvD,MAAM,WAAW,eAAe;IAC9B,QAAQ,EAAE,oBAAoB,CAAC;IAC/B,OAAO,EAAE;QAAE,MAAM,CAAC,EAAE,OAAO,CAAC;QAAC,WAAW,CAAC,EAAE,OAAO,CAAC;QAAC,MAAM,CAAC,EAAE,OAAO,CAAC;QAAC,SAAS,CAAC,EAAE,OAAO,GAAG,SAAS,CAAA;KAAE,CAAC;IACxG,4EAA4E;IAC5E,IAAI,CAAC,EAAE,SAAS,CAAC;IACjB,UAAU,EAAE,OAAO,CAAC;IACpB,WAAW,CAAC,EAAE,CAAC,GAAG,EAAE,oBAAoB,KAAK,IAAI,CAAC;CACnD;AAED,wBAAgB,UAAU,CAAC,EAAE,QAAQ,EAAE,OAAO,EAAE,IAAI,EAAE,UAAU,EAAE,WAAW,EAAE,EAAE,eAAe,2CA2F/F"}
@@ -2,11 +2,11 @@ import {
2
2
  ColorItemSchema,
3
3
  ColorSpaceSchema,
4
4
  brandGuideSectionDefs
5
- } from "../../chunk-QYQ3DQHI.js";
5
+ } from "../../chunk-VEIDANEU.js";
6
6
  import "../../chunk-5THS4K5D.js";
7
- import "../../chunk-VMDKWUMT.js";
7
+ import "../../chunk-XNGG5AGI.js";
8
8
  import "../../chunk-I5737FJY.js";
9
- import "../../chunk-CBWK3KPV.js";
9
+ import "../../chunk-VU3XR362.js";
10
10
  import "../../chunk-3DCG4DNU.js";
11
11
  import "../../chunk-WDSVHKAH.js";
12
12
  import "../../chunk-ZGFAYZWB.js";
@@ -3,7 +3,7 @@ import {
3
3
  pluralize,
4
4
  stripHtmlToPlainText,
5
5
  truncate
6
- } from "../../chunk-QYQ3DQHI.js";
6
+ } from "../../chunk-VEIDANEU.js";
7
7
  import {
8
8
  DeleteButton,
9
9
  DragHandle,
@@ -20,7 +20,7 @@ import "../../chunk-5THS4K5D.js";
20
20
  import {
21
21
  EditableRichText,
22
22
  defineHeadingSection
23
- } from "../../chunk-FECCYOPJ.js";
23
+ } from "../../chunk-DQTKWKSK.js";
24
24
  import {
25
25
  EditablePlainText,
26
26
  MAX_CONTAINER_COLUMNS,
@@ -28,10 +28,10 @@ import {
28
28
  colSpanClass,
29
29
  containerGridClass,
30
30
  spacerHiddenClass
31
- } from "../../chunk-VMDKWUMT.js";
31
+ } from "../../chunk-XNGG5AGI.js";
32
32
  import "../../chunk-4E66N6UV.js";
33
33
  import "../../chunk-I5737FJY.js";
34
- import "../../chunk-CBWK3KPV.js";
34
+ import "../../chunk-VU3XR362.js";
35
35
  import {
36
36
  DEFAULT_LINK,
37
37
  LinkValueSchema
@@ -0,0 +1,13 @@
1
+ interface EditorLoadingProps {
2
+ /** Delay before the spinner appears, to avoid a flash on instant cache hits. */
3
+ delayMs?: number;
4
+ }
5
+ /**
6
+ * Full-screen loading state shown while EditorShell hydrates and loads content.
7
+ * Keeps the existing aria-busy contract on the wrapper at all times; the visible
8
+ * spinner is delayed so a fast Dexie cache hit doesn't flash it. SSR-safe: the
9
+ * initial render shows the wrapper with no spinner, matching the server output.
10
+ */
11
+ export declare function EditorLoading({ delayMs }: EditorLoadingProps): import("react/jsx-runtime").JSX.Element;
12
+ export {};
13
+ //# sourceMappingURL=EditorLoading.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"EditorLoading.d.ts","sourceRoot":"","sources":["../../../src/components/shell/EditorLoading.tsx"],"names":[],"mappings":"AAGA,UAAU,kBAAkB;IAC1B,gFAAgF;IAChF,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,EAAE,OAAa,EAAE,EAAE,kBAAkB,2CAsBlE"}
@@ -1 +1 @@
1
- {"version":3,"file":"EditorShell.d.ts","sourceRoot":"","sources":["../../../src/components/shell/EditorShell.tsx"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAsEjD,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAQxD,UAAU,KAAK;IACb,OAAO,EAAE,MAAM,CAAC;IAIhB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,QAAQ,EAAE,CAAC;IACtB,YAAY,EAAE;QACZ,KAAK,EAAE,OAAO,CAAC;QACf,aAAa,EAAE,OAAO,CAAC;QACvB,YAAY,EAAE,OAAO,CAAC;QACtB,cAAc,EAAE,OAAO,CAAC;QACxB,kBAAkB,EAAE,OAAO,CAAC;QAC5B,cAAc,EAAE,OAAO,CAAC;KACzB,CAAC;IACF,WAAW,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,OAAO,GAAG,QAAQ,CAAA;KAAE,GAAG,IAAI,CAAC;CACjE;AAED,MAAM,CAAC,OAAO,UAAU,WAAW,CAAC,EAClC,OAAO,EACP,YAAY,EACZ,MAAM,EACN,SAAS,EAAE,gBAAgB,EAC3B,YAAY,EACZ,WAAW,GACZ,EAAE,KAAK,2CAo6BP"}
1
+ {"version":3,"file":"EditorShell.d.ts","sourceRoot":"","sources":["../../../src/components/shell/EditorShell.tsx"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAuEjD,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAQxD,UAAU,KAAK;IACb,OAAO,EAAE,MAAM,CAAC;IAIhB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,QAAQ,EAAE,CAAC;IACtB,YAAY,EAAE;QACZ,KAAK,EAAE,OAAO,CAAC;QACf,aAAa,EAAE,OAAO,CAAC;QACvB,YAAY,EAAE,OAAO,CAAC;QACtB,cAAc,EAAE,OAAO,CAAC;QACxB,kBAAkB,EAAE,OAAO,CAAC;QAC5B,cAAc,EAAE,OAAO,CAAC;KACzB,CAAC;IACF,WAAW,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,OAAO,GAAG,QAAQ,CAAA;KAAE,GAAG,IAAI,CAAC;CACjE;AAED,MAAM,CAAC,OAAO,UAAU,WAAW,CAAC,EAClC,OAAO,EACP,YAAY,EACZ,MAAM,EACN,SAAS,EAAE,gBAAgB,EAC3B,YAAY,EACZ,WAAW,GACZ,EAAE,KAAK,2CAo6BP"}
@@ -1 +1 @@
1
- {"version":3,"file":"SiteSettingsDisplay.d.ts","sourceRoot":"","sources":["../../../src/components/shell/SiteSettingsDisplay.tsx"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,2BAA2B,CAAC;AAE5D,UAAU,KAAK;IACb,UAAU,EAAE,UAAU,CAAC;IACvB,QAAQ,EAAE,CAAC,MAAM,EAAE,UAAU,KAAK,IAAI,CAAC;CACxC;AAWD,wBAAgB,mBAAmB,CAAC,EAAE,UAAU,EAAE,QAAQ,EAAE,EAAE,KAAK,2CAuIlE"}
1
+ {"version":3,"file":"SiteSettingsDisplay.d.ts","sourceRoot":"","sources":["../../../src/components/shell/SiteSettingsDisplay.tsx"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,2BAA2B,CAAC;AAE5D,UAAU,KAAK;IACb,UAAU,EAAE,UAAU,CAAC;IACvB,QAAQ,EAAE,CAAC,MAAM,EAAE,UAAU,KAAK,IAAI,CAAC;CACxC;AAWD,wBAAgB,mBAAmB,CAAC,EAAE,UAAU,EAAE,QAAQ,EAAE,EAAE,KAAK,2CAiKlE"}
@@ -4,6 +4,8 @@ export interface ResolvedMediaResult {
4
4
  poster: string | undefined;
5
5
  alt: string;
6
6
  kind: "image" | "animated" | "video";
7
+ width?: number;
8
+ height?: number;
7
9
  }
8
10
  export declare function useResolvedMedia(imageId: string | undefined): ResolvedMediaResult;
9
11
  //# sourceMappingURL=useResolvedMedia.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"useResolvedMedia.d.ts","sourceRoot":"","sources":["../../src/hooks/useResolvedMedia.ts"],"names":[],"mappings":"AAIA,MAAM,WAAW,mBAAmB;IAClC,GAAG,EAAE,MAAM,GAAG,SAAS,CAAC;IACxB,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC;IAC3B,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC;IAC3B,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,OAAO,GAAG,UAAU,GAAG,OAAO,CAAC;CACtC;AAED,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,GAAG,SAAS,GAAG,mBAAmB,CAyCjF"}
1
+ {"version":3,"file":"useResolvedMedia.d.ts","sourceRoot":"","sources":["../../src/hooks/useResolvedMedia.ts"],"names":[],"mappings":"AAIA,MAAM,WAAW,mBAAmB;IAClC,GAAG,EAAE,MAAM,GAAG,SAAS,CAAC;IACxB,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC;IAC3B,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC;IAC3B,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,OAAO,GAAG,UAAU,GAAG,OAAO,CAAC;IACrC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,GAAG,SAAS,GAAG,mBAAmB,CA6CjF"}
package/dist/index.js CHANGED
@@ -13,20 +13,21 @@ import {
13
13
  resolveMedia,
14
14
  sanitizeMediaName,
15
15
  setMediaProvider
16
- } from "./chunk-CBWK3KPV.js";
16
+ } from "./chunk-VU3XR362.js";
17
17
  import {
18
18
  AUDIENCE_COOKIE,
19
19
  AUDIENCE_MAX_AGE_SECONDS,
20
20
  LastOwnerError,
21
21
  SESSION_COOKIE,
22
22
  SESSION_MAX_AGE_SECONDS,
23
+ SessionExpiredError,
23
24
  deriveUiCapabilities,
24
25
  setSessionCookie,
25
26
  signAudienceToken,
26
27
  signSessionToken,
27
28
  verifyAudienceToken,
28
29
  verifySessionToken
29
- } from "./chunk-UNWNT52N.js";
30
+ } from "./chunk-EM5DFHDZ.js";
30
31
  import {
31
32
  AudienceColorSchema,
32
33
  AudienceNameSchema,
@@ -157,6 +158,7 @@ export {
157
158
  SESSION_COOKIE,
158
159
  SESSION_MAX_AGE_SECONDS,
159
160
  SectionMetaSchema,
161
+ SessionExpiredError,
160
162
  SessionSchema,
161
163
  SingleMediaReferenceSchema,
162
164
  SiteConfigSchema,
@@ -13,7 +13,7 @@ import {
13
13
  resolveMedia,
14
14
  sanitizeMediaName,
15
15
  setMediaProvider
16
- } from "../chunk-CBWK3KPV.js";
16
+ } from "../chunk-VU3XR362.js";
17
17
  import "../chunk-ZGFAYZWB.js";
18
18
  import {
19
19
  ImageManifestSchema,
@@ -1 +1 @@
1
- {"version":3,"file":"resolve.d.ts","sourceRoot":"","sources":["../../src/media/resolve.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAC5D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAEhD,wBAAgB,YAAY,CAC1B,IAAI,EAAE;IAAE,OAAO,CAAC,EAAE,MAAM,CAAC;IAAC,GAAG,CAAC,EAAE,MAAM,CAAA;CAAE,EACxC,QAAQ,EAAE,aAAa,EACvB,KAAK,EAAE,MAAM,EAAE,EACf,QAAQ,EAAE,aAAa,GACtB,aAAa,GAAG,IAAI,CAKtB;AAED;;;;;;GAMG;AACH,wBAAgB,yBAAyB,CACvC,OAAO,EAAE,OAAO,EAChB,QAAQ,EAAE,aAAa,EACvB,KAAK,EAAE,MAAM,EAAE,EACf,QAAQ,EAAE,aAAa,GACtB,OAAO,CAsCT"}
1
+ {"version":3,"file":"resolve.d.ts","sourceRoot":"","sources":["../../src/media/resolve.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAC5D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAEhD,wBAAgB,YAAY,CAC1B,IAAI,EAAE;IAAE,OAAO,CAAC,EAAE,MAAM,CAAC;IAAC,GAAG,CAAC,EAAE,MAAM,CAAA;CAAE,EACxC,QAAQ,EAAE,aAAa,EACvB,KAAK,EAAE,MAAM,EAAE,EACf,QAAQ,EAAE,aAAa,GACtB,aAAa,GAAG,IAAI,CAKtB;AAED;;;;;;GAMG;AACH,wBAAgB,yBAAyB,CACvC,OAAO,EAAE,OAAO,EAChB,QAAQ,EAAE,aAAa,EACvB,KAAK,EAAE,MAAM,EAAE,EACf,QAAQ,EAAE,aAAa,GACtB,OAAO,CA0CT"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@drawnagency/primitives",
3
- "version": "0.1.64",
3
+ "version": "0.1.66",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  "./package.json": "./package.json",
@@ -8,3 +8,19 @@ export class LastOwnerError extends Error {
8
8
  this.name = "LastOwnerError";
9
9
  }
10
10
  }
11
+
12
+ /**
13
+ * Thrown when an authenticated operation cannot proceed because the underlying
14
+ * Supabase session is missing, expired, or its refresh token is stale/used.
15
+ * This is distinct from a never-authenticated request: the caller passed the
16
+ * middleware gate (e.g. via a still-valid app session cookie) but the Supabase
17
+ * session needed for the operation is dead. API routes should translate this to
18
+ * HTTP 401 so the UI prompts the user to request a fresh link, rather than
19
+ * surfacing an opaque 500.
20
+ */
21
+ export class SessionExpiredError extends Error {
22
+ constructor(message = "Your session has expired. Please request a new link.") {
23
+ super(message);
24
+ this.name = "SessionExpiredError";
25
+ }
26
+ }
package/src/auth/index.ts CHANGED
@@ -17,4 +17,4 @@ export { deriveUiCapabilities } from "./capabilities";
17
17
 
18
18
  export { requireSessionSecret, isSameOriginRequest, safeNextPath } from "./security";
19
19
  export { signSessionToken, verifySessionToken, signAudienceToken, verifyAudienceToken, setSessionCookie, SESSION_COOKIE, AUDIENCE_COOKIE, SESSION_MAX_AGE_SECONDS, AUDIENCE_MAX_AGE_SECONDS } from "./cookies";
20
- export { LastOwnerError } from "./errors";
20
+ export { LastOwnerError, SessionExpiredError } from "./errors";
@@ -1,3 +1,4 @@
1
+ import { useState } from "react";
1
2
  import { cn } from "../../lib/cn";
2
3
  import { ImageIcon } from "lucide-react";
3
4
  import { useResolvedMedia } from "../../hooks/useResolvedMedia";
@@ -7,6 +8,8 @@ interface ResolvedMediaProps {
7
8
  src?: string;
8
9
  srcset?: string;
9
10
  alt?: string;
11
+ width?: number;
12
+ height?: number;
10
13
  className?: string;
11
14
  imgClassName?: string;
12
15
  invertFrom?: string;
@@ -17,6 +20,8 @@ export function ResolvedMedia({
17
20
  src: propSrc,
18
21
  srcset: propSrcset,
19
22
  alt: propAlt,
23
+ width: propWidth,
24
+ height: propHeight,
20
25
  className,
21
26
  imgClassName,
22
27
  invertFrom,
@@ -27,22 +32,61 @@ export function ResolvedMedia({
27
32
  const poster = resolved.poster;
28
33
  const alt = propAlt ?? resolved.alt;
29
34
  const kind = resolved.kind;
35
+ const width = propWidth ?? resolved.width;
36
+ const height = propHeight ?? resolved.height;
37
+
38
+ // `loaded` fades the placeholder out via onLoad in the hydrated editor. In the
39
+ // zero-JS viewer this stays false (no JS), so the placeholder remains painted
40
+ // behind the opaque loaded image — correct, no flash, no layout shift.
41
+ const [loaded, setLoaded] = useState(false);
30
42
 
31
43
  const invertClass =
32
44
  invertFrom === "light" ? "invert dark:invert-0" :
33
45
  invertFrom === "dark" ? "dark:invert" :
34
46
  undefined;
35
47
 
36
- const mediaClass = cn("h-full w-full", imgClassName, invertClass);
48
+ const mediaClass = cn("relative h-full w-full", imgClassName, invertClass);
37
49
 
38
50
  return (
39
- <div className={className}>
51
+ <div className={cn("relative", className)}>
40
52
  {src ? (
41
- kind === "video" ? (
42
- <video src={src} poster={poster} muted playsInline loop autoPlay className={mediaClass} />
43
- ) : (
44
- <img src={src} srcSet={srcset} alt={alt} className={mediaClass} />
45
- )
53
+ <>
54
+ <div
55
+ aria-hidden="true"
56
+ data-testid="media-placeholder"
57
+ className={cn(
58
+ "pointer-events-none absolute inset-0 bg-base-accent transition-opacity duration-300",
59
+ loaded ? "opacity-0" : "opacity-100",
60
+ )}
61
+ />
62
+ {kind === "video" ? (
63
+ <video
64
+ src={src}
65
+ poster={poster}
66
+ width={width}
67
+ height={height}
68
+ muted
69
+ playsInline
70
+ loop
71
+ autoPlay
72
+ className={mediaClass}
73
+ onLoadedData={() => setLoaded(true)}
74
+ />
75
+ ) : (
76
+ <img
77
+ src={src}
78
+ srcSet={srcset}
79
+ alt={alt}
80
+ width={width}
81
+ height={height}
82
+ loading="lazy"
83
+ decoding="async"
84
+ className={mediaClass}
85
+ onLoad={() => setLoaded(true)}
86
+ onError={() => setLoaded(true)}
87
+ />
88
+ )}
89
+ </>
46
90
  ) : (
47
91
  <div className="flex aspect-video h-full w-full flex-col items-center justify-center gap-1 rounded bg-base-accent text-base-contrast-light/50">
48
92
  <ImageIcon size={24} />
@@ -33,12 +33,18 @@ export function MediaBlock({ refValue, options, link, isEditMode, onRefChange }:
33
33
  // placeholder. In edit mode these are absent and ResolvedMedia falls back to the
34
34
  // live manifest via context. (Mirrors a3e8913, which did this for MediaGrid/SplitContent.)
35
35
  const baked = refValue as Record<string, unknown>;
36
+ const bakedWidth = typeof baked.width === "number" ? baked.width : undefined;
37
+ const bakedHeight = typeof baked.height === "number" ? baked.height : undefined;
38
+ const aspectRatio =
39
+ bakedWidth && bakedHeight && !options.square ? `${bakedWidth} / ${bakedHeight}` : undefined;
36
40
  const resolved = (
37
41
  <ResolvedMedia
38
42
  imageId={refValue.imageId || undefined}
39
43
  src={baked.src as string | undefined}
40
44
  srcset={baked.srcset as string | undefined}
41
45
  alt={baked.alt as string | undefined}
46
+ width={bakedWidth}
47
+ height={bakedHeight}
42
48
  className="h-full w-full"
43
49
  imgClassName={fitClass}
44
50
  invertFrom={refValue.invertFrom}
@@ -75,6 +81,7 @@ export function MediaBlock({ refValue, options, link, isEditMode, onRefChange }:
75
81
  options.square && "aspect-square",
76
82
  onImageClick && "cursor-pointer",
77
83
  )}
84
+ style={aspectRatio ? { aspectRatio } : undefined}
78
85
  onClick={onImageClick}
79
86
  onPointerDown={onImageClick ? (e) => e.stopPropagation() : undefined}
80
87
  >
@@ -43,15 +43,15 @@ export function PasswordInput({
43
43
  className={cn(
44
44
  "w-full rounded border border-base-200 bg-base px-3 py-2 text-sm text-base-contrast",
45
45
  "focus:border-base-contrast focus:outline-none focus:ring-1 focus:ring-base-contrast",
46
- showToggle && "pr-9",
46
+ showToggle && "pr-10",
47
47
  disabled && "cursor-not-allowed opacity-50",
48
48
  )}
49
49
  />
50
50
  {showToggle && (
51
51
  <IconButton
52
- icon={revealed ? <EyeOff size={16} /> : <Eye size={16} />}
52
+ icon={revealed ? <EyeOff size={20} /> : <Eye size={20} />}
53
53
  label={revealed ? "Hide password" : "Show password"}
54
- size="sm"
54
+ size="md"
55
55
  aria-pressed={revealed}
56
56
  disabled={disabled}
57
57
  onClick={() => setRevealed((v) => !v)}
@@ -0,0 +1,37 @@
1
+ import { useEffect, useState } from "react";
2
+ import { Loader2 } from "lucide-react";
3
+
4
+ interface EditorLoadingProps {
5
+ /** Delay before the spinner appears, to avoid a flash on instant cache hits. */
6
+ delayMs?: number;
7
+ }
8
+
9
+ /**
10
+ * Full-screen loading state shown while EditorShell hydrates and loads content.
11
+ * Keeps the existing aria-busy contract on the wrapper at all times; the visible
12
+ * spinner is delayed so a fast Dexie cache hit doesn't flash it. SSR-safe: the
13
+ * initial render shows the wrapper with no spinner, matching the server output.
14
+ */
15
+ export function EditorLoading({ delayMs = 200 }: EditorLoadingProps) {
16
+ const [showSpinner, setShowSpinner] = useState(false);
17
+
18
+ useEffect(() => {
19
+ const timer = setTimeout(() => setShowSpinner(true), delayMs);
20
+ return () => clearTimeout(timer);
21
+ }, [delayMs]);
22
+
23
+ return (
24
+ <div
25
+ className="flex min-h-screen items-center justify-center"
26
+ aria-busy="true"
27
+ aria-label="Loading content"
28
+ >
29
+ {showSpinner && (
30
+ <Loader2
31
+ className="size-8 animate-spin text-primary"
32
+ data-testid="editor-loading-spinner"
33
+ />
34
+ )}
35
+ </div>
36
+ );
37
+ }
@@ -75,6 +75,7 @@ import { ImageIcon, ListOrderedIcon, FilesIcon, X } from "lucide-react";
75
75
  import { ErrorBoundary } from "../shared/ErrorBoundary";
76
76
  import { HistoryToolbar } from "./HistoryToolbar";
77
77
  import { RestoreModal } from "./RestoreModal";
78
+ import { EditorLoading } from "./EditorLoading";
78
79
  import ViewRenderer from "../sections/ViewRenderer";
79
80
 
80
81
  export { useMediaLibrary } from "./MediaLibraryContext";
@@ -808,7 +809,7 @@ export default function EditorShell({
808
809
  // --- Render ---
809
810
 
810
811
  if (shellState.phase === "loading-content") {
811
- return <div className="min-h-screen" aria-busy="true" aria-label="Loading content" />;
812
+ return <EditorLoading />;
812
813
  }
813
814
 
814
815
  if (shellState.phase === "error") {
@@ -48,8 +48,15 @@ export function SiteSettingsDisplay({ siteConfig, onChange }: Props) {
48
48
  }
49
49
 
50
50
  function handleColorChange(color: string) {
51
- const contrast = deriveContrast(color);
52
- onChange({ ...siteConfig, primaryColor: color, primaryContrast: contrast });
51
+ update({ primaryColor: color });
52
+ }
53
+
54
+ function handleContrastChange(color: string) {
55
+ update({ primaryContrast: color });
56
+ }
57
+
58
+ function resetContrastToAuto() {
59
+ update({ primaryContrast: deriveContrast(siteConfig.primaryColor) });
53
60
  }
54
61
 
55
62
  function handleFontChange(field: "headingFont" | "bodyFont", family: string) {
@@ -79,6 +86,25 @@ export function SiteSettingsDisplay({ siteConfig, onChange }: Props) {
79
86
  </div>
80
87
  </div>
81
88
 
89
+ <div>
90
+ <FormLabel>Accent (button &amp; highlight text)</FormLabel>
91
+ <div className="flex items-center gap-3">
92
+ <ColorPicker
93
+ value={siteConfig.primaryContrast}
94
+ onChange={handleContrastChange}
95
+ label="Accent color"
96
+ />
97
+ <span className="text-sm text-base-contrast-light">{siteConfig.primaryContrast}</span>
98
+ <Button type="button" variant="ghost" size="sm" onClick={resetContrastToAuto}>
99
+ Reset to auto
100
+ </Button>
101
+ </div>
102
+ <p className="mt-1 text-xs text-base-contrast/70">
103
+ Shown as text on primary-colored buttons and as the text-selection color. "Reset to auto" derives a
104
+ legible light or dark value from your primary brand color.
105
+ </p>
106
+ </div>
107
+
82
108
  <div>
83
109
  <FormLabel>Favicon</FormLabel>
84
110
  <div className="flex items-center gap-3">
@@ -8,6 +8,8 @@ export interface ResolvedMediaResult {
8
8
  poster: string | undefined;
9
9
  alt: string;
10
10
  kind: "image" | "animated" | "video";
11
+ width?: number;
12
+ height?: number;
11
13
  }
12
14
 
13
15
  export function useResolvedMedia(imageId: string | undefined): ResolvedMediaResult {
@@ -26,10 +28,12 @@ export function useResolvedMedia(imageId: string | undefined): ResolvedMediaResu
26
28
  const item = manifestItem ?? pendingItem;
27
29
  const alt = item?.alt ?? "";
28
30
  const kind = item?.kind ?? "image";
31
+ const width = item?.width;
32
+ const height = item?.height;
29
33
 
30
34
  const localUrl = pendingLocalUrls[imageId];
31
35
  if (localUrl) {
32
- return { src: localUrl, srcset: undefined, poster: undefined, alt, kind };
36
+ return { src: localUrl, srcset: undefined, poster: undefined, alt, kind, width, height };
33
37
  }
34
38
 
35
39
  if (manifestItem) {
@@ -42,13 +46,15 @@ export function useResolvedMedia(imageId: string | undefined): ResolvedMediaResu
42
46
  poster: undefined,
43
47
  alt,
44
48
  kind,
49
+ width: resolved.width,
50
+ height: resolved.height,
45
51
  };
46
52
  }
47
53
  if (resolved && resolved.tag === "video") {
48
- return { src: resolved.src, srcset: undefined, poster: resolved.poster, alt, kind };
54
+ return { src: resolved.src, srcset: undefined, poster: resolved.poster, alt, kind, width: resolved.width, height: resolved.height };
49
55
  }
50
56
  }
51
57
 
52
- return { src: undefined, srcset: undefined, poster: undefined, alt, kind };
58
+ return { src: undefined, srcset: undefined, poster: undefined, alt, kind, width, height };
53
59
  }, [imageId, ctx]);
54
60
  }
@@ -45,11 +45,15 @@ export function resolveManifestReferences(
45
45
  if ("srcSet" in resolved && resolved.srcSet) {
46
46
  patch.srcset = resolved.srcSet;
47
47
  }
48
+ patch.width = resolved.width;
49
+ patch.height = resolved.height;
48
50
  } else if (resolved && resolved.tag === "video") {
49
51
  patch.src = resolved.src;
50
52
  if (resolved.poster) {
51
53
  patch.poster = resolved.poster;
52
54
  }
55
+ patch.width = resolved.width;
56
+ patch.height = resolved.height;
53
57
  }
54
58
  return { ...obj, ...patch };
55
59
  }