@xiwebdesign/xicms-core 7.10.0 → 7.11.0
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/admin/media/MediaPicker.d.ts +1 -1
- package/dist/admin/media/MediaPicker.js +1 -0
- package/dist/admin/media/MediaPicker.js.map +1 -1
- package/dist/admin/patterns/PatternInserter.js +1 -1
- package/dist/admin/patterns/PatternInserter.js.map +1 -1
- package/dist/admin/visual/VisualEditor.js +92 -15
- package/dist/admin/visual/VisualEditor.js.map +1 -1
- package/dist/admin/visual/wijzigingen.js +18 -2
- package/dist/admin/visual/wijzigingen.js.map +1 -1
- package/dist/demo/demoStore.d.ts +8 -0
- package/dist/demo/demoStore.js +2 -1
- package/dist/demo/demoStore.js.map +1 -1
- package/dist/editor/EditorProvider.js +15 -3
- package/dist/editor/EditorProvider.js.map +1 -1
- package/dist/styles.css +44 -19
- package/dist/version.js +1 -1
- package/package.json +1 -1
|
@@ -10,7 +10,7 @@ export declare function MediaPickerDialog({ siteId, open, onOpenChange, onSelect
|
|
|
10
10
|
open: boolean;
|
|
11
11
|
onOpenChange: (v: boolean) => void;
|
|
12
12
|
onSelect: (m: PickedMedia) => void;
|
|
13
|
-
}): import("react").JSX.Element;
|
|
13
|
+
}): import("react").JSX.Element | null;
|
|
14
14
|
/**
|
|
15
15
|
* Knop die de mediabibliotheek-kiezer opent: zoek + klik om een bestaande afbeelding te kiezen, of
|
|
16
16
|
* upload er direct een nieuwe in. Vervangt het plakken van losse URL's.
|
|
@@ -68,6 +68,7 @@ function MediaPickerDialog({
|
|
|
68
68
|
setBusy(false);
|
|
69
69
|
}
|
|
70
70
|
}
|
|
71
|
+
if (!open) return null;
|
|
71
72
|
return /* @__PURE__ */ jsx(Dialog, { open, onOpenChange, children: /* @__PURE__ */ jsxs(DialogContent, { title: "Afbeelding kiezen", width: "min(92vw, 42rem)", children: [
|
|
72
73
|
/* @__PURE__ */ jsxs("div", { className: "mb-3 flex items-center gap-2", children: [
|
|
73
74
|
/* @__PURE__ */ jsx(Input, { value: q, onChange: (e) => setQ(e.target.value), placeholder: "Zoek in bibliotheek\u2026", className: "h-9" }),
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/admin/media/MediaPicker.tsx"],"sourcesContent":["\"use client\";\n\nimport { useEffect, useMemo, useRef, useState } from \"react\";\nimport { collection, limit, onSnapshot, orderBy, query } from \"firebase/firestore\";\nimport { getDb } from \"../../firebase/client\";\nimport { DEMO_EVENT, isDemoActive, listDemoMediaAll } from \"../../demo/demoStore\";\nimport type { MediaItem } from \"../../content/types\";\nimport { Button, Input } from \"../ui/primitives\";\nimport { Dialog, DialogContent } from \"../ui/dialog\";\nimport { useToast } from \"../ui/toast\";\nimport { uploadImage } from \"../lib/upload\";\n\nexport interface PickedMedia {\n url: string;\n mediaId: string;\n alt?: string;\n}\n\n/** Paginagrootte: begrensd laden i.p.v. de hele bibliotheek bij elke kiezer-open. */\nconst PICKER_PAGE_SIZE = 48;\n\n/** Gecontroleerde mediabibliotheek-kiezer (open/onOpenChange extern) — zoek + klik om te kiezen, of\n * upload direct. Gebruikt door de knop hieronder én door de inline beeld-vervangen in de editor. */\nexport function MediaPickerDialog({\n siteId,\n open,\n onOpenChange,\n onSelect,\n}: {\n siteId: string;\n open: boolean;\n onOpenChange: (v: boolean) => void;\n onSelect: (m: PickedMedia) => void;\n}) {\n const { toast } = useToast();\n const [items, setItems] = useState<MediaItem[]>([]);\n const [q, setQ] = useState(\"\");\n const [busy, setBusy] = useState(false);\n const [pageCount, setPageCount] = useState(1);\n const [hasMore, setHasMore] = useState(false);\n const inputRef = useRef<HTMLInputElement>(null);\n\n useEffect(() => {\n if (!open) return;\n // Demo-modus: lokale bibliotheek (geseede foto's + eigen \"uploads\") i.p.v. Firestore.\n if (isDemoActive()) {\n const sync = () => setItems(listDemoMediaAll(siteId));\n sync();\n window.addEventListener(DEMO_EVENT, sync);\n return () => window.removeEventListener(DEMO_EVENT, sync);\n }\n const db = getDb();\n if (!db) return;\n // Begrensd laden (+\"meer laden\"): geen volledige collectie-read bij elke kiezer-open.\n const max = pageCount * PICKER_PAGE_SIZE;\n const qy = query(collection(db, \"sites\", siteId, \"media\"), orderBy(\"createdAt\", \"desc\"), limit(max + 1));\n return onSnapshot(\n qy,\n (snap) => {\n setHasMore(snap.docs.length > max);\n setItems(snap.docs.slice(0, max).map((d) => ({ id: d.id, ...(d.data() as Omit<MediaItem, \"id\">) })));\n },\n // Zonder onError worden lees-/rules-/App-Check-fouten stil geslikt → de kiezer toont dan\n // onterecht \"Nog geen afbeeldingen\". Nu een duidelijke melding.\n (err) => toast({ title: \"Kon mediabibliotheek niet laden\", description: err.message, tone: \"error\" }),\n );\n }, [open, siteId, toast, pageCount]);\n\n const filtered = useMemo(() => {\n const term = q.trim().toLowerCase();\n if (!term) return items;\n return items.filter((m) => `${m.name ?? \"\"} ${m.alt ?? \"\"}`.toLowerCase().includes(term));\n }, [items, q]);\n\n function pick(m: MediaItem) {\n onSelect({ url: m.url, mediaId: m.id, alt: m.alt });\n onOpenChange(false);\n }\n\n async function onUpload(files: FileList | null) {\n const f = files?.[0];\n if (!f) return;\n setBusy(true);\n try {\n const { url, mediaId } = await uploadImage(siteId, f);\n onSelect({ url, mediaId, alt: \"\" });\n onOpenChange(false);\n } catch (err) {\n toast({ title: \"Upload mislukt\", description: (err as Error).message, tone: \"error\" });\n } finally {\n setBusy(false);\n }\n }\n\n return (\n <Dialog open={open} onOpenChange={onOpenChange}>\n <DialogContent title=\"Afbeelding kiezen\" width=\"min(92vw, 42rem)\">\n <div className=\"mb-3 flex items-center gap-2\">\n <Input value={q} onChange={(e) => setQ(e.target.value)} placeholder=\"Zoek in bibliotheek…\" className=\"h-9\" />\n <Button type=\"button\" disabled={busy} onClick={() => inputRef.current?.click()}>\n {busy ? \"Uploaden…\" : \"Upload nieuwe\"}\n </Button>\n <input ref={inputRef} type=\"file\" accept=\"image/*\" hidden onChange={(e) => { void onUpload(e.target.files); e.target.value = \"\"; }} />\n </div>\n <div className=\"max-h-[55vh] overflow-y-auto\">\n {filtered.length === 0 ? (\n <p className=\"py-8 text-center text-sm text-slate-500\">\n {items.length === 0 ? \"Nog geen afbeeldingen — upload er een.\" : \"Geen resultaten.\"}\n </p>\n ) : (\n <div className=\"grid grid-cols-3 gap-2 sm:grid-cols-4\">\n {filtered.map((m) => (\n <button\n key={m.id}\n type=\"button\"\n onClick={() => pick(m)}\n title={m.alt || m.name}\n className=\"group relative overflow-hidden rounded-md border border-slate-200 focus:outline-none focus:ring-2 focus:ring-indigo-500\"\n >\n {/* eslint-disable-next-line @next/next/no-img-element */}\n <img src={m.url} alt={m.alt ?? m.name} className=\"aspect-square w-full object-cover transition group-hover:opacity-80\" />\n </button>\n ))}\n </div>\n )}\n {hasMore && !q.trim() && (\n <p className=\"mt-2 text-center\">\n <Button type=\"button\" variant=\"ghost\" size=\"sm\" onClick={() => setPageCount((c) => c + 1)}>\n Meer laden…\n </Button>\n </p>\n )}\n </div>\n </DialogContent>\n </Dialog>\n );\n}\n\n/**\n * Knop die de mediabibliotheek-kiezer opent: zoek + klik om een bestaande afbeelding te kiezen, of\n * upload er direct een nieuwe in. Vervangt het plakken van losse URL's.\n */\nexport function MediaPickerButton({\n siteId,\n onSelect,\n label = \"Kies / upload\",\n size = \"sm\",\n variant = \"secondary\",\n}: {\n siteId: string;\n onSelect: (m: PickedMedia) => void;\n label?: string;\n size?: \"sm\" | \"md\";\n variant?: \"primary\" | \"secondary\" | \"ghost\";\n}) {\n const [open, setOpen] = useState(false);\n return (\n <>\n <Button type=\"button\" variant={variant} size={size} onClick={() => setOpen(true)}>{label}</Button>\n <MediaPickerDialog siteId={siteId} open={open} onOpenChange={setOpen} onSelect={onSelect} />\n </>\n );\n}\n"],"mappings":";AAiGQ,SA4DJ,UA3DM,KADF;AA/FR,SAAS,WAAW,SAAS,QAAQ,gBAAgB;AACrD,SAAS,YAAY,OAAO,YAAY,SAAS,aAAa;AAC9D,SAAS,aAAa;AACtB,SAAS,YAAY,cAAc,wBAAwB;AAE3D,SAAS,QAAQ,aAAa;AAC9B,SAAS,QAAQ,qBAAqB;AACtC,SAAS,gBAAgB;AACzB,SAAS,mBAAmB;AAS5B,MAAM,mBAAmB;AAIlB,SAAS,kBAAkB;AAAA,EAChC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAKG;AACD,QAAM,EAAE,MAAM,IAAI,SAAS;AAC3B,QAAM,CAAC,OAAO,QAAQ,IAAI,SAAsB,CAAC,CAAC;AAClD,QAAM,CAAC,GAAG,IAAI,IAAI,SAAS,EAAE;AAC7B,QAAM,CAAC,MAAM,OAAO,IAAI,SAAS,KAAK;AACtC,QAAM,CAAC,WAAW,YAAY,IAAI,SAAS,CAAC;AAC5C,QAAM,CAAC,SAAS,UAAU,IAAI,SAAS,KAAK;AAC5C,QAAM,WAAW,OAAyB,IAAI;AAE9C,YAAU,MAAM;AACd,QAAI,CAAC,KAAM;AAEX,QAAI,aAAa,GAAG;AAClB,YAAM,OAAO,MAAM,SAAS,iBAAiB,MAAM,CAAC;AACpD,WAAK;AACL,aAAO,iBAAiB,YAAY,IAAI;AACxC,aAAO,MAAM,OAAO,oBAAoB,YAAY,IAAI;AAAA,IAC1D;AACA,UAAM,KAAK,MAAM;AACjB,QAAI,CAAC,GAAI;AAET,UAAM,MAAM,YAAY;AACxB,UAAM,KAAK,MAAM,WAAW,IAAI,SAAS,QAAQ,OAAO,GAAG,QAAQ,aAAa,MAAM,GAAG,MAAM,MAAM,CAAC,CAAC;AACvG,WAAO;AAAA,MACL;AAAA,MACA,CAAC,SAAS;AACR,mBAAW,KAAK,KAAK,SAAS,GAAG;AACjC,iBAAS,KAAK,KAAK,MAAM,GAAG,GAAG,EAAE,IAAI,CAAC,OAAO,EAAE,IAAI,EAAE,IAAI,GAAI,EAAE,KAAK,EAA4B,EAAE,CAAC;AAAA,MACrG;AAAA;AAAA;AAAA,MAGA,CAAC,QAAQ,MAAM,EAAE,OAAO,mCAAmC,aAAa,IAAI,SAAS,MAAM,QAAQ,CAAC;AAAA,IACtG;AAAA,EACF,GAAG,CAAC,MAAM,QAAQ,OAAO,SAAS,CAAC;AAEnC,QAAM,WAAW,QAAQ,MAAM;AAC7B,UAAM,OAAO,EAAE,KAAK,EAAE,YAAY;AAClC,QAAI,CAAC,KAAM,QAAO;AAClB,WAAO,MAAM,OAAO,CAAC,MAAM,GAAG,EAAE,QAAQ,EAAE,IAAI,EAAE,OAAO,EAAE,GAAG,YAAY,EAAE,SAAS,IAAI,CAAC;AAAA,EAC1F,GAAG,CAAC,OAAO,CAAC,CAAC;AAEb,WAAS,KAAK,GAAc;AAC1B,aAAS,EAAE,KAAK,EAAE,KAAK,SAAS,EAAE,IAAI,KAAK,EAAE,IAAI,CAAC;AAClD,iBAAa,KAAK;AAAA,EACpB;AAEA,iBAAe,SAAS,OAAwB;AAC9C,UAAM,IAAI,QAAQ,CAAC;AACnB,QAAI,CAAC,EAAG;AACR,YAAQ,IAAI;AACZ,QAAI;AACF,YAAM,EAAE,KAAK,QAAQ,IAAI,MAAM,YAAY,QAAQ,CAAC;AACpD,eAAS,EAAE,KAAK,SAAS,KAAK,GAAG,CAAC;AAClC,mBAAa,KAAK;AAAA,IACpB,SAAS,KAAK;AACZ,YAAM,EAAE,OAAO,kBAAkB,aAAc,IAAc,SAAS,MAAM,QAAQ,CAAC;AAAA,IACvF,UAAE;AACA,cAAQ,KAAK;AAAA,IACf;AAAA,EACF;AAEA,SACE,oBAAC,UAAO,MAAY,cAClB,+BAAC,iBAAc,OAAM,qBAAoB,OAAM,oBAC7C;AAAA,yBAAC,SAAI,WAAU,gCACb;AAAA,0BAAC,SAAM,OAAO,GAAG,UAAU,CAAC,MAAM,KAAK,EAAE,OAAO,KAAK,GAAG,aAAY,6BAAuB,WAAU,OAAM;AAAA,MAC3G,oBAAC,UAAO,MAAK,UAAS,UAAU,MAAM,SAAS,MAAM,SAAS,SAAS,MAAM,GAC1E,iBAAO,mBAAc,iBACxB;AAAA,MACA,oBAAC,WAAM,KAAK,UAAU,MAAK,QAAO,QAAO,WAAU,QAAM,MAAC,UAAU,CAAC,MAAM;AAAE,aAAK,SAAS,EAAE,OAAO,KAAK;AAAG,UAAE,OAAO,QAAQ;AAAA,MAAI,GAAG;AAAA,OACtI;AAAA,IACA,qBAAC,SAAI,WAAU,gCACZ;AAAA,eAAS,WAAW,IACnB,oBAAC,OAAE,WAAU,2CACV,gBAAM,WAAW,IAAI,gDAA2C,oBACnE,IAEA,oBAAC,SAAI,WAAU,yCACZ,mBAAS,IAAI,CAAC,MACb;AAAA,QAAC;AAAA;AAAA,UAEC,MAAK;AAAA,UACL,SAAS,MAAM,KAAK,CAAC;AAAA,UACrB,OAAO,EAAE,OAAO,EAAE;AAAA,UAClB,WAAU;AAAA,UAGV,8BAAC,SAAI,KAAK,EAAE,KAAK,KAAK,EAAE,OAAO,EAAE,MAAM,WAAU,uEAAsE;AAAA;AAAA,QAPlH,EAAE;AAAA,MAQT,CACD,GACH;AAAA,MAED,WAAW,CAAC,EAAE,KAAK,KAClB,oBAAC,OAAE,WAAU,oBACX,8BAAC,UAAO,MAAK,UAAS,SAAQ,SAAQ,MAAK,MAAK,SAAS,MAAM,aAAa,CAAC,MAAM,IAAI,CAAC,GAAG,8BAE3F,GACF;AAAA,OAEJ;AAAA,KACF,GACF;AAEJ;AAMO,SAAS,kBAAkB;AAAA,EAChC;AAAA,EACA;AAAA,EACA,QAAQ;AAAA,EACR,OAAO;AAAA,EACP,UAAU;AACZ,GAMG;AACD,QAAM,CAAC,MAAM,OAAO,IAAI,SAAS,KAAK;AACtC,SACE,iCACE;AAAA,wBAAC,UAAO,MAAK,UAAS,SAAkB,MAAY,SAAS,MAAM,QAAQ,IAAI,GAAI,iBAAM;AAAA,IACzF,oBAAC,qBAAkB,QAAgB,MAAY,cAAc,SAAS,UAAoB;AAAA,KAC5F;AAEJ;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../../src/admin/media/MediaPicker.tsx"],"sourcesContent":["\"use client\";\n\nimport { useEffect, useMemo, useRef, useState } from \"react\";\nimport { collection, limit, onSnapshot, orderBy, query } from \"firebase/firestore\";\nimport { getDb } from \"../../firebase/client\";\nimport { DEMO_EVENT, isDemoActive, listDemoMediaAll } from \"../../demo/demoStore\";\nimport type { MediaItem } from \"../../content/types\";\nimport { Button, Input } from \"../ui/primitives\";\nimport { Dialog, DialogContent } from \"../ui/dialog\";\nimport { useToast } from \"../ui/toast\";\nimport { uploadImage } from \"../lib/upload\";\n\nexport interface PickedMedia {\n url: string;\n mediaId: string;\n alt?: string;\n}\n\n/** Paginagrootte: begrensd laden i.p.v. de hele bibliotheek bij elke kiezer-open. */\nconst PICKER_PAGE_SIZE = 48;\n\n/** Gecontroleerde mediabibliotheek-kiezer (open/onOpenChange extern) — zoek + klik om te kiezen, of\n * upload direct. Gebruikt door de knop hieronder én door de inline beeld-vervangen in de editor. */\nexport function MediaPickerDialog({\n siteId,\n open,\n onOpenChange,\n onSelect,\n}: {\n siteId: string;\n open: boolean;\n onOpenChange: (v: boolean) => void;\n onSelect: (m: PickedMedia) => void;\n}) {\n const { toast } = useToast();\n const [items, setItems] = useState<MediaItem[]>([]);\n const [q, setQ] = useState(\"\");\n const [busy, setBusy] = useState(false);\n const [pageCount, setPageCount] = useState(1);\n const [hasMore, setHasMore] = useState(false);\n const inputRef = useRef<HTMLInputElement>(null);\n\n useEffect(() => {\n if (!open) return;\n // Demo-modus: lokale bibliotheek (geseede foto's + eigen \"uploads\") i.p.v. Firestore.\n if (isDemoActive()) {\n const sync = () => setItems(listDemoMediaAll(siteId));\n sync();\n window.addEventListener(DEMO_EVENT, sync);\n return () => window.removeEventListener(DEMO_EVENT, sync);\n }\n const db = getDb();\n if (!db) return;\n // Begrensd laden (+\"meer laden\"): geen volledige collectie-read bij elke kiezer-open.\n const max = pageCount * PICKER_PAGE_SIZE;\n const qy = query(collection(db, \"sites\", siteId, \"media\"), orderBy(\"createdAt\", \"desc\"), limit(max + 1));\n return onSnapshot(\n qy,\n (snap) => {\n setHasMore(snap.docs.length > max);\n setItems(snap.docs.slice(0, max).map((d) => ({ id: d.id, ...(d.data() as Omit<MediaItem, \"id\">) })));\n },\n // Zonder onError worden lees-/rules-/App-Check-fouten stil geslikt → de kiezer toont dan\n // onterecht \"Nog geen afbeeldingen\". Nu een duidelijke melding.\n (err) => toast({ title: \"Kon mediabibliotheek niet laden\", description: err.message, tone: \"error\" }),\n );\n }, [open, siteId, toast, pageCount]);\n\n const filtered = useMemo(() => {\n const term = q.trim().toLowerCase();\n if (!term) return items;\n return items.filter((m) => `${m.name ?? \"\"} ${m.alt ?? \"\"}`.toLowerCase().includes(term));\n }, [items, q]);\n\n function pick(m: MediaItem) {\n onSelect({ url: m.url, mediaId: m.id, alt: m.alt });\n onOpenChange(false);\n }\n\n async function onUpload(files: FileList | null) {\n const f = files?.[0];\n if (!f) return;\n setBusy(true);\n try {\n const { url, mediaId } = await uploadImage(siteId, f);\n onSelect({ url, mediaId, alt: \"\" });\n onOpenChange(false);\n } catch (err) {\n toast({ title: \"Upload mislukt\", description: (err as Error).message, tone: \"error\" });\n } finally {\n setBusy(false);\n }\n }\n\n // Dicht = niet gemount. Dit scheelt niet alleen een onzichtbare mediabibliotheek in de boom\n // (met zijn abonnementen en state): een permanent gemounte dichte dialoog bleek ook het\n // aanknopingspunt voor een scroll-lock-lek — zie het commentaar in EditorProvider bij\n // `visualEditorModule`. Wat er niet staat, kan niets vergrendelen.\n if (!open) return null;\n\n return (\n <Dialog open={open} onOpenChange={onOpenChange}>\n <DialogContent title=\"Afbeelding kiezen\" width=\"min(92vw, 42rem)\">\n <div className=\"mb-3 flex items-center gap-2\">\n <Input value={q} onChange={(e) => setQ(e.target.value)} placeholder=\"Zoek in bibliotheek…\" className=\"h-9\" />\n <Button type=\"button\" disabled={busy} onClick={() => inputRef.current?.click()}>\n {busy ? \"Uploaden…\" : \"Upload nieuwe\"}\n </Button>\n <input ref={inputRef} type=\"file\" accept=\"image/*\" hidden onChange={(e) => { void onUpload(e.target.files); e.target.value = \"\"; }} />\n </div>\n <div className=\"max-h-[55vh] overflow-y-auto\">\n {filtered.length === 0 ? (\n <p className=\"py-8 text-center text-sm text-slate-500\">\n {items.length === 0 ? \"Nog geen afbeeldingen — upload er een.\" : \"Geen resultaten.\"}\n </p>\n ) : (\n <div className=\"grid grid-cols-3 gap-2 sm:grid-cols-4\">\n {filtered.map((m) => (\n <button\n key={m.id}\n type=\"button\"\n onClick={() => pick(m)}\n title={m.alt || m.name}\n className=\"group relative overflow-hidden rounded-md border border-slate-200 focus:outline-none focus:ring-2 focus:ring-indigo-500\"\n >\n {/* eslint-disable-next-line @next/next/no-img-element */}\n <img src={m.url} alt={m.alt ?? m.name} className=\"aspect-square w-full object-cover transition group-hover:opacity-80\" />\n </button>\n ))}\n </div>\n )}\n {hasMore && !q.trim() && (\n <p className=\"mt-2 text-center\">\n <Button type=\"button\" variant=\"ghost\" size=\"sm\" onClick={() => setPageCount((c) => c + 1)}>\n Meer laden…\n </Button>\n </p>\n )}\n </div>\n </DialogContent>\n </Dialog>\n );\n}\n\n/**\n * Knop die de mediabibliotheek-kiezer opent: zoek + klik om een bestaande afbeelding te kiezen, of\n * upload er direct een nieuwe in. Vervangt het plakken van losse URL's.\n */\nexport function MediaPickerButton({\n siteId,\n onSelect,\n label = \"Kies / upload\",\n size = \"sm\",\n variant = \"secondary\",\n}: {\n siteId: string;\n onSelect: (m: PickedMedia) => void;\n label?: string;\n size?: \"sm\" | \"md\";\n variant?: \"primary\" | \"secondary\" | \"ghost\";\n}) {\n const [open, setOpen] = useState(false);\n return (\n <>\n <Button type=\"button\" variant={variant} size={size} onClick={() => setOpen(true)}>{label}</Button>\n <MediaPickerDialog siteId={siteId} open={open} onOpenChange={setOpen} onSelect={onSelect} />\n </>\n );\n}\n"],"mappings":"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","names":[]}
|
|
@@ -24,7 +24,7 @@ function PatternInserter({
|
|
|
24
24
|
}, [patterns, registry]);
|
|
25
25
|
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
26
26
|
/* @__PURE__ */ jsx(Button, { type: "button", variant: "secondary", size: "sm", onClick: () => setOpen(true), children: label }),
|
|
27
|
-
/* @__PURE__ */ jsx(Dialog, { open, onOpenChange: setOpen, children: /* @__PURE__ */ jsxs(DialogContent, { title: "Sectie toevoegen", width: "min(92vw, 36rem)", children: [
|
|
27
|
+
open && /* @__PURE__ */ jsx(Dialog, { open, onOpenChange: setOpen, children: /* @__PURE__ */ jsxs(DialogContent, { title: "Sectie toevoegen", width: "min(92vw, 36rem)", children: [
|
|
28
28
|
/* @__PURE__ */ jsx("p", { className: "mb-3 text-sm text-slate-500", children: "Kies een kant-en-klare sectie; je past de inhoud daarna aan." }),
|
|
29
29
|
/* @__PURE__ */ jsx("div", { className: "grid max-h-[60vh] gap-4 overflow-auto", children: byCategory.map(([cat, items]) => /* @__PURE__ */ jsxs("div", { children: [
|
|
30
30
|
/* @__PURE__ */ jsx("h4", { className: "mb-1 text-xs font-semibold uppercase tracking-wide text-slate-400", children: cat }),
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/admin/patterns/PatternInserter.tsx"],"sourcesContent":["\"use client\";\n\nimport { useMemo, useState } from \"react\";\nimport { Dialog, DialogContent, DialogClose } from \"../ui/dialog\";\nimport { Button } from \"../ui/primitives\";\nimport { useRegistry } from \"../../blocks/context\";\nimport { builtinPatterns, instantiatePattern, collectPatternTypes, type Pattern } from \"../../patterns\";\n\n/**\n * \"Sectie toevoegen\" — kiest een kant-en-klaar patroon en voegt de blocks (met verse keys) in.\n * Hergebruikt in de visuele editor én het /beheer-paginapaneel.\n */\nexport function PatternInserter({\n patterns = builtinPatterns,\n onInsert,\n label = \"+ Sectie toevoegen\",\n}: {\n patterns?: Pattern[];\n onInsert: (blocks: Array<Record<string, unknown>>) => void;\n label?: string;\n}) {\n const [open, setOpen] = useState(false);\n const registry = useRegistry();\n const byCategory = useMemo(() => {\n // Verberg patronen waarvan een benodigd blok niet in de registry zit (module uitgeschakeld):\n // invoegen zou anders \"Onbekend blok\"-gaten geven.\n const available = patterns.filter((p) => collectPatternTypes(p).every((t) => registry.get(t)));\n const map = new Map<string, Pattern[]>();\n for (const p of available) {\n const arr = map.get(p.category) ?? [];\n arr.push(p);\n map.set(p.category, arr);\n }\n return [...map.entries()];\n }, [patterns, registry]);\n\n return (\n <>\n <Button type=\"button\" variant=\"secondary\" size=\"sm\" onClick={() => setOpen(true)}>\n {label}\n </Button>\n <Dialog open={open} onOpenChange={setOpen}>\n <DialogContent title=\"Sectie toevoegen\" width=\"min(92vw, 36rem)\">\n <p className=\"mb-3 text-sm text-slate-500\">Kies een kant-en-klare sectie; je past de inhoud daarna aan.</p>\n <div className=\"grid max-h-[60vh] gap-4 overflow-auto\">\n {byCategory.map(([cat, items]) => (\n <div key={cat}>\n <h4 className=\"mb-1 text-xs font-semibold uppercase tracking-wide text-slate-400\">{cat}</h4>\n <div className=\"grid gap-2\">\n {items.map((p) => (\n <button\n key={p.id}\n type=\"button\"\n className=\"rounded-lg border border-slate-200 p-3 text-left text-sm hover:border-brand-500 hover:bg-slate-50\"\n onClick={() => {\n onInsert(instantiatePattern(p));\n setOpen(false);\n }}\n >\n <span className=\"flex items-center justify-between gap-2\">\n <span className=\"font-medium\">{p.label}</span>\n {/* Mini-preview: de iconen van de blokken die dit patroon invoegt. */}\n <span className=\"shrink-0 text-base opacity-70\" aria-hidden>\n {p.blocks.slice(0, 6).map((b, idx) => (\n <span key={idx}>{registry.get(b._type)?.icon ?? \"🧩\"}</span>\n ))}\n </span>\n </span>\n {p.description && <span className=\"block text-xs text-slate-500\">{p.description}</span>}\n </button>\n ))}\n </div>\n </div>\n ))}\n </div>\n <div className=\"mt-4 flex justify-end\">\n <DialogClose asChild>\n <Button variant=\"secondary\" size=\"sm\">Sluiten</Button>\n </DialogClose>\n </div>\n </DialogContent>\n </Dialog>\n </>\n );\n}\n"],"mappings":";AAqCI,mBACE,
|
|
1
|
+
{"version":3,"sources":["../../../src/admin/patterns/PatternInserter.tsx"],"sourcesContent":["\"use client\";\n\nimport { useMemo, useState } from \"react\";\nimport { Dialog, DialogContent, DialogClose } from \"../ui/dialog\";\nimport { Button } from \"../ui/primitives\";\nimport { useRegistry } from \"../../blocks/context\";\nimport { builtinPatterns, instantiatePattern, collectPatternTypes, type Pattern } from \"../../patterns\";\n\n/**\n * \"Sectie toevoegen\" — kiest een kant-en-klaar patroon en voegt de blocks (met verse keys) in.\n * Hergebruikt in de visuele editor én het /beheer-paginapaneel.\n */\nexport function PatternInserter({\n patterns = builtinPatterns,\n onInsert,\n label = \"+ Sectie toevoegen\",\n}: {\n patterns?: Pattern[];\n onInsert: (blocks: Array<Record<string, unknown>>) => void;\n label?: string;\n}) {\n const [open, setOpen] = useState(false);\n const registry = useRegistry();\n const byCategory = useMemo(() => {\n // Verberg patronen waarvan een benodigd blok niet in de registry zit (module uitgeschakeld):\n // invoegen zou anders \"Onbekend blok\"-gaten geven.\n const available = patterns.filter((p) => collectPatternTypes(p).every((t) => registry.get(t)));\n const map = new Map<string, Pattern[]>();\n for (const p of available) {\n const arr = map.get(p.category) ?? [];\n arr.push(p);\n map.set(p.category, arr);\n }\n return [...map.entries()];\n }, [patterns, registry]);\n\n return (\n <>\n <Button type=\"button\" variant=\"secondary\" size=\"sm\" onClick={() => setOpen(true)}>\n {label}\n </Button>\n {/* Dicht = niet gemount; zelfde overweging als bij de mediakiezer (zie MediaPicker). */}\n {open && (\n <Dialog open={open} onOpenChange={setOpen}>\n <DialogContent title=\"Sectie toevoegen\" width=\"min(92vw, 36rem)\">\n <p className=\"mb-3 text-sm text-slate-500\">Kies een kant-en-klare sectie; je past de inhoud daarna aan.</p>\n <div className=\"grid max-h-[60vh] gap-4 overflow-auto\">\n {byCategory.map(([cat, items]) => (\n <div key={cat}>\n <h4 className=\"mb-1 text-xs font-semibold uppercase tracking-wide text-slate-400\">{cat}</h4>\n <div className=\"grid gap-2\">\n {items.map((p) => (\n <button\n key={p.id}\n type=\"button\"\n className=\"rounded-lg border border-slate-200 p-3 text-left text-sm hover:border-brand-500 hover:bg-slate-50\"\n onClick={() => {\n onInsert(instantiatePattern(p));\n setOpen(false);\n }}\n >\n <span className=\"flex items-center justify-between gap-2\">\n <span className=\"font-medium\">{p.label}</span>\n {/* Mini-preview: de iconen van de blokken die dit patroon invoegt. */}\n <span className=\"shrink-0 text-base opacity-70\" aria-hidden>\n {p.blocks.slice(0, 6).map((b, idx) => (\n <span key={idx}>{registry.get(b._type)?.icon ?? \"🧩\"}</span>\n ))}\n </span>\n </span>\n {p.description && <span className=\"block text-xs text-slate-500\">{p.description}</span>}\n </button>\n ))}\n </div>\n </div>\n ))}\n </div>\n <div className=\"mt-4 flex justify-end\">\n <DialogClose asChild>\n <Button variant=\"secondary\" size=\"sm\">Sluiten</Button>\n </DialogClose>\n </div>\n </DialogContent>\n </Dialog>\n )}\n </>\n );\n}\n"],"mappings":";AAqCI,mBACE,KAuBgB,YAxBlB;AAnCJ,SAAS,SAAS,gBAAgB;AAClC,SAAS,QAAQ,eAAe,mBAAmB;AACnD,SAAS,cAAc;AACvB,SAAS,mBAAmB;AAC5B,SAAS,iBAAiB,oBAAoB,2BAAyC;AAMhF,SAAS,gBAAgB;AAAA,EAC9B,WAAW;AAAA,EACX;AAAA,EACA,QAAQ;AACV,GAIG;AACD,QAAM,CAAC,MAAM,OAAO,IAAI,SAAS,KAAK;AACtC,QAAM,WAAW,YAAY;AAC7B,QAAM,aAAa,QAAQ,MAAM;AAG/B,UAAM,YAAY,SAAS,OAAO,CAAC,MAAM,oBAAoB,CAAC,EAAE,MAAM,CAAC,MAAM,SAAS,IAAI,CAAC,CAAC,CAAC;AAC7F,UAAM,MAAM,oBAAI,IAAuB;AACvC,eAAW,KAAK,WAAW;AACzB,YAAM,MAAM,IAAI,IAAI,EAAE,QAAQ,KAAK,CAAC;AACpC,UAAI,KAAK,CAAC;AACV,UAAI,IAAI,EAAE,UAAU,GAAG;AAAA,IACzB;AACA,WAAO,CAAC,GAAG,IAAI,QAAQ,CAAC;AAAA,EAC1B,GAAG,CAAC,UAAU,QAAQ,CAAC;AAEvB,SACE,iCACE;AAAA,wBAAC,UAAO,MAAK,UAAS,SAAQ,aAAY,MAAK,MAAK,SAAS,MAAM,QAAQ,IAAI,GAC5E,iBACH;AAAA,IAEC,QACD,oBAAC,UAAO,MAAY,cAAc,SAChC,+BAAC,iBAAc,OAAM,oBAAmB,OAAM,oBAC5C;AAAA,0BAAC,OAAE,WAAU,+BAA8B,0EAA4D;AAAA,MACvG,oBAAC,SAAI,WAAU,yCACZ,qBAAW,IAAI,CAAC,CAAC,KAAK,KAAK,MAC1B,qBAAC,SACC;AAAA,4BAAC,QAAG,WAAU,qEAAqE,eAAI;AAAA,QACvF,oBAAC,SAAI,WAAU,cACZ,gBAAM,IAAI,CAAC,MACV;AAAA,UAAC;AAAA;AAAA,YAEC,MAAK;AAAA,YACL,WAAU;AAAA,YACV,SAAS,MAAM;AACb,uBAAS,mBAAmB,CAAC,CAAC;AAC9B,sBAAQ,KAAK;AAAA,YACf;AAAA,YAEA;AAAA,mCAAC,UAAK,WAAU,2CACd;AAAA,oCAAC,UAAK,WAAU,eAAe,YAAE,OAAM;AAAA,gBAEvC,oBAAC,UAAK,WAAU,iCAAgC,eAAW,MACxD,YAAE,OAAO,MAAM,GAAG,CAAC,EAAE,IAAI,CAAC,GAAG,QAC5B,oBAAC,UAAgB,mBAAS,IAAI,EAAE,KAAK,GAAG,QAAQ,eAArC,GAA0C,CACtD,GACH;AAAA,iBACF;AAAA,cACC,EAAE,eAAe,oBAAC,UAAK,WAAU,gCAAgC,YAAE,aAAY;AAAA;AAAA;AAAA,UAjB3E,EAAE;AAAA,QAkBT,CACD,GACH;AAAA,WAzBQ,GA0BV,CACD,GACH;AAAA,MACA,oBAAC,SAAI,WAAU,yBACb,8BAAC,eAAY,SAAO,MAClB,8BAAC,UAAO,SAAQ,aAAY,MAAK,MAAK,qBAAO,GAC/C,GACF;AAAA,OACF,GACF;AAAA,KAEF;AAEJ;","names":[]}
|
|
@@ -7,8 +7,6 @@ import { useRegistry } from "../../blocks/context.js";
|
|
|
7
7
|
import { introspectToFields } from "../lib/zodFields.js";
|
|
8
8
|
import { BlockErrorBoundary } from "../../blocks/BlockErrorBoundary.js";
|
|
9
9
|
import { MediaPickerDialog } from "../media/MediaPicker.js";
|
|
10
|
-
import { useFocusTrap, useScrollLock } from "@astryxdesign/core/hooks";
|
|
11
|
-
import { useNestedDialogOpen } from "../lib/useNestedDialog.js";
|
|
12
10
|
import { useThemeVars } from "../../XiCMSProvider.js";
|
|
13
11
|
import { buildBlockAddOptions } from "../../blocks/registry.js";
|
|
14
12
|
import DOMPurify from "dompurify";
|
|
@@ -111,14 +109,12 @@ function VisualEditorInner({ onClose }) {
|
|
|
111
109
|
const [liveBlocks, setLiveBlocks] = useState(void 0);
|
|
112
110
|
useBalkOffset(true);
|
|
113
111
|
const { vars: themeVars, darkAuto } = useThemeVars();
|
|
114
|
-
const
|
|
115
|
-
const { containerRef, focusFirst } = useFocusTrap({ isActive: !nestedDialogOpen });
|
|
116
|
-
useScrollLock(true);
|
|
112
|
+
const containerRef = useRef(null);
|
|
117
113
|
useEffect(() => {
|
|
118
114
|
const opener = document.activeElement;
|
|
119
|
-
|
|
115
|
+
containerRef.current?.querySelector("button, [href], input, select, textarea, [tabindex]:not([tabindex='-1'])")?.focus();
|
|
120
116
|
return () => opener?.focus?.();
|
|
121
|
-
}, [
|
|
117
|
+
}, []);
|
|
122
118
|
const [dragIndex, setDragIndex] = useState(null);
|
|
123
119
|
const [dropIndex, setDropIndex] = useState(null);
|
|
124
120
|
const draggingRef = useRef(false);
|
|
@@ -381,7 +377,18 @@ function VisualEditorInner({ onClose }) {
|
|
|
381
377
|
for (const el of Array.from(document.querySelectorAll("[data-xicms-block]"))) {
|
|
382
378
|
const key = el.dataset.xicmsBlock;
|
|
383
379
|
if (!key) continue;
|
|
384
|
-
|
|
380
|
+
let r = el.getBoundingClientRect();
|
|
381
|
+
if (r.width === 0 && r.height === 0 && el.children.length > 0) {
|
|
382
|
+
let top = Infinity, right = -Infinity, bottom = -Infinity;
|
|
383
|
+
for (const kind of Array.from(el.children)) {
|
|
384
|
+
const kr = kind.getBoundingClientRect();
|
|
385
|
+
if (kr.width === 0 && kr.height === 0) continue;
|
|
386
|
+
top = Math.min(top, kr.top);
|
|
387
|
+
right = Math.max(right, kr.right);
|
|
388
|
+
bottom = Math.max(bottom, kr.bottom);
|
|
389
|
+
}
|
|
390
|
+
if (Number.isFinite(top)) r = { top, right, bottom, width: right, height: bottom - top };
|
|
391
|
+
}
|
|
385
392
|
if (r.width === 0 && r.height === 0) continue;
|
|
386
393
|
if (r.bottom < veilig || r.top > window.innerHeight) continue;
|
|
387
394
|
const type = el.dataset.xicmsBlockType ?? "";
|
|
@@ -534,6 +541,12 @@ function VisualEditorInner({ onClose }) {
|
|
|
534
541
|
el.classList.remove("is-editing");
|
|
535
542
|
if (editingRef.current === el) editingRef.current = null;
|
|
536
543
|
}, []);
|
|
544
|
+
const meldGeenTekst = useCallback(() => {
|
|
545
|
+
toast({
|
|
546
|
+
title: "Dit blok heeft geen eigen tekst",
|
|
547
|
+
description: "Wijs een product aan voor zijn naam, of gebruik het tandwiel voor de instellingen van dit blok."
|
|
548
|
+
});
|
|
549
|
+
}, [toast]);
|
|
537
550
|
const startInline = useCallback((index) => {
|
|
538
551
|
const target = targetsRef.current[index];
|
|
539
552
|
if (!target) return;
|
|
@@ -628,12 +641,17 @@ function VisualEditorInner({ onClose }) {
|
|
|
628
641
|
apply();
|
|
629
642
|
if (wilInlineRef.current) {
|
|
630
643
|
wilInlineRef.current = false;
|
|
631
|
-
if (targetsRef.current.length > 0)
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
644
|
+
if (targetsRef.current.length > 0) {
|
|
645
|
+
startInline(0);
|
|
646
|
+
} else {
|
|
647
|
+
let pogingen = 0;
|
|
648
|
+
const opnieuw = () => {
|
|
649
|
+
apply();
|
|
650
|
+
if (targetsRef.current.length > 0) startInline(0);
|
|
651
|
+
else if (++pogingen < 3) requestAnimationFrame(opnieuw);
|
|
652
|
+
else meldGeenTekst();
|
|
653
|
+
};
|
|
654
|
+
requestAnimationFrame(opnieuw);
|
|
637
655
|
}
|
|
638
656
|
}
|
|
639
657
|
const obs = new MutationObserver(() => {
|
|
@@ -703,7 +721,10 @@ function VisualEditorInner({ onClose }) {
|
|
|
703
721
|
const t2 = e.target;
|
|
704
722
|
const inField2 = !!t2 && (t2.isContentEditable || t2.tagName === "INPUT" || t2.tagName === "TEXTAREA" || t2.tagName === "SELECT");
|
|
705
723
|
const dialogOpen = document.querySelector('dialog[open], [role="dialog"][data-state="open"], [role="alertdialog"][data-state="open"]');
|
|
706
|
-
if (!inField2 && !dialogOpen)
|
|
724
|
+
if (!inField2 && !dialogOpen) {
|
|
725
|
+
if (document.querySelector(".xicms-ve__panel")) setPanelOpen(false);
|
|
726
|
+
else handleClose();
|
|
727
|
+
}
|
|
707
728
|
return;
|
|
708
729
|
}
|
|
709
730
|
const t = e.target;
|
|
@@ -776,6 +797,30 @@ function VisualEditorInner({ onClose }) {
|
|
|
776
797
|
setBlocks(blocks.filter((_, idx) => idx !== i));
|
|
777
798
|
if (selectedKey === removed._key) setSelectedKey(null);
|
|
778
799
|
}
|
|
800
|
+
const klaarMetBlok = useCallback(() => {
|
|
801
|
+
document.activeElement?.blur();
|
|
802
|
+
setSelectedKey(null);
|
|
803
|
+
setPanelOpen(false);
|
|
804
|
+
void flushSave().then(() => toast({ title: "Opgeslagen als werkversie", description: "Publiceer (rechtsonder) om het live te zetten.", tone: "success" })).catch(() => setStatus("error"));
|
|
805
|
+
}, [flushSave, toast]);
|
|
806
|
+
async function herstelBlok(i) {
|
|
807
|
+
const key = blocks[i]?._key;
|
|
808
|
+
if (!key) return;
|
|
809
|
+
const liveBlok = (liveBlocks ?? []).find((b) => String(b._key) === key);
|
|
810
|
+
if (!liveBlok) {
|
|
811
|
+
await remove(i);
|
|
812
|
+
return;
|
|
813
|
+
}
|
|
814
|
+
const ok = await confirm({
|
|
815
|
+
title: "Wijzigingen van dit blok verwerpen?",
|
|
816
|
+
description: "Het blok wordt teruggezet naar de versie die nu live staat. Met Ongedaan maken (Ctrl+Z) kun je hier nog op terugkomen.",
|
|
817
|
+
confirmLabel: "Verwerpen",
|
|
818
|
+
tone: "danger"
|
|
819
|
+
});
|
|
820
|
+
if (!ok) return;
|
|
821
|
+
setBlocks(blocks.map((b, idx) => idx === i ? structuredClone(liveBlok) : b));
|
|
822
|
+
toast({ title: "Blok teruggezet", description: "Dit blok is weer gelijk aan de live versie.", tone: "success" });
|
|
823
|
+
}
|
|
779
824
|
function addBlock(type) {
|
|
780
825
|
const def = registry.get(type);
|
|
781
826
|
const base = def?.defaults?.() ?? {};
|
|
@@ -1012,6 +1057,11 @@ function VisualEditorInner({ onClose }) {
|
|
|
1012
1057
|
"aria-label": `Tekst van ${b.label} bewerken`,
|
|
1013
1058
|
onMouseDown: (e) => e.preventDefault(),
|
|
1014
1059
|
onClick: () => {
|
|
1060
|
+
if (b.key === selectedKey) {
|
|
1061
|
+
if (targetsRef.current.length > 0) startInline(0);
|
|
1062
|
+
else meldGeenTekst();
|
|
1063
|
+
return;
|
|
1064
|
+
}
|
|
1015
1065
|
wilInlineRef.current = true;
|
|
1016
1066
|
setSelectedProduct(null);
|
|
1017
1067
|
setSelectedKey(b.key);
|
|
@@ -1109,6 +1159,33 @@ function VisualEditorInner({ onClose }) {
|
|
|
1109
1159
|
onClick: () => void remove(i),
|
|
1110
1160
|
children: /* @__PURE__ */ jsx("svg", { viewBox: "0 0 24 24", width: "13", height: "13", fill: "none", stroke: "currentColor", strokeWidth: "1.6", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: /* @__PURE__ */ jsx("path", { d: "M3 6h18M8 6V4a1 1 0 0 1 1-1h6a1 1 0 0 1 1 1v2M6 6l1 14a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2l1-14" }) })
|
|
1111
1161
|
}
|
|
1162
|
+
),
|
|
1163
|
+
(wijzigingen.velden.has(b.key) || wijzigingen.blokken.has(b.key)) && /* @__PURE__ */ jsx(
|
|
1164
|
+
"button",
|
|
1165
|
+
{
|
|
1166
|
+
type: "button",
|
|
1167
|
+
className: "xicms-ve-blokknop",
|
|
1168
|
+
title: "Wijzigingen van dit blok verwerpen (terug naar live)",
|
|
1169
|
+
"aria-label": `Wijzigingen van ${b.label} verwerpen`,
|
|
1170
|
+
onMouseDown: (e) => e.preventDefault(),
|
|
1171
|
+
onClick: () => void herstelBlok(i),
|
|
1172
|
+
children: /* @__PURE__ */ jsxs("svg", { viewBox: "0 0 24 24", width: "13", height: "13", fill: "none", stroke: "currentColor", strokeWidth: "1.6", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: [
|
|
1173
|
+
/* @__PURE__ */ jsx("path", { d: "M3 7v6h6" }),
|
|
1174
|
+
/* @__PURE__ */ jsx("path", { d: "M3 13a9 9 0 1 0 3-7.7L3 7" })
|
|
1175
|
+
] })
|
|
1176
|
+
}
|
|
1177
|
+
),
|
|
1178
|
+
b.key === selectedKey && /* @__PURE__ */ jsx(
|
|
1179
|
+
"button",
|
|
1180
|
+
{
|
|
1181
|
+
type: "button",
|
|
1182
|
+
className: "xicms-ve-blokknop xicms-ve-blokknop--klaar",
|
|
1183
|
+
title: "Klaar met dit blok (opslaan als werkversie)",
|
|
1184
|
+
"aria-label": `Klaar met ${b.label}`,
|
|
1185
|
+
onMouseDown: (e) => e.preventDefault(),
|
|
1186
|
+
onClick: klaarMetBlok,
|
|
1187
|
+
children: /* @__PURE__ */ jsx("svg", { viewBox: "0 0 24 24", width: "13", height: "13", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: /* @__PURE__ */ jsx("path", { d: "M20 6L9 17l-5-5" }) })
|
|
1188
|
+
}
|
|
1112
1189
|
)
|
|
1113
1190
|
] }) })
|
|
1114
1191
|
] }, b.key);
|