@withpotter/components 0.13.0-next.74 → 0.13.0-next.77

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.
@@ -2,6 +2,7 @@
2
2
  import { jsx, jsxs } from "react/jsx-runtime";
3
3
  import { useEffect, useRef, useState } from "react";
4
4
  import { resolveWebUrl } from "@withpotter/lib/web-url";
5
+ import { tileSource } from "./template-tile-source";
5
6
  const BASE_WIDTH = 1280;
6
7
  const RENDER_GRACE_MS = 6e3;
7
8
  function TemplateLiveThumb({
@@ -16,6 +17,7 @@ function TemplateLiveThumb({
16
17
  const [shouldLoadVideo, setShouldLoadVideo] = useState(false);
17
18
  const [isVisible, setIsVisible] = useState(false);
18
19
  const [videoFailed, setVideoFailed] = useState(false);
20
+ const [posterFailed, setPosterFailed] = useState(false);
19
21
  const [rendered, setRendered] = useState(null);
20
22
  const candidates = (Array.isArray(image) ? image : [image]).filter(
21
23
  (src) => !!src
@@ -90,12 +92,21 @@ function TemplateLiveThumb({
90
92
  );
91
93
  return () => clearTimeout(t);
92
94
  }, [still]);
95
+ const source = tileSource({
96
+ hasVideo: !!media && (!!media.mp4 || !!media.webm),
97
+ hasPoster: !!media?.poster,
98
+ videoWanted: !!media && shouldLoadVideo,
99
+ videoFailed,
100
+ posterFailed,
101
+ stillAvailable: !!still,
102
+ renderGaveUp: rendered === false
103
+ });
93
104
  return /* @__PURE__ */ jsx(
94
105
  "div",
95
106
  {
96
107
  ref,
97
108
  className: "relative aspect-[16/9] w-full overflow-hidden bg-white",
98
- children: media && shouldLoadVideo && !videoFailed ? /* @__PURE__ */ jsxs(
109
+ children: source === "video" && media ? /* @__PURE__ */ jsxs(
99
110
  "video",
100
111
  {
101
112
  ref: videoRef,
@@ -121,7 +132,7 @@ function TemplateLiveThumb({
121
132
  media.webm ? /* @__PURE__ */ jsx("source", { src: media.webm, type: "video/webm" }) : null
122
133
  ]
123
134
  }
124
- ) : media?.poster && !videoFailed ? (
135
+ ) : source === "poster" && media ? (
125
136
  // eslint-disable-next-line @next/next/no-img-element
126
137
  /* @__PURE__ */ jsx(
127
138
  "img",
@@ -130,11 +141,11 @@ function TemplateLiveThumb({
130
141
  alt: "",
131
142
  "aria-hidden": true,
132
143
  loading: "lazy",
133
- onError: () => setVideoFailed(true),
144
+ onError: () => setPosterFailed(true),
134
145
  className: "absolute inset-0 h-full w-full object-cover object-top"
135
146
  }
136
147
  )
137
- ) : still ? (
148
+ ) : source === "still" && still ? (
138
149
  // eslint-disable-next-line @next/next/no-img-element
139
150
  /* @__PURE__ */ jsx(
140
151
  "img",
@@ -147,7 +158,7 @@ function TemplateLiveThumb({
147
158
  className: "absolute inset-0 h-full w-full object-cover object-top"
148
159
  }
149
160
  )
150
- ) : rendered === false ? /* @__PURE__ */ jsx("div", { className: "absolute inset-0 flex items-center justify-center bg-[var(--bg-elevated)] px-3 text-center", children: /* @__PURE__ */ jsxs("span", { className: "text-xs text-[var(--text-tertiary)]", children: [
161
+ ) : source === "unavailable" ? /* @__PURE__ */ jsx("div", { className: "absolute inset-0 flex items-center justify-center bg-[var(--bg-elevated)] px-3 text-center", children: /* @__PURE__ */ jsxs("span", { className: "text-xs text-[var(--text-tertiary)]", children: [
151
162
  name ?? "Preview",
152
163
  " unavailable"
153
164
  ] }) }) : /* @__PURE__ */ jsx(
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/templates/template-live-thumb.tsx"],"sourcesContent":["'use client';\n\nimport { useEffect, useRef, useState } from 'react';\nimport { resolveWebUrl } from '@withpotter/lib/web-url';\n\nconst BASE_WIDTH = 1280;\n/** Long enough for a cold render, short enough not to hold a blank tile. */\nconst RENDER_GRACE_MS = 6000;\n\nexport interface TemplatePreviewMedia {\n poster: string;\n webm?: string;\n mp4?: string;\n width: number;\n height: number;\n durationMs: number;\n sourceHash: string;\n}\n\n/**\n * A template's picture.\n *\n * PREFERS A STILL, FALLS BACK TO A LIVE RENDER. It used to be a live\n * `/preview/{id}` iframe unconditionally, which meant a gallery booted a full\n * storefront render per tile just to show a thumbnail. Slow on a Lagos\n * connection, uncacheable, and — the part that actually bit — a hard runtime\n * dependency on the renderer for a static need. With web-render down or\n * mid-deploy, every tile in the product was a blank white box.\n *\n * So: a real image when one exists, the live render only when it does not, and\n * a legible placeholder when neither answers. The live path stays because it is\n * genuinely useful (it can never be stale, and it is right for the preview\n * panel where someone asked to see the thing work), but it is no longer the\n * only way a template can be seen.\n */\nexport function TemplateLiveThumb({\n url,\n preview,\n image,\n name,\n}: {\n url: string;\n preview?: TemplatePreviewMedia | null;\n /**\n * Pictures to try, best first. When one loads, nothing is rendered live.\n *\n * A LIST, not one path, because there are two places a real picture can come\n * from and only one of them was being read. `stillFor` points at a still\n * generated into this app's own `public/templates/`, which has to be\n * regenerated by hand whenever a template is added — so the three most recent\n * templates had no still, fell straight through to the live iframe, and every\n * one of them showed as a blank white box. Meanwhile the template package\n * itself ships `thumbnail.png`, a clean full-page capture, and nothing in this\n * app ever asked for it.\n */\n image?: string | (string | null | undefined)[] | null;\n /** Named in the placeholder, so a failed tile is still identifiable. */\n name?: string;\n}) {\n const ref = useRef<HTMLDivElement>(null);\n const videoRef = useRef<HTMLVideoElement>(null);\n const [scale, setScale] = useState(0.3);\n const [shouldLoadVideo, setShouldLoadVideo] = useState(false);\n const [isVisible, setIsVisible] = useState(false);\n const [videoFailed, setVideoFailed] = useState(false);\n /** null while the frame is given a chance, false once it is not coming. */\n const [rendered, setRendered] = useState<boolean | null>(null);\n /**\n * A picture that 404s must fall through to the next one, not sit there broken.\n *\n * `stillFor` always returns a path — it cannot know whether the file exists —\n * so the `image` prop is never falsy and the stated precedence (picture, then\n * live render, then placeholder) silently collapsed to its first step.\n */\n const candidates = (Array.isArray(image) ? image : [image]).filter(\n (src): src is string => !!src,\n );\n const [failedCount, setFailedCount] = useState(0);\n const still = candidates[failedCount] ?? null;\n const media = preview\n ? {\n poster: `${resolveWebUrl()}${preview.poster}`,\n webm: preview.webm ? `${resolveWebUrl()}${preview.webm}` : undefined,\n mp4: preview.mp4 ? `${resolveWebUrl()}${preview.mp4}` : undefined,\n }\n : null;\n\n useEffect(() => {\n if (!preview) return;\n const connection = (\n navigator as Navigator & { connection?: { saveData?: boolean } }\n ).connection;\n if (\n window.matchMedia('(prefers-reduced-motion: reduce)').matches ||\n connection?.saveData === true\n ) {\n return;\n }\n const host = ref.current;\n if (!host) return;\n const loadObserver = new IntersectionObserver(\n ([entry]) => {\n if (!entry.isIntersecting) return;\n setShouldLoadVideo(true);\n loadObserver.disconnect();\n },\n { rootMargin: '600px 0px' },\n );\n const visibilityObserver = new IntersectionObserver(\n ([entry]) => setIsVisible(entry.isIntersecting),\n { threshold: 0.01 },\n );\n loadObserver.observe(host);\n visibilityObserver.observe(host);\n return () => {\n loadObserver.disconnect();\n visibilityObserver.disconnect();\n };\n }, [preview]);\n\n useEffect(() => {\n const video = videoRef.current;\n if (!video) return;\n const syncPlayback = () => {\n if (isVisible && document.visibilityState === 'visible') {\n void video.play().catch((error: unknown) => {\n if (error instanceof DOMException && error.name === 'AbortError') {\n return;\n }\n setVideoFailed(true);\n });\n } else {\n video.pause();\n }\n };\n syncPlayback();\n document.addEventListener('visibilitychange', syncPlayback);\n return () => document.removeEventListener('visibilitychange', syncPlayback);\n }, [isVisible, shouldLoadVideo]);\n\n useEffect(() => {\n const el = ref.current;\n if (!el) return;\n const ro = new ResizeObserver(([entry]) => {\n if (entry) setScale(entry.contentRect.width / BASE_WIDTH);\n });\n ro.observe(el);\n return () => ro.disconnect();\n }, []);\n\n useEffect(() => {\n if (still) return;\n // A refused connection never fires onLoad, so the only way to know the\n // render is not coming is to stop waiting for it.\n const t = setTimeout(\n () => setRendered((r) => (r === null ? false : r)),\n RENDER_GRACE_MS,\n );\n return () => clearTimeout(t);\n }, [still]);\n\n return (\n <div\n ref={ref}\n className=\"relative aspect-[16/9] w-full overflow-hidden bg-white\"\n >\n {media && shouldLoadVideo && !videoFailed ? (\n <video\n ref={videoRef}\n className=\"absolute inset-0 h-full w-full object-cover object-top\"\n muted\n loop\n playsInline\n preload=\"metadata\"\n poster={media.poster}\n aria-hidden\n onError={() => setVideoFailed(true)}\n onCanPlay={() => {\n if (!isVisible || document.visibilityState !== 'visible') return;\n void videoRef.current?.play().catch((error: unknown) => {\n if (\n error instanceof DOMException &&\n error.name === 'AbortError'\n ) {\n return;\n }\n setVideoFailed(true);\n });\n }}\n >\n {media.mp4 ? <source src={media.mp4} type=\"video/mp4\" /> : null}\n {media.webm ? <source src={media.webm} type=\"video/webm\" /> : null}\n </video>\n ) : media?.poster && !videoFailed ? (\n // eslint-disable-next-line @next/next/no-img-element\n <img\n src={media.poster}\n alt=\"\"\n aria-hidden\n loading=\"lazy\"\n onError={() => setVideoFailed(true)}\n className=\"absolute inset-0 h-full w-full object-cover object-top\"\n />\n ) : still ? (\n // eslint-disable-next-line @next/next/no-img-element\n <img\n src={still}\n alt=\"\"\n aria-hidden\n loading=\"lazy\"\n // Try the next candidate, then the live render, then the placeholder.\n onError={() => setFailedCount((n) => n + 1)}\n className=\"absolute inset-0 h-full w-full object-cover object-top\"\n />\n ) : rendered === false ? (\n <div className=\"absolute inset-0 flex items-center justify-center bg-[var(--bg-elevated)] px-3 text-center\">\n <span className=\"text-xs text-[var(--text-tertiary)]\">\n {name ?? 'Preview'} unavailable\n </span>\n </div>\n ) : (\n <iframe\n src={url}\n tabIndex={-1}\n aria-hidden\n loading=\"lazy\"\n onLoad={() => setRendered(true)}\n className=\"pointer-events-none absolute left-0 top-0 origin-top-left border-0\"\n style={{\n width: BASE_WIDTH,\n height: BASE_WIDTH * (5 / 3),\n transform: `scale(${scale})`,\n }}\n />\n )}\n </div>\n );\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/templates/template-live-thumb.tsx"],"sourcesContent":["'use client';\n\nimport { useEffect, useRef, useState } from 'react';\nimport { resolveWebUrl } from '@withpotter/lib/web-url';\nimport { tileSource } from './template-tile-source';\n\nconst BASE_WIDTH = 1280;\n/** Long enough for a cold render, short enough not to hold a blank tile. */\nconst RENDER_GRACE_MS = 6000;\n\nexport interface TemplatePreviewMedia {\n poster: string;\n webm?: string;\n mp4?: string;\n width: number;\n height: number;\n durationMs: number;\n sourceHash: string;\n}\n\n/**\n * A template's picture.\n *\n * PREFERS A STILL, FALLS BACK TO A LIVE RENDER. It used to be a live\n * `/preview/{id}` iframe unconditionally, which meant a gallery booted a full\n * storefront render per tile just to show a thumbnail. Slow on a Lagos\n * connection, uncacheable, and — the part that actually bit — a hard runtime\n * dependency on the renderer for a static need. With web-render down or\n * mid-deploy, every tile in the product was a blank white box.\n *\n * So: a real image when one exists, the live render only when it does not, and\n * a legible placeholder when neither answers. The live path stays because it is\n * genuinely useful (it can never be stale, and it is right for the preview\n * panel where someone asked to see the thing work), but it is no longer the\n * only way a template can be seen.\n */\nexport function TemplateLiveThumb({\n url,\n preview,\n image,\n name,\n}: {\n url: string;\n preview?: TemplatePreviewMedia | null;\n /**\n * Pictures to try, best first. When one loads, nothing is rendered live.\n *\n * A LIST, not one path, because there are two places a real picture can come\n * from and only one of them was being read. `stillFor` points at a still\n * generated into this app's own `public/templates/`, which has to be\n * regenerated by hand whenever a template is added — so the three most recent\n * templates had no still, fell straight through to the live iframe, and every\n * one of them showed as a blank white box. Meanwhile the template package\n * itself ships `thumbnail.png`, a clean full-page capture, and nothing in this\n * app ever asked for it.\n */\n image?: string | (string | null | undefined)[] | null;\n /** Named in the placeholder, so a failed tile is still identifiable. */\n name?: string;\n}) {\n const ref = useRef<HTMLDivElement>(null);\n const videoRef = useRef<HTMLVideoElement>(null);\n const [scale, setScale] = useState(0.3);\n const [shouldLoadVideo, setShouldLoadVideo] = useState(false);\n const [isVisible, setIsVisible] = useState(false);\n const [videoFailed, setVideoFailed] = useState(false);\n /** The poster is its own failure: a video that is not there must not take a working poster with it. */\n const [posterFailed, setPosterFailed] = useState(false);\n /** null while the frame is given a chance, false once it is not coming. */\n const [rendered, setRendered] = useState<boolean | null>(null);\n /**\n * A picture that 404s must fall through to the next one, not sit there broken.\n *\n * `stillFor` always returns a path — it cannot know whether the file exists —\n * so the `image` prop is never falsy and the stated precedence (picture, then\n * live render, then placeholder) silently collapsed to its first step.\n */\n const candidates = (Array.isArray(image) ? image : [image]).filter(\n (src): src is string => !!src,\n );\n const [failedCount, setFailedCount] = useState(0);\n const still = candidates[failedCount] ?? null;\n const media = preview\n ? {\n poster: `${resolveWebUrl()}${preview.poster}`,\n webm: preview.webm ? `${resolveWebUrl()}${preview.webm}` : undefined,\n mp4: preview.mp4 ? `${resolveWebUrl()}${preview.mp4}` : undefined,\n }\n : null;\n\n useEffect(() => {\n if (!preview) return;\n const connection = (\n navigator as Navigator & { connection?: { saveData?: boolean } }\n ).connection;\n if (\n window.matchMedia('(prefers-reduced-motion: reduce)').matches ||\n connection?.saveData === true\n ) {\n return;\n }\n const host = ref.current;\n if (!host) return;\n const loadObserver = new IntersectionObserver(\n ([entry]) => {\n if (!entry.isIntersecting) return;\n setShouldLoadVideo(true);\n loadObserver.disconnect();\n },\n { rootMargin: '600px 0px' },\n );\n const visibilityObserver = new IntersectionObserver(\n ([entry]) => setIsVisible(entry.isIntersecting),\n { threshold: 0.01 },\n );\n loadObserver.observe(host);\n visibilityObserver.observe(host);\n return () => {\n loadObserver.disconnect();\n visibilityObserver.disconnect();\n };\n }, [preview]);\n\n useEffect(() => {\n const video = videoRef.current;\n if (!video) return;\n const syncPlayback = () => {\n if (isVisible && document.visibilityState === 'visible') {\n void video.play().catch((error: unknown) => {\n if (error instanceof DOMException && error.name === 'AbortError') {\n return;\n }\n setVideoFailed(true);\n });\n } else {\n video.pause();\n }\n };\n syncPlayback();\n document.addEventListener('visibilitychange', syncPlayback);\n return () => document.removeEventListener('visibilitychange', syncPlayback);\n }, [isVisible, shouldLoadVideo]);\n\n useEffect(() => {\n const el = ref.current;\n if (!el) return;\n const ro = new ResizeObserver(([entry]) => {\n if (entry) setScale(entry.contentRect.width / BASE_WIDTH);\n });\n ro.observe(el);\n return () => ro.disconnect();\n }, []);\n\n useEffect(() => {\n if (still) return;\n // A refused connection never fires onLoad, so the only way to know the\n // render is not coming is to stop waiting for it.\n const t = setTimeout(\n () => setRendered((r) => (r === null ? false : r)),\n RENDER_GRACE_MS,\n );\n return () => clearTimeout(t);\n }, [still]);\n\n const source = tileSource({\n hasVideo: !!media && (!!media.mp4 || !!media.webm),\n hasPoster: !!media?.poster,\n videoWanted: !!media && shouldLoadVideo,\n videoFailed,\n posterFailed,\n stillAvailable: !!still,\n renderGaveUp: rendered === false,\n });\n\n return (\n <div\n ref={ref}\n className=\"relative aspect-[16/9] w-full overflow-hidden bg-white\"\n >\n {source === 'video' && media ? (\n <video\n ref={videoRef}\n className=\"absolute inset-0 h-full w-full object-cover object-top\"\n muted\n loop\n playsInline\n preload=\"metadata\"\n poster={media.poster}\n aria-hidden\n onError={() => setVideoFailed(true)}\n onCanPlay={() => {\n if (!isVisible || document.visibilityState !== 'visible') return;\n void videoRef.current?.play().catch((error: unknown) => {\n if (\n error instanceof DOMException &&\n error.name === 'AbortError'\n ) {\n return;\n }\n setVideoFailed(true);\n });\n }}\n >\n {media.mp4 ? <source src={media.mp4} type=\"video/mp4\" /> : null}\n {media.webm ? <source src={media.webm} type=\"video/webm\" /> : null}\n </video>\n ) : source === 'poster' && media ? (\n // eslint-disable-next-line @next/next/no-img-element\n <img\n src={media.poster}\n alt=\"\"\n aria-hidden\n loading=\"lazy\"\n onError={() => setPosterFailed(true)}\n className=\"absolute inset-0 h-full w-full object-cover object-top\"\n />\n ) : source === 'still' && still ? (\n // eslint-disable-next-line @next/next/no-img-element\n <img\n src={still}\n alt=\"\"\n aria-hidden\n loading=\"lazy\"\n // Try the next candidate, then the live render, then the placeholder.\n onError={() => setFailedCount((n) => n + 1)}\n className=\"absolute inset-0 h-full w-full object-cover object-top\"\n />\n ) : source === 'unavailable' ? (\n <div className=\"absolute inset-0 flex items-center justify-center bg-[var(--bg-elevated)] px-3 text-center\">\n <span className=\"text-xs text-[var(--text-tertiary)]\">\n {name ?? 'Preview'} unavailable\n </span>\n </div>\n ) : (\n <iframe\n src={url}\n tabIndex={-1}\n aria-hidden\n loading=\"lazy\"\n onLoad={() => setRendered(true)}\n className=\"pointer-events-none absolute left-0 top-0 origin-top-left border-0\"\n style={{\n width: BASE_WIDTH,\n height: BASE_WIDTH * (5 / 3),\n transform: `scale(${scale})`,\n }}\n />\n )}\n </div>\n );\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,24 @@
1
+ /** What a template tile draws, best first. */
2
+ export type TileSource = 'video' | 'poster' | 'still' | 'unavailable' | 'live';
3
+ /**
4
+ * Which picture a template tile shows.
5
+ *
6
+ * The order is a video once one is near the screen, else the template's own poster, else a still, else the live render, else
7
+ * a placeholder. A failure moves a tile down only as far as the thing that failed: a video that does not exist leaves the
8
+ * poster showing. It used to drop the tile all the way to the live render, which asks the engine for `/preview/<id>` once per
9
+ * tile (about twenty requests, most of them 404 on a Box that does not serve those templates) and ended in "unavailable"
10
+ * after the render grace, with a perfectly good poster loaded and thrown away.
11
+ */
12
+ export declare function tileSource({ hasVideo, hasPoster, videoWanted, videoFailed, posterFailed, stillAvailable, renderGaveUp, }: {
13
+ /** The template has a video (an mp4 or a webm). */
14
+ hasVideo: boolean;
15
+ hasPoster: boolean;
16
+ /** The tile is near the screen and the visitor has not asked for less motion. */
17
+ videoWanted: boolean;
18
+ videoFailed: boolean;
19
+ posterFailed: boolean;
20
+ /** A still picture is left to try (one that has not failed). */
21
+ stillAvailable: boolean;
22
+ /** The live render did not answer in time. */
23
+ renderGaveUp: boolean;
24
+ }): TileSource;
@@ -0,0 +1,18 @@
1
+ function tileSource({
2
+ hasVideo,
3
+ hasPoster,
4
+ videoWanted,
5
+ videoFailed,
6
+ posterFailed,
7
+ stillAvailable,
8
+ renderGaveUp
9
+ }) {
10
+ if (hasVideo && videoWanted && !videoFailed && !posterFailed) return "video";
11
+ if (hasPoster && !posterFailed) return "poster";
12
+ if (stillAvailable) return "still";
13
+ return renderGaveUp ? "unavailable" : "live";
14
+ }
15
+ export {
16
+ tileSource
17
+ };
18
+ //# sourceMappingURL=template-tile-source.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/templates/template-tile-source.ts"],"sourcesContent":["/** What a template tile draws, best first. */\nexport type TileSource = 'video' | 'poster' | 'still' | 'unavailable' | 'live';\n\n/**\n * Which picture a template tile shows.\n *\n * The order is a video once one is near the screen, else the template's own poster, else a still, else the live render, else\n * a placeholder. A failure moves a tile down only as far as the thing that failed: a video that does not exist leaves the\n * poster showing. It used to drop the tile all the way to the live render, which asks the engine for `/preview/<id>` once per\n * tile (about twenty requests, most of them 404 on a Box that does not serve those templates) and ended in \"unavailable\"\n * after the render grace, with a perfectly good poster loaded and thrown away.\n */\nexport function tileSource({\n hasVideo,\n hasPoster,\n videoWanted,\n videoFailed,\n posterFailed,\n stillAvailable,\n renderGaveUp,\n}: {\n /** The template has a video (an mp4 or a webm). */\n hasVideo: boolean;\n hasPoster: boolean;\n /** The tile is near the screen and the visitor has not asked for less motion. */\n videoWanted: boolean;\n videoFailed: boolean;\n posterFailed: boolean;\n /** A still picture is left to try (one that has not failed). */\n stillAvailable: boolean;\n /** The live render did not answer in time. */\n renderGaveUp: boolean;\n}): TileSource {\n if (hasVideo && videoWanted && !videoFailed && !posterFailed) return 'video';\n if (hasPoster && !posterFailed) return 'poster';\n if (stillAvailable) return 'still';\n return renderGaveUp ? 'unavailable' : 'live';\n}\n"],"mappings":"AAYO,SAAS,WAAW;AAAA,EACzB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAYe;AACb,MAAI,YAAY,eAAe,CAAC,eAAe,CAAC,aAAc,QAAO;AACrE,MAAI,aAAa,CAAC,aAAc,QAAO;AACvC,MAAI,eAAgB,QAAO;AAC3B,SAAO,eAAe,gBAAgB;AACxC;","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@withpotter/components",
3
- "version": "0.13.0-next.74",
3
+ "version": "0.13.0-next.77",
4
4
  "description": "Box dashboard feature components: editors, mobile screens, pickers, and composite UI built on @withpotter/ui-kit.",
5
5
  "license": "UNLICENSED",
6
6
  "sideEffects": [
@@ -50,9 +50,9 @@
50
50
  "@tiptap/react": "^3.22.5",
51
51
  "@tiptap/starter-kit": "^3.22.5",
52
52
  "@tiptap/suggestion": "^3.22.5",
53
- "@withpotter/hooks": "^0.13.0-next.74",
54
- "@withpotter/lib": "^0.13.0-next.74",
55
- "@withpotter/ui-kit": "^0.13.0-next.74",
53
+ "@withpotter/hooks": "^0.13.0-next.77",
54
+ "@withpotter/lib": "^0.13.0-next.77",
55
+ "@withpotter/ui-kit": "^0.13.0-next.77",
56
56
  "@xyflow/react": "^12.11.2",
57
57
  "dompurify": "^3.3.1",
58
58
  "jsqr": "1.4.0",
@@ -77,7 +77,7 @@
77
77
  "typescript": "^5"
78
78
  },
79
79
  "publishConfig": {
80
- "registry": "https://registry.npmjs.org",
80
+ "registry": "https://registry.npmjs.org/",
81
81
  "access": "restricted"
82
82
  }
83
83
  }