@drawnagency/primitives 0.1.0 → 0.2.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.
Files changed (139) hide show
  1. package/dist/auth/index.js +26 -3
  2. package/dist/chunk-2VTPWODA.js +60 -0
  3. package/dist/chunk-CS7F6IOY.js +39 -0
  4. package/dist/chunk-HOJAF4VD.js +264 -0
  5. package/dist/chunk-IP6ODLXX.js +341 -0
  6. package/dist/chunk-T4BJ6RSB.js +58 -0
  7. package/dist/chunk-UKEVUCIZ.js +200 -0
  8. package/dist/chunk-UMSFICAC.js +36 -0
  9. package/dist/index.js +156 -4
  10. package/dist/lib/index.js +62 -12
  11. package/dist/lib/sanitize.d.ts.map +1 -1
  12. package/dist/media/index.js +36 -9
  13. package/dist/schemas/index.js +52 -7
  14. package/package.json +5 -4
  15. package/src/lib/sanitize.ts +6 -2
  16. package/dist/auth/cookies.js +0 -44
  17. package/dist/auth/errors.js +0 -10
  18. package/dist/auth/security.js +0 -48
  19. package/dist/auth/types.js +0 -1
  20. package/dist/components/brandguide/ColorSwatchSettings.js +0 -10
  21. package/dist/components/brandguide/Colors.js +0 -79
  22. package/dist/components/brandguide/DoDontList.js +0 -22
  23. package/dist/components/brandguide/DoDontMediaGrid.js +0 -5
  24. package/dist/components/editor/AudiencePicker.js +0 -24
  25. package/dist/components/editor/DeleteButton.js +0 -6
  26. package/dist/components/editor/DragHandle.js +0 -8
  27. package/dist/components/editor/InsertButton.js +0 -7
  28. package/dist/components/editor/SectionWrapper.js +0 -135
  29. package/dist/components/editor/SettingsButton.js +0 -6
  30. package/dist/components/editor/SettingsForm.js +0 -64
  31. package/dist/components/editor/StatusBadge.js +0 -10
  32. package/dist/components/editor/StatusPicker.js +0 -30
  33. package/dist/components/editor/index.js +0 -7
  34. package/dist/components/primitives/CustomParagraph.js +0 -24
  35. package/dist/components/primitives/EditableGrid.js +0 -90
  36. package/dist/components/primitives/EditableList.js +0 -54
  37. package/dist/components/primitives/EditablePlainText.js +0 -52
  38. package/dist/components/primitives/EditableRichText.js +0 -86
  39. package/dist/components/primitives/HeadingSection.js +0 -7
  40. package/dist/components/primitives/IconPicker.js +0 -21
  41. package/dist/components/primitives/LinkPopover.js +0 -48
  42. package/dist/components/primitives/MediaSettingsForms.js +0 -42
  43. package/dist/components/primitives/ResolvedMedia.js +0 -9
  44. package/dist/components/primitives/RichTextToolbar.js +0 -26
  45. package/dist/components/primitives/tiptap-presets.js +0 -44
  46. package/dist/components/primitives/useEditableCollection.js +0 -61
  47. package/dist/components/primitives/useEditablePlainText.js +0 -27
  48. package/dist/components/primitives/useEditableRichText.js +0 -52
  49. package/dist/components/sections/Button/CTAButton.js +0 -18
  50. package/dist/components/sections/Button/index.js +0 -28
  51. package/dist/components/sections/Colors/index.js +0 -34
  52. package/dist/components/sections/DoDontList/index.js +0 -33
  53. package/dist/components/sections/DoDontMediaGrid/index.js +0 -41
  54. package/dist/components/sections/IconList/IconList.js +0 -131
  55. package/dist/components/sections/IconList/IconListSettings.js +0 -22
  56. package/dist/components/sections/IconList/index.js +0 -27
  57. package/dist/components/sections/LinkHeading/index.js +0 -15
  58. package/dist/components/sections/MediaGrid/MediaGrid.js +0 -62
  59. package/dist/components/sections/MediaGrid/index.js +0 -35
  60. package/dist/components/sections/Prose/Prose.js +0 -11
  61. package/dist/components/sections/Prose/index.js +0 -15
  62. package/dist/components/sections/SectionLayout.js +0 -15
  63. package/dist/components/sections/SplitContent/SplitContent.js +0 -31
  64. package/dist/components/sections/SplitContent/SplitContentSettings.js +0 -17
  65. package/dist/components/sections/SplitContent/index.js +0 -27
  66. package/dist/components/sections/SubHeading/index.js +0 -18
  67. package/dist/components/sections/SubSubHeading/index.js +0 -18
  68. package/dist/components/sections/ViewRenderer.js +0 -13
  69. package/dist/components/sections/register-schemas.js +0 -15
  70. package/dist/components/sections/register.js +0 -15
  71. package/dist/components/shared/Button.js +0 -27
  72. package/dist/components/shared/Checkbox.js +0 -10
  73. package/dist/components/shared/ColorPicker.js +0 -5
  74. package/dist/components/shared/ErrorBoundary.js +0 -30
  75. package/dist/components/shared/FontPicker.js +0 -190
  76. package/dist/components/shared/FormLabel.js +0 -5
  77. package/dist/components/shared/IconButton.js +0 -16
  78. package/dist/components/shared/Input.js +0 -8
  79. package/dist/components/shared/Navigation.js +0 -71
  80. package/dist/components/shared/PasswordInput.js +0 -11
  81. package/dist/components/shared/Popover.js +0 -33
  82. package/dist/components/shared/PopoverItem.js +0 -6
  83. package/dist/components/shared/Select.js +0 -9
  84. package/dist/components/shared/Textarea.js +0 -8
  85. package/dist/components/shared/Toggle.js +0 -5
  86. package/dist/components/shared/Tooltip.js +0 -8
  87. package/dist/components/shared/icons.js +0 -23
  88. package/dist/components/shell/AudienceAddForm.js +0 -43
  89. package/dist/components/shell/AudienceRow.js +0 -74
  90. package/dist/components/shell/EditorContext.js +0 -24
  91. package/dist/components/shell/EditorLoginForm.js +0 -46
  92. package/dist/components/shell/EditorModal.js +0 -43
  93. package/dist/components/shell/EditorModalContext.js +0 -20
  94. package/dist/components/shell/EditorShell.js +0 -483
  95. package/dist/components/shell/MediaLibraryContext.js +0 -5
  96. package/dist/components/shell/MediaLibraryModal.js +0 -145
  97. package/dist/components/shell/ProcessingIndicator.js +0 -15
  98. package/dist/components/shell/SectionSkeleton.js +0 -22
  99. package/dist/components/shell/SectionTypePicker.js +0 -15
  100. package/dist/components/shell/SiteSettingsDisplay.js +0 -28
  101. package/dist/components/shell/SiteSettingsModal.js +0 -40
  102. package/dist/components/shell/SiteSettingsUsers.js +0 -87
  103. package/dist/components/shell/SiteSettingsViewerAccess.js +0 -94
  104. package/dist/components/shell/ViewerLoginForm.js +0 -40
  105. package/dist/data/google-fonts.json +0 -7718
  106. package/dist/hooks/index.js +0 -6
  107. package/dist/hooks/useActiveHeadings.js +0 -99
  108. package/dist/hooks/useEditorPersistence.js +0 -73
  109. package/dist/hooks/useEditorPublish.js +0 -145
  110. package/dist/hooks/useFocusTrap.js +0 -51
  111. package/dist/hooks/useMediaPipeline.js +0 -253
  112. package/dist/hooks/useResolvedMedia.js +0 -39
  113. package/dist/lib/cn.js +0 -5
  114. package/dist/lib/contrast.js +0 -11
  115. package/dist/lib/dexie.js +0 -236
  116. package/dist/lib/events.js +0 -15
  117. package/dist/lib/google-fonts.js +0 -11
  118. package/dist/lib/grid.js +0 -7
  119. package/dist/lib/icons.js +0 -27
  120. package/dist/lib/loader.js +0 -57
  121. package/dist/lib/nav.js +0 -58
  122. package/dist/lib/registry.js +0 -64
  123. package/dist/lib/safeRedirect.js +0 -11
  124. package/dist/lib/sanitize.js +0 -6
  125. package/dist/lib/timestamp.js +0 -28
  126. package/dist/media/github.js +0 -60
  127. package/dist/media/queue.js +0 -116
  128. package/dist/media/resolve.js +0 -50
  129. package/dist/media/types.js +0 -1
  130. package/dist/media/utils.js +0 -41
  131. package/dist/media/videoPoster.js +0 -44
  132. package/dist/media/worker.js +0 -73
  133. package/dist/schemas/audience.js +0 -19
  134. package/dist/schemas/auth.js +0 -22
  135. package/dist/schemas/media-grid-options.js +0 -7
  136. package/dist/schemas/media.js +0 -28
  137. package/dist/schemas/sections.js +0 -12
  138. package/dist/schemas/shared.js +0 -71
  139. package/dist/schemas/site-config.js +0 -26
@@ -1,7 +0,0 @@
1
- import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { AddIcon } from "../shared/icons";
3
- import { IconButton } from "../shared/IconButton";
4
- import { Tooltip, Kbd } from "../shared/Tooltip";
5
- export function InsertButton({ index, onInsert }) {
6
- return (_jsx(Tooltip, { content: _jsxs(_Fragment, { children: [_jsx(Kbd, { children: "Click" }), " to add below", _jsx("br", {}), _jsx(Kbd, { children: "Alt+Click" }), " to add above"] }), children: _jsx(IconButton, { icon: _jsx(AddIcon, { size: 18 }), label: "Add section", className: "pointer-events-auto rounded-md text-base-contrast-light/80 hover:bg-base-contrast-light/10 hover:text-base-contrast", tabIndex: -1, onClick: (e) => onInsert(e.altKey ? index : index + 1) }) }));
7
- }
@@ -1,135 +0,0 @@
1
- import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
- import { useState, useRef, useEffect } from "react";
3
- import { draggable, dropTargetForElements, } from "@atlaskit/pragmatic-drag-and-drop/element/adapter";
4
- import { attachClosestEdge, extractClosestEdge, } from "@atlaskit/pragmatic-drag-and-drop-hitbox/closest-edge";
5
- import { DragHandle } from "./DragHandle";
6
- import { InsertButton } from "./InsertButton";
7
- import { DeleteButton } from "./DeleteButton";
8
- import { SettingsButton } from "./SettingsButton";
9
- import { StatusBadge } from "./StatusBadge";
10
- import { StatusPicker } from "./StatusPicker";
11
- import { AudiencePicker } from "./AudiencePicker";
12
- import { SettingsForm } from "./SettingsForm";
13
- import { useEditorContext } from "../shell/EditorContext";
14
- import { useEditorModal } from "../shell/EditorModalContext";
15
- import { cn } from "../../lib/cn";
16
- export function SectionWrapper({ sectionId, sectionType, status, dirty, index, isLast, definition, options, audiences, access, onAccessChange, onStatusChange, onSectionChange, onReorder, onRequestInsert, onDelete, children, }) {
17
- const { isEditMode, showAllChrome } = useEditorContext();
18
- const { openModal } = useEditorModal();
19
- const [isActive, setIsActive] = useState(false);
20
- const [chromeReady, setChromeReady] = useState(isEditMode);
21
- useEffect(() => {
22
- if (isEditMode) {
23
- const id = requestAnimationFrame(() => setChromeReady(true));
24
- return () => cancelAnimationFrame(id);
25
- }
26
- setChromeReady(false);
27
- }, [isEditMode]);
28
- const blockRef = useRef(null);
29
- const handleRef = useRef(null);
30
- const [isDragging, setIsDragging] = useState(false);
31
- const [closestEdge, setClosestEdge] = useState(null);
32
- const hasSettings = !!((definition.settings || definition.settingsForm) &&
33
- onSectionChange);
34
- useEffect(() => {
35
- const block = blockRef.current;
36
- if (!block)
37
- return;
38
- const onFocusIn = () => setIsActive(true);
39
- const onFocusOut = (e) => {
40
- if (!block.contains(e.relatedTarget))
41
- setIsActive(false);
42
- };
43
- block.addEventListener("focusin", onFocusIn);
44
- block.addEventListener("focusout", onFocusOut);
45
- return () => {
46
- block.removeEventListener("focusin", onFocusIn);
47
- block.removeEventListener("focusout", onFocusOut);
48
- };
49
- }, []);
50
- useEffect(() => {
51
- const block = blockRef.current;
52
- const handle = handleRef.current;
53
- if (!block || !handle || !onReorder)
54
- return;
55
- // Use the parent .section-wrapper as the drop target so the hit area
56
- // includes the padding (where the cursor actually is during drags)
57
- const dropElement = block.closest(".section-wrapper") ?? block;
58
- const cleanupDraggable = draggable({
59
- element: dropElement,
60
- dragHandle: handle,
61
- getInitialData: () => ({ dragType: "section", sectionId, index }),
62
- onGenerateDragPreview: () => {
63
- dropElement.style.opacity = "0.4";
64
- requestAnimationFrame(() => {
65
- dropElement.style.opacity = "";
66
- });
67
- },
68
- onDragStart: () => {
69
- setIsDragging(true);
70
- setIsActive(true);
71
- },
72
- onDrop: () => {
73
- setIsDragging(false);
74
- setIsActive(false);
75
- },
76
- });
77
- const cleanupDropTarget = dropTargetForElements({
78
- element: dropElement,
79
- canDrop: ({ source }) => source.data.dragType === "section",
80
- getData: ({ input, element }) => attachClosestEdge({ sectionId, index }, { input, element, allowedEdges: ["top", "bottom"] }),
81
- onDragEnter: ({ self }) => {
82
- setClosestEdge(extractClosestEdge(self.data));
83
- },
84
- onDrag: ({ self }) => {
85
- setClosestEdge(extractClosestEdge(self.data));
86
- },
87
- onDragLeave: () => {
88
- setClosestEdge(null);
89
- },
90
- onDrop: ({ source, self }) => {
91
- setClosestEdge(null);
92
- const fromIndex = source.data.index;
93
- const edge = extractClosestEdge(self.data);
94
- let toIndex = index;
95
- if (edge === "bottom")
96
- toIndex = index + 1;
97
- if (fromIndex < toIndex)
98
- toIndex--;
99
- if (fromIndex !== toIndex) {
100
- onReorder(fromIndex, toIndex);
101
- }
102
- },
103
- });
104
- return () => {
105
- cleanupDraggable();
106
- cleanupDropTarget();
107
- };
108
- }, [sectionId, index, onReorder]);
109
- function handleSettingsClick() {
110
- if (!onSectionChange)
111
- return;
112
- if (definition.settingsForm) {
113
- const CustomForm = definition.settingsForm;
114
- openModal(`${definition.label} Settings`, _jsx(CustomForm, { ...(options ?? {}), onChange: (values) => onSectionChange({ content: {}, options: values }) }));
115
- }
116
- else if (definition.settings) {
117
- openModal(`${definition.label} Settings`, _jsx(SettingsForm, { schema: definition.settings, values: options ?? {}, onChange: (result) => onSectionChange(result) }));
118
- }
119
- }
120
- if (!isEditMode) {
121
- if (status && status !== "published") {
122
- return (_jsxs("div", { className: "relative", "data-section-id": sectionId, "data-section-type": sectionType, children: [_jsx("div", { className: "pointer-events-none absolute right-0 bottom-full z-30 mb-1", children: _jsx(StatusBadge, { status: status, dirty: dirty }) }), children] }));
123
- }
124
- return _jsx(_Fragment, { children: children });
125
- }
126
- // Chrome visible via: showAllChrome toggle, parent SectionLayout hover (group-hover), or focus within
127
- const alwaysShow = showAllChrome || isActive;
128
- return (_jsxs("div", { ref: blockRef, className: cn("relative", isDragging && "opacity-50", isActive && "outline outline-2 outline-primary/50 rounded-sm", !isActive &&
129
- showAllChrome &&
130
- "outline outline-2 outline-primary/20 rounded-sm", !alwaysShow &&
131
- "group-hover:outline group-hover:outline-2 group-hover:outline-primary/20 group-hover:rounded-sm"), "data-section-id": sectionId, "data-section-type": sectionType, children: [onRequestInsert && !isDragging && chromeReady && (_jsx("div", { className: cn("absolute -left-16 bottom-full z-30 mb-1 transition-opacity", alwaysShow ? "opacity-100" : "opacity-0 group-hover:opacity-100"), children: _jsx(InsertButton, { index: index, onInsert: onRequestInsert }) })), _jsxs("div", { className: cn("absolute left-0 bottom-full z-30 mb-1 flex items-center gap-1.5 transition-opacity", alwaysShow ? "opacity-100" : "opacity-0 group-hover:opacity-100"), children: [_jsx(DragHandle, { ref: handleRef }), _jsx("span", { className: "pointer-events-none select-none whitespace-nowrap text-sm capitalize text-base-contrast-light/80", children: definition.label })] }), _jsxs("div", { className: cn("pointer-events-none absolute right-0 bottom-full z-30 mb-1 flex items-center gap-2 transition-opacity", alwaysShow ? "opacity-100" : "opacity-0 group-hover:opacity-100"), children: [_jsx("div", { className: "pointer-events-auto", children: _jsx(AudiencePicker, { access: access, audiences: audiences, onChange: (newAccess) => onAccessChange?.(newAccess) }) }), _jsx("div", { className: "pointer-events-auto", children: _jsx(StatusPicker, { status: status, dirty: dirty, onChange: (s) => onStatusChange?.(s) }) }), hasSettings && (_jsx(SettingsButton, { onClick: handleSettingsClick })), onDelete && _jsx(DeleteButton, { onDelete: onDelete })] }), closestEdge === "top" && (_jsx("div", { className: "absolute right-0 left-0 z-10 h-0.5 bg-primary", style: {
132
- top: "calc(var(--section-gap) / 2 * -1)",
133
- transform: "translateY(-50%)",
134
- } })), closestEdge === "bottom" && isLast && (_jsx("div", { className: "absolute right-0 bottom-0 left-0 z-10 h-0.5 bg-primary" })), children] }));
135
- }
@@ -1,6 +0,0 @@
1
- import { jsx as _jsx } from "react/jsx-runtime";
2
- import { SettingsIcon } from "../shared/icons";
3
- import { IconButton } from "../shared/IconButton";
4
- export function SettingsButton({ onClick }) {
5
- return (_jsx(IconButton, { icon: _jsx(SettingsIcon, { size: 16 }), label: "Section settings", onClick: onClick, className: "pointer-events-auto" }));
6
- }
@@ -1,64 +0,0 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useId, useState } from "react";
3
- import { Input } from "../shared/Input";
4
- import { Select } from "../shared/Select";
5
- import { Checkbox } from "../shared/Checkbox";
6
- import { FormLabel } from "../shared/FormLabel";
7
- import { cn } from "../../lib/cn";
8
- function resolveValues(schema, values) {
9
- const resolved = {};
10
- for (const [key, field] of Object.entries(schema)) {
11
- resolved[key] = values[key] ?? field.default;
12
- }
13
- return resolved;
14
- }
15
- function coerceValue(field, value) {
16
- if (field.type === "select" && "coerce" in field && field.coerce === "number") {
17
- return Number(value);
18
- }
19
- return value;
20
- }
21
- function splitByTarget(schema, allValues) {
22
- const content = {};
23
- const options = {};
24
- for (const [key, field] of Object.entries(schema)) {
25
- const value = coerceValue(field, allValues[key]);
26
- const target = field.target ?? "options";
27
- if (target === "content") {
28
- content[key] = value;
29
- }
30
- else {
31
- options[key] = value;
32
- }
33
- }
34
- return { content, options };
35
- }
36
- function RangeField({ field, value, onChange, }) {
37
- const id = useId();
38
- return (_jsxs("div", { children: [_jsx(FormLabel, { htmlFor: id, children: field.label }), _jsx("input", { id: id, type: "range", min: field.min, max: field.max, step: field.step, value: value, onChange: (e) => onChange(Number(e.target.value)), className: cn("w-full accent-primary") })] }));
39
- }
40
- export function SettingsForm({ schema, values, onChange }) {
41
- const [local, setLocal] = useState(() => resolveValues(schema, values));
42
- function handleFieldChange(key, newValue) {
43
- const next = { ...local, [key]: newValue };
44
- setLocal(next);
45
- onChange(splitByTarget(schema, next));
46
- }
47
- return (_jsx("div", { className: "flex flex-col gap-4", children: Object.entries(schema).map(([key, field]) => {
48
- const value = local[key];
49
- switch (field.type) {
50
- case "text":
51
- return (_jsx(Input, { label: field.label, value: String(value ?? ""), placeholder: field.placeholder, onChange: (v) => handleFieldChange(key, v) }, key));
52
- case "number":
53
- return (_jsx(Input, { label: field.label, type: "number", value: String(value ?? ""), onChange: (v) => handleFieldChange(key, v === "" ? "" : Number(v)) }, key));
54
- case "checkbox":
55
- return (_jsx(Checkbox, { label: field.label, checked: Boolean(value), onChange: (checked) => handleFieldChange(key, checked) }, key));
56
- case "select":
57
- return (_jsx(Select, { label: field.label, value: String(value ?? ""), options: field.options, onChange: (v) => handleFieldChange(key, v) }, key));
58
- case "range":
59
- return (_jsx(RangeField, { field: field, value: Number(value ?? field.min), onChange: (v) => handleFieldChange(key, v) }, key));
60
- default:
61
- return null;
62
- }
63
- }) }));
64
- }
@@ -1,10 +0,0 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { cn } from "../../lib/cn";
3
- const statusClasses = {
4
- published: "bg-status-published-bg text-status-published-text",
5
- draft: "bg-status-draft-bg text-status-draft-text",
6
- archived: "bg-status-archived-bg text-status-archived-text",
7
- };
8
- export function StatusBadge({ status, dirty }) {
9
- return (_jsxs("span", { className: cn("rounded-full px-2 py-0.5 text-xs font-medium", statusClasses[status] ?? statusClasses.archived), children: [status, dirty && (_jsx("span", { className: "ml-1 opacity-70", children: "\u00B7 Unsaved" }))] }));
10
- }
@@ -1,30 +0,0 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useRef, useState } from "react";
3
- import { Check } from "lucide-react";
4
- import { cn } from "../../lib/cn";
5
- import { Popover } from "../shared/Popover";
6
- import { PopoverItem } from "../shared/PopoverItem";
7
- const STATUSES = ["draft", "published", "archived"];
8
- const statusClasses = {
9
- published: "bg-status-published-bg text-status-published-text",
10
- draft: "bg-status-draft-bg text-status-draft-text",
11
- archived: "bg-status-archived-bg text-status-archived-text",
12
- };
13
- const dotClasses = {
14
- published: "bg-status-published-bg",
15
- draft: "bg-status-draft-bg",
16
- archived: "bg-status-archived-bg",
17
- };
18
- export function StatusPicker({ status, dirty, onChange }) {
19
- const [open, setOpen] = useState(false);
20
- const buttonRef = useRef(null);
21
- function handleSelect(next) {
22
- if (next !== status)
23
- onChange(next);
24
- setOpen(false);
25
- }
26
- return (_jsxs("div", { className: "relative", children: [_jsxs("button", { ref: buttonRef, type: "button", onClick: () => setOpen((v) => !v), "aria-haspopup": "true", "aria-expanded": open, className: cn("cursor-pointer rounded-full px-2 py-0.5 text-xs font-medium", statusClasses[status] ?? statusClasses.archived), children: [status, dirty && _jsx("span", { className: "ml-1 opacity-70", children: "\u00B7 Unsaved" })] }), _jsx(Popover, { isOpen: open, onClose: () => setOpen(false), anchorRef: buttonRef, align: "end", className: "w-44", children: _jsx("ul", { role: "radiogroup", "aria-label": "Section status", className: "py-1", children: STATUSES.map((s) => {
27
- const checked = s === status;
28
- return (_jsx("li", { children: _jsxs(PopoverItem, { role: "radio", "aria-checked": checked, onClick: () => handleSelect(s), children: [_jsx("span", { "aria-hidden": "true", className: cn("h-3 w-3 shrink-0 rounded-full border border-base-200", dotClasses[s]) }), _jsx("span", { className: "flex-1 text-sm font-medium capitalize text-base-contrast", children: s }), checked && _jsx(Check, { size: 14, strokeWidth: 3, className: "text-primary" })] }) }, s));
29
- }) }) })] }));
30
- }
@@ -1,7 +0,0 @@
1
- export { DragHandle } from "./DragHandle";
2
- export { InsertButton } from "./InsertButton";
3
- export { DeleteButton } from "./DeleteButton";
4
- export { SettingsButton } from "./SettingsButton";
5
- export { SettingsForm } from "./SettingsForm";
6
- export { StatusBadge } from "./StatusBadge";
7
- export { SectionWrapper } from "./SectionWrapper";
@@ -1,24 +0,0 @@
1
- import Paragraph from "@tiptap/extension-paragraph";
2
- const ALLOWED_CLASSES = ["large", "lead-in"];
3
- export const CustomParagraph = Paragraph.extend({
4
- addAttributes() {
5
- return {
6
- ...this.parent?.(),
7
- class: {
8
- default: null,
9
- parseHTML: (element) => {
10
- const cls = element.getAttribute("class");
11
- if (cls && ALLOWED_CLASSES.includes(cls)) {
12
- return cls;
13
- }
14
- return null;
15
- },
16
- renderHTML: (attributes) => {
17
- if (!attributes.class)
18
- return {};
19
- return { class: attributes.class };
20
- },
21
- },
22
- };
23
- },
24
- });
@@ -1,90 +0,0 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useRef, useEffect, useState } from "react";
3
- import { draggable, dropTargetForElements } from "@atlaskit/pragmatic-drag-and-drop/element/adapter";
4
- import { attachClosestEdge, extractClosestEdge } from "@atlaskit/pragmatic-drag-and-drop-hitbox/closest-edge";
5
- import { monitorForElements } from "@atlaskit/pragmatic-drag-and-drop/element/adapter";
6
- import { ImageIcon } from "lucide-react";
7
- import { AddIcon, DragHandle, DeleteIcon, SettingsIcon } from "../shared/icons";
8
- import { IconButton } from "../shared/IconButton";
9
- import { cn } from "../../lib/cn";
10
- import { gridColsClass } from "../../lib/grid";
11
- import { useEditableCollection } from "./useEditableCollection";
12
- export function EditableGrid({ items, columns, onChange, createItem, isEditMode, renderItem, onItemSettings, onItemImageClick, chromeTopClass, className, }) {
13
- const { wrappedItems, onReorder, onAdd, onInsert, onRemove } = useEditableCollection({
14
- items,
15
- onChange,
16
- createItem,
17
- });
18
- const [dragState, setDragState] = useState({ sourceId: null, closestEdge: null });
19
- const [isDragging, setIsDragging] = useState(false);
20
- useEffect(() => {
21
- return monitorForElements({
22
- onDragStart: ({ source }) => {
23
- if (source.data.dragType === "grid-cell")
24
- setIsDragging(true);
25
- },
26
- onDrop: ({ source }) => {
27
- if (source.data.dragType === "grid-cell")
28
- setIsDragging(false);
29
- },
30
- });
31
- }, []);
32
- if (!isEditMode) {
33
- return (_jsx("div", { className: cn("grid gap-4", gridColsClass[columns] || "grid-cols-1", className), children: wrappedItems.map((wrapped, index) => (_jsx("div", { children: renderItem(wrapped.data, { isEditMode: false, index }) }, wrapped.id))) }));
34
- }
35
- return (_jsxs("div", { className: cn("group/grid relative grid gap-4", gridColsClass[columns] || "grid-cols-1", className), children: [wrappedItems.map((wrapped, index) => (_jsx(GridCell, { id: wrapped.id, index: index, isLast: index === wrappedItems.length - 1, isDragging: isDragging, onReorder: onReorder, onRemove: onRemove, onInsert: onInsert, onSettings: onItemSettings ? () => onItemSettings(index) : undefined, onImageClick: onItemImageClick ? () => onItemImageClick(index) : undefined, chromeTopClass: chromeTopClass, dragState: dragState, setDragState: setDragState, children: renderItem(wrapped.data, { isEditMode: true, index }) }, wrapped.id))), _jsx(IconButton, { icon: _jsx(AddIcon, { size: 16 }), label: "Add item", size: "lg", intent: "primary", onClick: onAdd, className: "absolute -bottom-6 left-1/2 z-20 -translate-x-1/2 rounded-full border border-base-200 bg-base opacity-0 transition-opacity group-hover/grid:opacity-100" })] }));
36
- }
37
- function GridCell({ id, index, isLast, isDragging, onReorder, onRemove, onInsert, onSettings, onImageClick, chromeTopClass, dragState, setDragState, children, }) {
38
- const cellRef = useRef(null);
39
- const handleRef = useRef(null);
40
- useEffect(() => {
41
- const cell = cellRef.current;
42
- const handle = handleRef.current;
43
- if (!cell || !handle)
44
- return;
45
- const cleanupDraggable = draggable({
46
- element: handle,
47
- getInitialData: () => ({ dragType: "grid-cell", id, index }),
48
- onDragStart: () => setDragState({ sourceId: id, closestEdge: null }),
49
- onDrop: () => setDragState({ sourceId: null, closestEdge: null }),
50
- });
51
- const cleanupDropTarget = dropTargetForElements({
52
- element: cell,
53
- canDrop: ({ source }) => source.data.dragType === "grid-cell",
54
- getData: ({ input, element }) => attachClosestEdge({ id, index }, { input, element, allowedEdges: ["left", "right"] }),
55
- onDragEnter: ({ self }) => {
56
- const edge = extractClosestEdge(self.data);
57
- if (edge)
58
- setDragState((prev) => ({ ...prev, closestEdge: { id, edge: edge } }));
59
- },
60
- onDrag: ({ self }) => {
61
- const edge = extractClosestEdge(self.data);
62
- if (edge)
63
- setDragState((prev) => ({ ...prev, closestEdge: { id, edge: edge } }));
64
- },
65
- onDragLeave: () => {
66
- setDragState((prev) => prev.closestEdge?.id === id ? { ...prev, closestEdge: null } : prev);
67
- },
68
- onDrop: ({ source, self }) => {
69
- const fromIndex = source.data.index;
70
- const edge = extractClosestEdge(self.data);
71
- let toIndex = index;
72
- if (edge === "right")
73
- toIndex = index + 1;
74
- if (fromIndex < toIndex)
75
- toIndex--;
76
- if (fromIndex !== toIndex) {
77
- onReorder(fromIndex, toIndex);
78
- }
79
- },
80
- });
81
- return () => {
82
- cleanupDraggable();
83
- cleanupDropTarget();
84
- };
85
- }, [id, index, onReorder, setDragState]);
86
- const isCellDragging = dragState.sourceId === id;
87
- const showLeftEdge = dragState.closestEdge?.id === id && dragState.closestEdge.edge === "left" && dragState.sourceId !== id;
88
- const showRightEdge = dragState.closestEdge?.id === id && dragState.closestEdge.edge === "right" && dragState.sourceId !== id;
89
- return (_jsxs("div", { ref: cellRef, className: cn("group/cell relative", isCellDragging && "opacity-50"), children: [showLeftEdge && (_jsx("div", { className: "absolute top-0 bottom-0 left-0 z-10 w-0.5 -translate-x-2 bg-primary" })), showRightEdge && (_jsx("div", { className: "absolute top-0 right-0 bottom-0 z-10 w-0.5 translate-x-2 bg-primary" })), !isDragging && !isLast && (_jsx(IconButton, { icon: _jsx(AddIcon, { size: 12 }), label: "Insert item", size: "sm", intent: "primary", onClick: () => onInsert(index + 1), className: "absolute top-1/2 -right-2 z-20 -translate-y-1/2 translate-x-1/2 rounded-full border border-base-200 bg-base opacity-0 group-hover/cell:opacity-100" })), _jsx(IconButton, { ref: handleRef, icon: _jsx(DragHandle, { size: 16 }), label: "Drag to reorder", className: cn("absolute left-1.5 z-10 cursor-grab bg-base/80 opacity-0 shadow-sm group-hover/cell:opacity-100 no-hover:opacity-100", chromeTopClass || "top-1.5"), tabIndex: -1 }), _jsxs("div", { className: cn("absolute right-1.5 z-10 flex items-center gap-1 opacity-0 group-hover/cell:opacity-100 no-hover:opacity-100", chromeTopClass || "top-1.5"), children: [onImageClick && (_jsx(IconButton, { icon: _jsx(ImageIcon, { size: 16 }), label: "Change image", onClick: onImageClick, className: "bg-base/80 shadow-sm" })), onSettings && (_jsx(IconButton, { icon: _jsx(SettingsIcon, { size: 16 }), label: "Item settings", onClick: onSettings, className: "bg-base/80 shadow-sm" })), _jsx(IconButton, { icon: _jsx(DeleteIcon, { size: 16 }), label: "Delete item", intent: "destructive", onClick: () => onRemove(id), className: "bg-base/80 shadow-sm" })] }), children] }));
90
- }
@@ -1,54 +0,0 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useRef, useEffect, useState } from "react";
3
- import { draggable, dropTargetForElements } from "@atlaskit/pragmatic-drag-and-drop/element/adapter";
4
- import { DragHandle, DeleteIcon } from "../shared/icons";
5
- import { IconButton } from "../shared/IconButton";
6
- import { cn } from "../../lib/cn";
7
- import { useEditableCollection } from "./useEditableCollection";
8
- export function EditableList({ items, onChange, createItem, isEditMode, renderItem, className, }) {
9
- const { wrappedItems, onReorder, onAdd, onRemove } = useEditableCollection({
10
- items,
11
- onChange,
12
- createItem,
13
- });
14
- const [dragState, setDragState] = useState({ sourceId: null, targetId: null });
15
- if (!isEditMode) {
16
- return (_jsx("div", { className: cn("flex flex-col gap-2", className), children: wrappedItems.map((wrapped, index) => (_jsx("div", { children: renderItem(wrapped.data, { isEditMode: false, index }) }, wrapped.id))) }));
17
- }
18
- return (_jsxs("div", { className: cn("flex flex-col gap-2", className), children: [wrappedItems.map((wrapped, index) => (_jsx(ListRow, { id: wrapped.id, index: index, onReorder: onReorder, onRemove: onRemove, dragState: dragState, setDragState: setDragState, children: renderItem(wrapped.data, { isEditMode: true, index }) }, wrapped.id))), _jsx("button", { onClick: onAdd, className: "cursor-pointer rounded border-2 border-dashed border-base-200 px-3 py-2 text-sm text-base-contrast-light hover:border-primary hover:text-primary", children: "Add row" })] }));
19
- }
20
- function ListRow({ id, index, onReorder, onRemove, dragState, setDragState, children, }) {
21
- const rowRef = useRef(null);
22
- const handleRef = useRef(null);
23
- useEffect(() => {
24
- const row = rowRef.current;
25
- const handle = handleRef.current;
26
- if (!row || !handle)
27
- return;
28
- const cleanupDraggable = draggable({
29
- element: handle,
30
- getInitialData: () => ({ dragType: "list-row", id, index }),
31
- onDragStart: () => setDragState({ sourceId: id, targetId: null }),
32
- onDrop: () => setDragState({ sourceId: null, targetId: null }),
33
- });
34
- const cleanupDropTarget = dropTargetForElements({
35
- element: row,
36
- canDrop: ({ source }) => source.data.dragType === "list-row",
37
- getData: () => ({ id, index }),
38
- onDragEnter: () => setDragState((prev) => ({ ...prev, targetId: id })),
39
- onDragLeave: () => setDragState((prev) => prev.targetId === id ? { ...prev, targetId: null } : prev),
40
- onDrop: ({ source }) => {
41
- const fromIndex = source.data.index;
42
- if (fromIndex !== index) {
43
- onReorder(fromIndex, index);
44
- }
45
- },
46
- });
47
- return () => {
48
- cleanupDraggable();
49
- cleanupDropTarget();
50
- };
51
- }, [id, index, onReorder, setDragState]);
52
- const isDropTarget = dragState.targetId === id && dragState.sourceId !== id;
53
- return (_jsxs("div", { ref: rowRef, className: cn("group/row relative flex items-center gap-2", isDropTarget && "border-t-2 border-primary"), children: [_jsx(IconButton, { ref: handleRef, icon: _jsx(DragHandle, { size: 14 }), label: "Drag to reorder", size: "sm", className: "shrink-0 cursor-grab opacity-0 group-hover/row:opacity-100 no-hover:opacity-100", tabIndex: -1 }), _jsx("div", { className: "min-w-0 flex-1", children: children }), _jsx(IconButton, { icon: _jsx(DeleteIcon, { size: 14 }), label: "Delete item", size: "sm", intent: "destructive", onClick: () => onRemove(id), className: "shrink-0 opacity-0 group-hover/row:opacity-100 no-hover:opacity-100" })] }));
54
- }
@@ -1,52 +0,0 @@
1
- import { useRef, useEffect, useCallback, createElement, } from "react";
2
- import { useEditablePlainText } from "./useEditablePlainText";
3
- export function EditablePlainText({ tag, value, onChange, isEditMode, multiline = false, placeholder, className, ...htmlAttrs }) {
4
- const ref = useRef(null);
5
- const { value: localValue, handleInput, handleBlur, shouldPreventEnter, } = useEditablePlainText({ value, onChange, multiline });
6
- // Set textContent via ref (safe — plain text only, no HTML injection)
7
- useEffect(() => {
8
- if (ref.current && ref.current.textContent !== localValue) {
9
- ref.current.textContent = localValue;
10
- }
11
- }, [localValue]);
12
- const onInput = useCallback(() => {
13
- if (ref.current) {
14
- handleInput(ref.current.textContent || "");
15
- }
16
- }, [handleInput]);
17
- const onKeyDown = useCallback((e) => {
18
- if (e.key === "Enter" && shouldPreventEnter) {
19
- e.preventDefault();
20
- }
21
- }, [shouldPreventEnter]);
22
- const onPaste = useCallback((e) => {
23
- e.preventDefault();
24
- const text = e.clipboardData.getData("text/plain");
25
- const selection = window.getSelection();
26
- if (selection && selection.rangeCount > 0) {
27
- const range = selection.getRangeAt(0);
28
- range.deleteContents();
29
- range.insertNode(document.createTextNode(text));
30
- range.collapse(false);
31
- selection.removeAllRanges();
32
- selection.addRange(range);
33
- }
34
- if (ref.current) {
35
- handleInput(ref.current.textContent || "");
36
- }
37
- }, [handleInput]);
38
- return createElement(tag, {
39
- ref,
40
- className: isEditMode && placeholder
41
- ? `editable-placeholder ${className ?? ""}`
42
- : className,
43
- contentEditable: isEditMode ? "true" : undefined,
44
- suppressContentEditableWarning: isEditMode || undefined,
45
- onInput: isEditMode ? onInput : undefined,
46
- onBlur: isEditMode ? handleBlur : undefined,
47
- onKeyDown: isEditMode ? onKeyDown : undefined,
48
- onPaste: isEditMode ? onPaste : undefined,
49
- "data-placeholder": isEditMode && placeholder ? placeholder : undefined,
50
- ...htmlAttrs,
51
- }, value);
52
- }
@@ -1,86 +0,0 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useRef, useCallback, useEffect } from "react";
3
- import { EditorContent } from "@tiptap/react";
4
- import { BubbleMenu } from "@tiptap/react/menus";
5
- import { sanitizeHtml } from "../../lib/sanitize";
6
- import { useEditableRichText } from "./useEditableRichText";
7
- import { RichTextToolbar } from "./RichTextToolbar";
8
- export function EditableRichText({ value, onChange, isEditMode, preset = "rich", className, }) {
9
- const { isEditorActive, editor, activate, deactivate } = useEditableRichText({ value, onChange, preset });
10
- const wrapperRef = useRef(null);
11
- const blurTimeoutRef = useRef(null);
12
- const clickCoordsRef = useRef(null);
13
- // Clear blur timeout on unmount to prevent stale callback execution
14
- useEffect(() => {
15
- return () => {
16
- if (blurTimeoutRef.current) {
17
- clearTimeout(blurTimeoutRef.current);
18
- }
19
- };
20
- }, []);
21
- // Focus the editor and place cursor at click position after activation
22
- useEffect(() => {
23
- if (!isEditorActive || !editor)
24
- return;
25
- requestAnimationFrame(() => {
26
- if (editor.isDestroyed)
27
- return;
28
- const coords = clickCoordsRef.current;
29
- if (coords) {
30
- try {
31
- const pos = editor.view.posAtCoords(coords);
32
- if (pos) {
33
- editor.commands.focus();
34
- editor.commands.setTextSelection(pos.pos);
35
- }
36
- else {
37
- editor.commands.focus('end');
38
- }
39
- }
40
- catch {
41
- editor.commands.focus('end');
42
- }
43
- finally {
44
- clickCoordsRef.current = null;
45
- }
46
- }
47
- else {
48
- editor.commands.focus('end');
49
- }
50
- });
51
- }, [isEditorActive, editor]);
52
- const handleClick = useCallback((e) => {
53
- if (isEditMode && !isEditorActive) {
54
- clickCoordsRef.current = { left: e.clientX, top: e.clientY };
55
- activate();
56
- }
57
- }, [isEditMode, isEditorActive, activate]);
58
- const handleFocus = useCallback(() => {
59
- if (blurTimeoutRef.current) {
60
- clearTimeout(blurTimeoutRef.current);
61
- blurTimeoutRef.current = null;
62
- }
63
- }, []);
64
- const handleBlur = useCallback(() => {
65
- blurTimeoutRef.current = setTimeout(() => {
66
- if (wrapperRef.current &&
67
- !wrapperRef.current.contains(document.activeElement)) {
68
- deactivate();
69
- }
70
- }, 150);
71
- }, [deactivate]);
72
- if (!isEditMode) {
73
- // eslint-disable-next-line react/no-danger
74
- return (_jsx("div", { className: className, dangerouslySetInnerHTML: { __html: sanitizeHtml(value) } }));
75
- }
76
- // Edit mode, not yet active: static HTML with click-to-activate
77
- if (!isEditorActive || !editor) {
78
- // eslint-disable-next-line react/no-danger
79
- return (_jsx("div", { className: className, onClick: handleClick, dangerouslySetInnerHTML: { __html: sanitizeHtml(value) } }));
80
- }
81
- // Edit mode, active: TipTap editor with bubble menu on text selection
82
- return (_jsxs("div", { ref: wrapperRef, className: className, onFocus: handleFocus, onBlur: handleBlur, children: [_jsx(BubbleMenu, { editor: editor, shouldShow: ({ editor: ed, state }) => {
83
- const { from, to } = state.selection;
84
- return ed.isFocused && from !== to;
85
- }, children: _jsx(RichTextToolbar, { editor: editor, preset: preset }) }), _jsx(EditorContent, { editor: editor })] }));
86
- }
@@ -1,7 +0,0 @@
1
- import { jsx as _jsx } from "react/jsx-runtime";
2
- import { toSectionId } from "../../lib/nav";
3
- import { EditablePlainText } from "./EditablePlainText";
4
- import { cn } from "../../lib/cn";
5
- export function HeadingSection({ heading, tag, placeholder, className, onChange }) {
6
- return (_jsx(EditablePlainText, { tag: tag, value: heading, onChange: onChange ?? (() => { }), isEditMode: !!onChange, placeholder: placeholder, id: toSectionId(heading), className: cn("scroll-mt-20", className) }));
7
- }
@@ -1,21 +0,0 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useEffect, useRef } from "react";
3
- import { cn } from "../../lib/cn";
4
- import { curatedIcons } from "../../lib/icons";
5
- export function IconPicker({ selected, onSelect, onClose, showRemove = true }) {
6
- const panelRef = useRef(null);
7
- useEffect(() => {
8
- function handleMouseDown(e) {
9
- if (panelRef.current && !panelRef.current.contains(e.target)) {
10
- onClose();
11
- }
12
- }
13
- document.addEventListener("mousedown", handleMouseDown);
14
- return () => document.removeEventListener("mousedown", handleMouseDown);
15
- }, [onClose]);
16
- return (_jsxs("div", { ref: panelRef, className: "absolute z-50 w-56 rounded-lg border border-base-200 bg-base p-2 shadow-lg", children: [_jsx("div", { className: "grid grid-cols-5 gap-1", children: curatedIcons.map((entry) => {
17
- const Icon = entry.icon;
18
- const isSelected = selected === entry.id;
19
- return (_jsx("button", { "aria-label": entry.label, className: cn("cursor-pointer flex h-9 w-9 items-center justify-center rounded transition-colors", "text-base-contrast-light hover:bg-base-accent hover:text-base-contrast", isSelected && "ring-2 ring-primary bg-base-accent text-primary"), onClick: () => onSelect(entry.id), children: _jsx(Icon, { size: 18 }) }, entry.id));
20
- }) }), showRemove && (_jsx("button", { className: "cursor-pointer mt-2 w-full rounded px-2 py-1.5 text-center text-xs text-base-contrast-light hover:bg-base-accent hover:text-base-contrast", onClick: () => onSelect(null), children: "Remove icon" }))] }));
21
- }