@drawnagency/primitives 0.1.70 → 0.1.72

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 (85) hide show
  1. package/dist/{chunk-VVK6ZRCN.js → chunk-2HZB675H.js} +12 -12
  2. package/dist/chunk-7SIZ2LV5.js +117 -0
  3. package/dist/{chunk-2BHPWO7M.js → chunk-BTGHF3SW.js} +158 -32
  4. package/dist/{chunk-3DCG4DNU.js → chunk-PZJL2S3Z.js} +8 -4
  5. package/dist/chunk-RBTSUR5V.js +37 -0
  6. package/dist/{chunk-EWKCEPMW.js → chunk-SFKYUKVN.js} +1 -1
  7. package/dist/{chunk-QKOIEWZ4.js → chunk-SWIHUIFT.js} +38 -18
  8. package/dist/{chunk-6UDYJQL6.js → chunk-TOBPMMRM.js} +1 -1
  9. package/dist/{chunk-TYYCKVAW.js → chunk-UBK2IZ6G.js} +276 -96
  10. package/dist/{chunk-WXVKX6PY.js → chunk-VHINNGIF.js} +41 -95
  11. package/dist/{chunk-KWBM27UR.js → chunk-WCALKWPO.js} +4 -59
  12. package/dist/{chunk-WNPMBFD6.js → chunk-ZRWGV5O6.js} +3 -3
  13. package/dist/chunk-ZSPCE6MZ.js +70 -0
  14. package/dist/components/chat/ChatPanel.d.ts +4 -1
  15. package/dist/components/chat/ChatPanel.d.ts.map +1 -1
  16. package/dist/components/editor/SettingsForm.d.ts.map +1 -1
  17. package/dist/components/editor/index.js +6 -3
  18. package/dist/components/primitives/index.js +7 -7
  19. package/dist/components/sections/Video/VideoBlock.d.ts +31 -0
  20. package/dist/components/sections/Video/VideoBlock.d.ts.map +1 -0
  21. package/dist/components/sections/Video/index.d.ts +28 -0
  22. package/dist/components/sections/Video/index.d.ts.map +1 -0
  23. package/dist/components/sections/all-sections.d.ts +25 -0
  24. package/dist/components/sections/all-sections.d.ts.map +1 -1
  25. package/dist/components/sections/brand-guide.js +7 -7
  26. package/dist/components/sections/core-sections.d.ts +25 -0
  27. package/dist/components/sections/core-sections.d.ts.map +1 -1
  28. package/dist/components/sections/register-schemas.js +13 -12
  29. package/dist/components/sections/register.js +13 -12
  30. package/dist/components/shared/Tabs.d.ts +1 -1
  31. package/dist/components/shared/VideoSourceField.d.ts +8 -0
  32. package/dist/components/shared/VideoSourceField.d.ts.map +1 -0
  33. package/dist/components/shell/ChatbotConfigModal.d.ts.map +1 -1
  34. package/dist/components/shell/EditorShell.d.ts.map +1 -1
  35. package/dist/components/shell/MediaLibraryModal.d.ts +4 -1
  36. package/dist/components/shell/MediaLibraryModal.d.ts.map +1 -1
  37. package/dist/hooks/useEditorPublish.d.ts.map +1 -1
  38. package/dist/hooks/useMediaPipeline.d.ts +9 -1
  39. package/dist/hooks/useMediaPipeline.d.ts.map +1 -1
  40. package/dist/index.js +24 -24
  41. package/dist/lib/index.js +10 -10
  42. package/dist/lib/registry.d.ts +6 -0
  43. package/dist/lib/registry.d.ts.map +1 -1
  44. package/dist/lib/video-url.d.ts +7 -0
  45. package/dist/lib/video-url.d.ts.map +1 -0
  46. package/dist/lib/video-url.js +9 -0
  47. package/dist/media/index.js +2 -2
  48. package/dist/media/videoPoster.d.ts +1 -0
  49. package/dist/media/videoPoster.d.ts.map +1 -1
  50. package/dist/schemas/index.js +4 -4
  51. package/dist/schemas/link.d.ts.map +1 -1
  52. package/dist/schemas/link.js +1 -1
  53. package/dist/schemas/media.d.ts +13 -0
  54. package/dist/schemas/media.d.ts.map +1 -1
  55. package/dist/schemas/shared.d.ts.map +1 -1
  56. package/dist/schemas/site-config.d.ts +3 -0
  57. package/dist/schemas/site-config.d.ts.map +1 -1
  58. package/dist/storage/index.d.ts +1 -1
  59. package/dist/storage/index.d.ts.map +1 -1
  60. package/dist/storage/types.d.ts +26 -0
  61. package/dist/storage/types.d.ts.map +1 -1
  62. package/package.json +5 -1
  63. package/src/components/chat/ChatPanel.tsx +78 -49
  64. package/src/components/editor/SettingsForm.tsx +11 -0
  65. package/src/components/sections/Video/VideoBlock.tsx +149 -0
  66. package/src/components/sections/Video/index.tsx +111 -0
  67. package/src/components/sections/core-sections.ts +2 -0
  68. package/src/components/shared/Tabs.tsx +2 -2
  69. package/src/components/shared/VideoSourceField.tsx +144 -0
  70. package/src/components/shell/ChatbotConfigModal.tsx +44 -3
  71. package/src/components/shell/EditorShell.tsx +8 -0
  72. package/src/components/shell/MediaLibraryModal.tsx +47 -1
  73. package/src/hooks/useEditorPublish.ts +3 -0
  74. package/src/hooks/useMediaPipeline.ts +188 -25
  75. package/src/lib/registry.ts +7 -0
  76. package/src/lib/video-url.ts +39 -0
  77. package/src/media/videoPoster.ts +2 -2
  78. package/src/schemas/link.ts +15 -4
  79. package/src/schemas/media.ts +12 -0
  80. package/src/schemas/shared.ts +23 -14
  81. package/src/schemas/site-config.ts +10 -2
  82. package/src/storage/index.ts +1 -1
  83. package/src/storage/types.ts +23 -0
  84. package/dist/chunk-4E66N6UV.js +0 -10
  85. package/dist/chunk-I5737FJY.js +0 -62
@@ -0,0 +1,39 @@
1
+ export type ParsedVideoUrl = { provider: "youtube" | "vimeo"; id: string };
2
+
3
+ const YT_ID = /^[\w-]{6,20}$/;
4
+
5
+ export function parseVideoUrl(raw: string): ParsedVideoUrl | null {
6
+ let url: URL;
7
+ try {
8
+ url = new URL(raw.trim());
9
+ } catch {
10
+ return null;
11
+ }
12
+ if (url.protocol !== "https:" && url.protocol !== "http:") return null;
13
+ const host = url.hostname.replace(/^www\./, "");
14
+
15
+ if (host === "youtube.com" || host === "m.youtube.com" || host === "youtube-nocookie.com") {
16
+ if (url.pathname === "/watch") {
17
+ const v = url.searchParams.get("v");
18
+ return v && YT_ID.test(v) ? { provider: "youtube", id: v } : null;
19
+ }
20
+ const m = url.pathname.match(/^\/(?:embed|shorts|live)\/([\w-]{6,20})$/);
21
+ return m ? { provider: "youtube", id: m[1] } : null;
22
+ }
23
+ if (host === "youtu.be") {
24
+ const m = url.pathname.match(/^\/([\w-]{6,20})$/);
25
+ return m ? { provider: "youtube", id: m[1] } : null;
26
+ }
27
+ if (host === "vimeo.com" || host === "player.vimeo.com") {
28
+ // Last numeric path segment: /76979871, /video/76979871, /channels/x/76979871
29
+ const m = url.pathname.match(/\/(\d{6,12})$/);
30
+ return m ? { provider: "vimeo", id: m[1] } : null;
31
+ }
32
+ return null;
33
+ }
34
+
35
+ export function embedUrl(p: ParsedVideoUrl): string {
36
+ return p.provider === "youtube"
37
+ ? `https://www.youtube-nocookie.com/embed/${p.id}`
38
+ : `https://player.vimeo.com/video/${p.id}`;
39
+ }
@@ -1,7 +1,7 @@
1
1
  export function generateVideoPoster(
2
2
  blob: Blob,
3
3
  quality: number,
4
- ): Promise<{ posterBlob: Blob; width: number; height: number }> {
4
+ ): Promise<{ posterBlob: Blob; width: number; height: number; duration: number }> {
5
5
  return new Promise((resolve, reject) => {
6
6
  const video = document.createElement("video");
7
7
  const url = URL.createObjectURL(blob);
@@ -59,7 +59,7 @@ export function generateVideoPoster(
59
59
  reject(new Error("Failed to create poster blob"));
60
60
  return;
61
61
  }
62
- resolve({ posterBlob, width: w, height: h });
62
+ resolve({ posterBlob, width: w, height: h, duration: video.duration });
63
63
  },
64
64
  "image/webp",
65
65
  quality / 100,
@@ -21,12 +21,23 @@ export function isSafeHref(href: string): boolean {
21
21
  }
22
22
 
23
23
  export const LinkValueSchema = z.discriminatedUnion("kind", [
24
- z.object({ kind: z.literal("external"), href: z.string().refine(isSafeHref, "Unsafe href scheme"), target: LinkTargetSchema }),
24
+ z.object({
25
+ kind: z.literal("external"),
26
+ href: z
27
+ .string()
28
+ .refine(isSafeHref, "Unsafe href scheme")
29
+ .describe("An absolute http(s)/mailto URL, a relative path, or empty (unset). javascript:, data:, and protocol-relative URLs are rejected."),
30
+ target: LinkTargetSchema.describe("Where the link opens: '_self' (same tab) or '_blank' (new tab)."),
31
+ }),
25
32
  z.object({
26
33
  kind: z.literal("internal"),
27
- pageId: z.string(),
28
- anchorSectionId: z.string().nullable().optional(),
29
- target: LinkTargetSchema,
34
+ pageId: z.string().describe("Id of the page to link to, within the same site."),
35
+ anchorSectionId: z
36
+ .string()
37
+ .nullable()
38
+ .optional()
39
+ .describe("Optional section id on the target page to scroll to; null or omitted links to the page itself."),
40
+ target: LinkTargetSchema.describe("Where the link opens: '_self' (same tab) or '_blank' (new tab)."),
30
41
  }),
31
42
  ]);
32
43
 
@@ -20,6 +20,15 @@ export const MediaItemSchema = z.object({
20
20
  folder: z.string(),
21
21
  variants: z.array(VariantSchema),
22
22
  alt: z.string().default(""),
23
+ // Where the original bytes live. "github" = committed to the repo (images,
24
+ // small loops); "assets" = large original in the AssetStore bucket. Legacy
25
+ // manifests have no field — default keeps them on the git path.
26
+ storage: z.enum(["github", "assets"]).default("github"),
27
+ // Public CDN URL for assets-backed originals, baked at upload time so
28
+ // resolution needs no env access in the browser or at SSR.
29
+ url: z.string().optional(),
30
+ // Seconds; videos only. Informational (library UI).
31
+ duration: z.number().positive().optional(),
23
32
  });
24
33
 
25
34
  export type MediaItem = z.infer<typeof MediaItemSchema>;
@@ -34,6 +43,9 @@ export const MediaConfigSchema = z.object({
34
43
  sizes: z.array(z.number()).default([640, 1080, 1920]),
35
44
  maxFileSize: z.number().default(5242880),
36
45
  quality: z.number().min(1).max(100).default(85),
46
+ // Per-file cap for bucket-backed video uploads (bytes). The git-path cap is
47
+ // maxFileSize; anything between the two routes to the AssetStore.
48
+ maxAssetSize: z.number().default(209_715_200),
37
49
  });
38
50
 
39
51
  export type MediaConfig = z.infer<typeof MediaConfigSchema>;
@@ -33,12 +33,21 @@ export type TextLine = z.infer<typeof TextLineSchema>;
33
33
 
34
34
  // --- Media References ---
35
35
  const BaseMediaRef = z.object({
36
- imageId: z.string().default(""),
37
- caption: z.union([z.string(), z.array(z.string())]).optional(),
38
- background: z.string().optional(),
39
- invertFrom: z.string().optional(),
40
- border: z.boolean().optional(),
41
- objectFit: z.enum(["cover", "contain"]).optional(),
36
+ imageId: z.string().default("").describe("Media-library id of the image/video asset."),
37
+ caption: z
38
+ .union([z.string(), z.array(z.string())])
39
+ .optional()
40
+ .describe("Optional caption: a single string, or multiple lines as an array of strings."),
41
+ background: z.string().optional().describe("Optional CSS background color/value shown behind a transparent image."),
42
+ invertFrom: z
43
+ .string()
44
+ .optional()
45
+ .describe("Optional CSS color; the image inverts when the page background matches it (e.g. for dark-mode logo swaps)."),
46
+ border: z.boolean().optional().describe("When true, draw a border around the media."),
47
+ objectFit: z
48
+ .enum(["cover", "contain"])
49
+ .optional()
50
+ .describe("How the image fills its box: 'contain' (fit whole image) or 'cover' (fill and crop)."),
42
51
  });
43
52
 
44
53
  const ImageRef = BaseMediaRef.extend({
@@ -47,22 +56,22 @@ const ImageRef = BaseMediaRef.extend({
47
56
 
48
57
  const VideoRef = BaseMediaRef.extend({
49
58
  type: z.literal("video"),
50
- poster: z.string().optional(),
51
- autoplay: z.boolean().optional(),
52
- loop: z.boolean().optional(),
53
- muted: z.boolean().optional(),
59
+ poster: z.string().optional().describe("Optional poster-image URL shown before playback starts."),
60
+ autoplay: z.boolean().optional().describe("When true, the video starts playing automatically."),
61
+ loop: z.boolean().optional().describe("When true, the video repeats when it finishes."),
62
+ muted: z.boolean().optional().describe("When true, the video plays without sound."),
54
63
  });
55
64
 
56
65
  const DoDontImageRef = BaseMediaRef.extend({
57
66
  type: z.literal("doDontImage"),
58
- doDont: z.enum(["do", "dont"]),
67
+ doDont: z.enum(["do", "dont"]).describe("Whether this is a 'do' (correct) or 'dont' (incorrect) example."),
59
68
  });
60
69
 
61
70
  const LinkedImageRef = BaseMediaRef.extend({
62
71
  type: z.literal("linkedImage"),
63
- href: z.string(),
64
- target: z.string().optional(),
65
- linkText: z.string().optional(),
72
+ href: z.string().describe("URL the image links to."),
73
+ target: z.string().optional().describe("Link target, e.g. '_self' or '_blank'."),
74
+ linkText: z.string().optional().describe("Optional accessible link text/label."),
66
75
  });
67
76
 
68
77
  export const MediaReferenceSchema = z.discriminatedUnion("type", [
@@ -149,8 +149,16 @@ export const ChatbotConfigSchema = z
149
149
  .object({
150
150
  enabled: z.boolean().default(false),
151
151
  systemPrompt: z.string().max(8_000).default(""),
152
- // Audience names the widget is shown to; [] = all audiences.
152
+ // Audience names the widget is shown to — strict opt-in: [] = no viewer
153
+ // sees it (editors always can via /edit). Gates: [...slug].astro + /api/chat.
153
154
  audiences: z.array(z.string()).default([]),
155
+ // Per-audience prompt overlays, keyed by audience name. Appended AFTER the
156
+ // base systemPrompt, and ONLY for requests whose session audience matches
157
+ // (or an editor's explicit test-panel simulation) — enforced in
158
+ // assembleBrandChat. Keys are not validated against the audience list:
159
+ // entries for deselected/renamed audiences are inert (strict opt-in means
160
+ // those audiences cannot chat at all).
161
+ audiencePrompts: z.record(z.string(), z.string().max(8_000)).default({}),
154
162
  tasks: z.array(ChatbotTaskSchema).max(20).default([]),
155
163
  })
156
164
  .superRefine((cfg, ctx) => {
@@ -188,7 +196,7 @@ export const SiteConfigSchema = z.object({
188
196
  // Brand chatbot config (editor-owned; the platform kill switch lives in the
189
197
  // sites table, not here). Every field defaults so pre-existing
190
198
  // site-config.json files keep parsing.
191
- chatbot: ChatbotConfigSchema.default({ enabled: false, systemPrompt: "", audiences: [], tasks: [] }),
199
+ chatbot: ChatbotConfigSchema.default({ enabled: false, systemPrompt: "", audiences: [], tasks: [], audiencePrompts: {} }),
192
200
  });
193
201
 
194
202
  export type SiteConfig = z.infer<typeof SiteConfigSchema>;
@@ -1,2 +1,2 @@
1
- export type { StorageProvider, FileWrite } from "./types";
1
+ export type { StorageProvider, FileWrite, AssetStore } from "./types";
2
2
  export { StorageConflictError } from "./types";
@@ -38,3 +38,26 @@ export type FileWrite = {
38
38
  content: string | Uint8Array | null;
39
39
  encoding?: "utf-8" | "base64";
40
40
  };
41
+
42
+ /**
43
+ * Optional large-asset store (Supabase Storage bucket). Holds video originals
44
+ * too big for the git path; deliberately generic so future large kinds (zips)
45
+ * can slot in. All paths are SITE-RELATIVE ("media/{hash}/original.mp4") —
46
+ * implementations prepend the tenant prefix ("sites/{siteId}/") themselves so
47
+ * a caller can never address another tenant's objects.
48
+ */
49
+ export interface AssetStore {
50
+ /** Mint a signed direct-upload URL for the browser (PUT). */
51
+ createUploadUrl(
52
+ path: string,
53
+ opts: { contentType: string },
54
+ ): Promise<{ uploadUrl: string; path: string; publicUrl: string }>;
55
+
56
+ /** Deterministic public CDN URL for an object (no network call). */
57
+ getPublicUrl(path: string): string;
58
+
59
+ delete(paths: string[]): Promise<void>;
60
+
61
+ /** Recursive listing under a site-relative prefix. */
62
+ list(prefix: string): Promise<{ path: string; size: number; updatedAt: string }[]>;
63
+ }
@@ -1,10 +0,0 @@
1
- // src/components/shared/PagesContext.tsx
2
- import { createContext, useContext } from "react";
3
- var PagesContext = createContext({ pages: [], getPageHeadings: () => [] });
4
- function usePages() {
5
- return useContext(PagesContext);
6
- }
7
-
8
- export {
9
- usePages
10
- };
@@ -1,62 +0,0 @@
1
- import {
2
- cn
3
- } from "./chunk-7EOSDFS3.js";
4
-
5
- // src/components/shared/icons.tsx
6
- import { GripVertical, Plus, Trash2, X, Settings } from "lucide-react";
7
- import { GripVertical as GripVertical2, Plus as Plus2, Trash2 as Trash22, X as X2, Settings as Settings2 } from "lucide-react";
8
- import { jsx } from "react/jsx-runtime";
9
- function DragHandle({ size = 16 }) {
10
- return /* @__PURE__ */ jsx(GripVertical, { size });
11
- }
12
- function AddIcon({ size = 14 }) {
13
- return /* @__PURE__ */ jsx(Plus, { size });
14
- }
15
- function DeleteIcon({ size = 14 }) {
16
- return /* @__PURE__ */ jsx(Trash2, { size });
17
- }
18
- function SettingsIcon({ size = 14 }) {
19
- return /* @__PURE__ */ jsx(Settings, { size });
20
- }
21
-
22
- // src/components/shared/IconButton.tsx
23
- import { forwardRef } from "react";
24
- import { jsx as jsx2 } from "react/jsx-runtime";
25
- var sizeClasses = {
26
- sm: "h-chrome-sm w-chrome-sm",
27
- md: "h-chrome-md w-chrome-md",
28
- lg: "h-chrome-lg w-chrome-lg"
29
- };
30
- var intentClasses = {
31
- default: "text-base-contrast-light hover:text-base-contrast",
32
- destructive: "text-destructive hover:text-destructive-hover",
33
- primary: "text-primary/60 hover:text-primary"
34
- };
35
- var IconButton = forwardRef(
36
- function IconButton2({ icon, label, size = "md", intent = "default", className, ...rest }, ref) {
37
- return /* @__PURE__ */ jsx2(
38
- "button",
39
- {
40
- ref,
41
- type: "button",
42
- className: cn(
43
- "cursor-pointer flex items-center justify-center rounded transition-colors",
44
- sizeClasses[size],
45
- intentClasses[intent],
46
- className
47
- ),
48
- "aria-label": label,
49
- ...rest,
50
- children: icon
51
- }
52
- );
53
- }
54
- );
55
-
56
- export {
57
- DragHandle,
58
- AddIcon,
59
- DeleteIcon,
60
- SettingsIcon,
61
- IconButton
62
- };