@drawnagency/primitives 0.1.56 → 0.1.58
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/adapter-HH47ZPGM.js +1779 -0
- package/dist/auth/index.js +1 -0
- package/dist/{chunk-EU6NZ4GS.js → chunk-AN62WPW7.js} +22 -163
- package/dist/chunk-ESE5UBQI.js +73 -0
- package/dist/{chunk-KGYWQDBB.js → chunk-ICLXLWQ5.js} +9 -72
- package/dist/chunk-JSBRDJBE.js +30 -0
- package/dist/chunk-NSCT3AMV.js +32 -0
- package/dist/chunk-RFZNNCAS.js +160 -0
- package/dist/chunk-TG43X7JO.js +123 -0
- package/dist/{chunk-7IAWF7LE.js → chunk-V7JN2DDU.js} +2 -19
- package/dist/chunk-VKAGMEKE.js +90 -0
- package/dist/chunk-ZU2MKPTG.js +29 -0
- package/dist/closest-edge-EBOXL3YW.js +72 -0
- package/dist/components/editor/ChildBlockWrapper.d.ts +19 -0
- package/dist/components/editor/ChildBlockWrapper.d.ts.map +1 -0
- package/dist/components/editor/ColSpanControl.d.ts +9 -0
- package/dist/components/editor/ColSpanControl.d.ts.map +1 -0
- package/dist/components/editor/SectionWrapper.d.ts +1 -1
- package/dist/components/editor/SectionWrapper.d.ts.map +1 -1
- package/dist/components/editor/SettingsForm.d.ts +5 -1
- package/dist/components/editor/SettingsForm.d.ts.map +1 -1
- package/dist/components/primitives/EditableGrid.d.ts.map +1 -1
- package/dist/components/primitives/IconPicker.d.ts +7 -1
- package/dist/components/primitives/IconPicker.d.ts.map +1 -1
- package/dist/components/sections/Container/Container.d.ts +20 -0
- package/dist/components/sections/Container/Container.d.ts.map +1 -0
- package/dist/components/sections/Container/ContainerSettingsForm.d.ts +17 -0
- package/dist/components/sections/Container/ContainerSettingsForm.d.ts.map +1 -0
- package/dist/components/sections/Container/index.d.ts +11 -0
- package/dist/components/sections/Container/index.d.ts.map +1 -0
- package/dist/components/sections/IconList/IconList.d.ts +1 -0
- package/dist/components/sections/IconList/IconList.d.ts.map +1 -1
- package/dist/components/sections/IconList/IconListSettings.d.ts +3 -4
- package/dist/components/sections/IconList/IconListSettings.d.ts.map +1 -1
- package/dist/components/sections/IconList/index.d.ts +1 -0
- package/dist/components/sections/IconList/index.d.ts.map +1 -1
- package/dist/components/sections/Media/MediaBlock.d.ts +19 -0
- package/dist/components/sections/Media/MediaBlock.d.ts.map +1 -0
- package/dist/components/sections/{MediaGrid → Media}/index.d.ts +15 -25
- package/dist/components/sections/Media/index.d.ts.map +1 -0
- package/dist/components/sections/Prose/index.d.ts.map +1 -1
- package/dist/components/sections/Spacer/Spacer.d.ts +2 -0
- package/dist/components/sections/Spacer/Spacer.d.ts.map +1 -0
- package/dist/components/sections/Spacer/index.d.ts +6 -0
- package/dist/components/sections/Spacer/index.d.ts.map +1 -0
- package/dist/components/sections/all-sections.d.ts +29 -103
- package/dist/components/sections/all-sections.d.ts.map +1 -1
- package/dist/components/sections/register-schemas.d.ts.map +1 -1
- package/dist/components/sections/register-schemas.js +4094 -0
- package/dist/components/shared/Tabs.d.ts +24 -0
- package/dist/components/shared/Tabs.d.ts.map +1 -0
- package/dist/components/shell/EditorShell.d.ts.map +1 -1
- package/dist/components/shell/SiteSettingsModal.d.ts.map +1 -1
- package/dist/components/shell/blockMoveDispatch.d.ts +21 -0
- package/dist/components/shell/blockMoveDispatch.d.ts.map +1 -0
- package/dist/hooks/useBlockDnd.d.ts +48 -0
- package/dist/hooks/useBlockDnd.d.ts.map +1 -0
- package/dist/index.js +69 -56
- package/dist/lib/block-dnd.d.ts +42 -0
- package/dist/lib/block-dnd.d.ts.map +1 -0
- package/dist/lib/block-move.d.ts +31 -0
- package/dist/lib/block-move.d.ts.map +1 -0
- package/dist/lib/container-grid.d.ts +29 -0
- package/dist/lib/container-grid.d.ts.map +1 -0
- package/dist/lib/container-ops.d.ts +44 -0
- package/dist/lib/container-ops.d.ts.map +1 -0
- package/dist/lib/dexie.d.ts.map +1 -1
- package/dist/lib/dexie.js +15 -0
- package/dist/lib/env.js +1 -0
- package/dist/lib/index.js +19 -13
- package/dist/lib/loader.d.ts.map +1 -1
- package/dist/lib/migrate-sections-transform.d.ts +12 -0
- package/dist/lib/migrate-sections-transform.d.ts.map +1 -0
- package/dist/lib/migrate-sections-transform.js +7 -0
- package/dist/lib/registry.d.ts +39 -0
- package/dist/lib/registry.d.ts.map +1 -1
- package/dist/lib/registry.js +27 -0
- package/dist/media/index.js +1 -0
- package/dist/schemas/auth.js +1 -0
- package/dist/schemas/block.d.ts +20 -0
- package/dist/schemas/block.d.ts.map +1 -0
- package/dist/schemas/block.js +15 -0
- package/dist/schemas/index.js +13 -4
- package/dist/schemas/link.d.ts +7 -0
- package/dist/schemas/link.d.ts.map +1 -1
- package/dist/schemas/rich-text.d.ts +9 -0
- package/dist/schemas/rich-text.d.ts.map +1 -0
- package/dist/schemas/sections.d.ts +2 -0
- package/dist/schemas/sections.d.ts.map +1 -1
- package/dist/schemas/shared.d.ts +30 -0
- package/dist/schemas/shared.d.ts.map +1 -1
- package/dist/types/database.js +2 -0
- package/package.json +17 -1
- package/src/components/brandguide/Colors.tsx +35 -33
- package/src/components/editor/ChildBlockWrapper.tsx +108 -0
- package/src/components/editor/ColSpanControl.tsx +56 -0
- package/src/components/editor/SectionWrapper.tsx +44 -20
- package/src/components/editor/SettingsForm.tsx +100 -73
- package/src/components/primitives/EditableGrid.tsx +40 -36
- package/src/components/primitives/IconPicker.tsx +116 -26
- package/src/components/sections/Container/Container.tsx +354 -0
- package/src/components/sections/Container/ContainerSettingsForm.tsx +113 -0
- package/src/components/sections/Container/index.tsx +51 -0
- package/src/components/sections/IconList/IconList.tsx +113 -43
- package/src/components/sections/IconList/IconListSettings.tsx +2 -2
- package/src/components/sections/IconList/index.tsx +1 -1
- package/src/components/sections/Media/MediaBlock.tsx +103 -0
- package/src/components/sections/Media/index.tsx +85 -0
- package/src/components/sections/Prose/index.tsx +1 -0
- package/src/components/sections/Spacer/Spacer.tsx +6 -0
- package/src/components/sections/Spacer/index.tsx +18 -0
- package/src/components/sections/all-sections.ts +10 -8
- package/src/components/sections/register-schemas.ts +5 -2
- package/src/components/shared/Tabs.tsx +63 -0
- package/src/components/shell/EditorShell.tsx +105 -13
- package/src/components/shell/SiteSettingsModal.tsx +41 -51
- package/src/components/shell/blockMoveDispatch.ts +40 -0
- package/src/hooks/useBlockDnd.ts +144 -0
- package/src/lib/block-dnd.ts +58 -0
- package/src/lib/block-move.ts +236 -0
- package/src/lib/container-grid.ts +58 -0
- package/src/lib/container-ops.ts +159 -0
- package/src/lib/dexie.ts +22 -0
- package/src/lib/loader.ts +16 -4
- package/src/lib/migrate-sections-transform.ts +147 -0
- package/src/lib/registry.ts +48 -0
- package/src/schemas/block.ts +40 -0
- package/src/schemas/link.ts +19 -1
- package/src/schemas/rich-text.ts +11 -0
- package/src/schemas/sections.ts +5 -1
- package/src/schemas/shared.ts +6 -0
- package/dist/components/brandguide/DoDontList.d.ts +0 -16
- package/dist/components/brandguide/DoDontList.d.ts.map +0 -1
- package/dist/components/brandguide/DoDontMediaGrid.d.ts +0 -16
- package/dist/components/brandguide/DoDontMediaGrid.d.ts.map +0 -1
- package/dist/components/primitives/MediaSettingsForms.d.ts +0 -23
- package/dist/components/primitives/MediaSettingsForms.d.ts.map +0 -1
- package/dist/components/sections/DoDontList/index.d.ts +0 -21
- package/dist/components/sections/DoDontList/index.d.ts.map +0 -1
- package/dist/components/sections/DoDontMediaGrid/index.d.ts +0 -55
- package/dist/components/sections/DoDontMediaGrid/index.d.ts.map +0 -1
- package/dist/components/sections/MediaGrid/MediaGrid.d.ts +0 -17
- package/dist/components/sections/MediaGrid/MediaGrid.d.ts.map +0 -1
- package/dist/components/sections/MediaGrid/index.d.ts.map +0 -1
- package/dist/components/sections/SplitContent/SplitContent.d.ts +0 -14
- package/dist/components/sections/SplitContent/SplitContent.d.ts.map +0 -1
- package/dist/components/sections/SplitContent/index.d.ts +0 -13
- package/dist/components/sections/SplitContent/index.d.ts.map +0 -1
- package/src/components/brandguide/DoDontList.d.ts.map +0 -1
- package/src/components/brandguide/DoDontList.tsx +0 -67
- package/src/components/brandguide/DoDontMediaGrid.d.ts.map +0 -1
- package/src/components/brandguide/DoDontMediaGrid.tsx +0 -19
- package/src/components/primitives/MediaSettingsForms.tsx +0 -128
- package/src/components/sections/DoDontList/index.d.ts.map +0 -1
- package/src/components/sections/DoDontList/index.tsx +0 -45
- package/src/components/sections/DoDontMediaGrid/index.d.ts.map +0 -1
- package/src/components/sections/DoDontMediaGrid/index.tsx +0 -63
- package/src/components/sections/MediaGrid/MediaGrid.d.ts.map +0 -1
- package/src/components/sections/MediaGrid/MediaGrid.tsx +0 -239
- package/src/components/sections/MediaGrid/index.d.ts.map +0 -1
- package/src/components/sections/MediaGrid/index.tsx +0 -57
- package/src/components/sections/SplitContent/SplitContent.d.ts.map +0 -1
- package/src/components/sections/SplitContent/SplitContent.tsx +0 -84
- package/src/components/sections/SplitContent/index.d.ts.map +0 -1
- package/src/components/sections/SplitContent/index.tsx +0 -55
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { useState, useRef, useEffect } from "react";
|
|
2
|
-
import { ArrowRightLeft } from "lucide-react";
|
|
2
|
+
import { ArrowRightLeft, Plus } from "lucide-react";
|
|
3
3
|
import type { Edge } from "@atlaskit/pragmatic-drag-and-drop-hitbox/closest-edge";
|
|
4
4
|
import { DragHandle } from "./DragHandle";
|
|
5
5
|
import { InsertButton } from "./InsertButton";
|
|
@@ -11,6 +11,7 @@ import { SettingsForm } from "./SettingsForm";
|
|
|
11
11
|
import { useEditorContext } from "../shell/EditorContext";
|
|
12
12
|
import { useEditorModal } from "../shell/EditorModalContext";
|
|
13
13
|
import type { WrapperProps } from "../../lib/registry";
|
|
14
|
+
import { buildBlockDragData, buildBlockDropData, isBlockDragData, canDropBlock } from "../../lib/block-dnd";
|
|
14
15
|
import { cn } from "../../lib/cn";
|
|
15
16
|
|
|
16
17
|
export function SectionWrapper({
|
|
@@ -20,6 +21,8 @@ export function SectionWrapper({
|
|
|
20
21
|
dirty,
|
|
21
22
|
index,
|
|
22
23
|
isLast,
|
|
24
|
+
containerId,
|
|
25
|
+
isContainerBlock,
|
|
23
26
|
definition,
|
|
24
27
|
options,
|
|
25
28
|
audiences,
|
|
@@ -31,6 +34,7 @@ export function SectionWrapper({
|
|
|
31
34
|
onRequestInsert,
|
|
32
35
|
onDelete,
|
|
33
36
|
onMoveSection,
|
|
37
|
+
onAddChild,
|
|
34
38
|
mainStatus,
|
|
35
39
|
contentDiffersFromMain,
|
|
36
40
|
isLocalOnly,
|
|
@@ -77,7 +81,7 @@ export function SectionWrapper({
|
|
|
77
81
|
useEffect(() => {
|
|
78
82
|
const block = blockRef.current;
|
|
79
83
|
const handle = handleRef.current;
|
|
80
|
-
if (!block || !handle || !
|
|
84
|
+
if (!block || !handle || !isEditMode) return;
|
|
81
85
|
|
|
82
86
|
let cleanup: (() => void) | undefined;
|
|
83
87
|
let cancelled = false;
|
|
@@ -96,7 +100,13 @@ export function SectionWrapper({
|
|
|
96
100
|
const cleanupDraggable = draggable({
|
|
97
101
|
element: dropElement,
|
|
98
102
|
dragHandle: handle,
|
|
99
|
-
getInitialData: () =>
|
|
103
|
+
getInitialData: () =>
|
|
104
|
+
buildBlockDragData({
|
|
105
|
+
blockId: sectionId,
|
|
106
|
+
containerId,
|
|
107
|
+
index,
|
|
108
|
+
isContainer: isContainerBlock,
|
|
109
|
+
}),
|
|
100
110
|
onGenerateDragPreview: () => {
|
|
101
111
|
dropElement.style.opacity = "0.4";
|
|
102
112
|
requestAnimationFrame(() => {
|
|
@@ -115,31 +125,32 @@ export function SectionWrapper({
|
|
|
115
125
|
|
|
116
126
|
const cleanupDropTarget = dropTargetForElements({
|
|
117
127
|
element: dropElement,
|
|
118
|
-
canDrop: ({ source }) =>
|
|
128
|
+
canDrop: ({ source }) =>
|
|
129
|
+
isBlockDragData(source.data) && canDropBlock(source.data, containerId),
|
|
119
130
|
getData: ({ input, element }) =>
|
|
120
131
|
attachClosestEdge(
|
|
121
|
-
{
|
|
132
|
+
buildBlockDropData({ dropContainerId: containerId, index }),
|
|
122
133
|
{ input, element, allowedEdges: ["top", "bottom"] },
|
|
123
134
|
),
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
135
|
+
onDrag: ({ self, source, location }) => {
|
|
136
|
+
const innermost = location.current.dropTargets[0]?.element === self.element;
|
|
137
|
+
if (
|
|
138
|
+
!innermost ||
|
|
139
|
+
!isBlockDragData(source.data) ||
|
|
140
|
+
source.data.blockId === sectionId
|
|
141
|
+
) {
|
|
142
|
+
setClosestEdge(null);
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
128
145
|
setClosestEdge(extractClosestEdge(self.data));
|
|
129
146
|
},
|
|
130
147
|
onDragLeave: () => {
|
|
131
148
|
setClosestEdge(null);
|
|
132
149
|
},
|
|
133
|
-
onDrop: (
|
|
150
|
+
onDrop: () => {
|
|
151
|
+
// Central monitor (EditorContent) dispatches the move; we only
|
|
152
|
+
// clear local hover state here.
|
|
134
153
|
setClosestEdge(null);
|
|
135
|
-
const fromIndex = source.data.index as number;
|
|
136
|
-
const edge = extractClosestEdge(self.data);
|
|
137
|
-
let toIndex = index;
|
|
138
|
-
if (edge === "bottom") toIndex = index + 1;
|
|
139
|
-
if (fromIndex < toIndex) toIndex--;
|
|
140
|
-
if (fromIndex !== toIndex) {
|
|
141
|
-
onReorder(fromIndex, toIndex);
|
|
142
|
-
}
|
|
143
154
|
},
|
|
144
155
|
});
|
|
145
156
|
|
|
@@ -153,7 +164,7 @@ export function SectionWrapper({
|
|
|
153
164
|
cancelled = true;
|
|
154
165
|
cleanup?.();
|
|
155
166
|
};
|
|
156
|
-
}, [sectionId, index,
|
|
167
|
+
}, [sectionId, index, containerId, isContainerBlock, isEditMode]);
|
|
157
168
|
|
|
158
169
|
function handleSettingsClick() {
|
|
159
170
|
if (!onSectionChange) return;
|
|
@@ -164,7 +175,8 @@ export function SectionWrapper({
|
|
|
164
175
|
`${definition.label} Settings`,
|
|
165
176
|
<CustomForm
|
|
166
177
|
{...(options ?? {})}
|
|
167
|
-
onChange={(
|
|
178
|
+
onChange={(result: { content: Record<string, unknown>; options: Record<string, unknown> }) =>
|
|
179
|
+
onSectionChange(result as unknown as Record<string, unknown>)}
|
|
168
180
|
/>,
|
|
169
181
|
);
|
|
170
182
|
} else if (definition.settings) {
|
|
@@ -174,6 +186,7 @@ export function SectionWrapper({
|
|
|
174
186
|
schema={definition.settings}
|
|
175
187
|
values={options ?? {}}
|
|
176
188
|
onChange={(result) => onSectionChange(result as unknown as Record<string, unknown>)}
|
|
189
|
+
tabs={definition.settingsTabs}
|
|
177
190
|
/>,
|
|
178
191
|
);
|
|
179
192
|
}
|
|
@@ -284,6 +297,17 @@ export function SectionWrapper({
|
|
|
284
297
|
/>
|
|
285
298
|
</div>
|
|
286
299
|
|
|
300
|
+
{onAddChild && (
|
|
301
|
+
<button
|
|
302
|
+
type="button"
|
|
303
|
+
onClick={onAddChild}
|
|
304
|
+
aria-label="Add block"
|
|
305
|
+
className="pointer-events-auto cursor-pointer rounded p-1 text-base-contrast-light hover:text-primary"
|
|
306
|
+
>
|
|
307
|
+
<Plus size={16} />
|
|
308
|
+
</button>
|
|
309
|
+
)}
|
|
310
|
+
|
|
287
311
|
{hasSettings && (
|
|
288
312
|
<SettingsButton onClick={handleSettingsClick} />
|
|
289
313
|
)}
|
|
@@ -5,6 +5,7 @@ import { Select } from "../shared/Select";
|
|
|
5
5
|
import { Checkbox } from "../shared/Checkbox";
|
|
6
6
|
import { FormLabel } from "../shared/FormLabel";
|
|
7
7
|
import { LinkField } from "../shared/LinkField";
|
|
8
|
+
import { Tabs } from "../shared/Tabs";
|
|
8
9
|
import type { LinkValue } from "../../schemas/link";
|
|
9
10
|
import { cn } from "../../lib/cn";
|
|
10
11
|
|
|
@@ -17,6 +18,7 @@ interface SettingsFormProps {
|
|
|
17
18
|
schema: SettingsSchema;
|
|
18
19
|
values: Record<string, unknown>;
|
|
19
20
|
onChange: (result: SettingsFormResult) => void;
|
|
21
|
+
tabs?: { label: string; fields: string[] }[];
|
|
20
22
|
}
|
|
21
23
|
|
|
22
24
|
function resolveValues(
|
|
@@ -37,6 +39,13 @@ function coerceValue(field: SettingsFieldDef, value: unknown): unknown {
|
|
|
37
39
|
return value;
|
|
38
40
|
}
|
|
39
41
|
|
|
42
|
+
function normalizeEmpty(field: SettingsFieldDef, value: unknown): unknown {
|
|
43
|
+
if (field.type === "select" && "emptyIsUndefined" in field && field.emptyIsUndefined && value === "") {
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|
|
46
|
+
return value;
|
|
47
|
+
}
|
|
48
|
+
|
|
40
49
|
function splitByTarget(
|
|
41
50
|
schema: SettingsSchema,
|
|
42
51
|
allValues: Record<string, unknown>,
|
|
@@ -44,7 +53,7 @@ function splitByTarget(
|
|
|
44
53
|
const content: Record<string, unknown> = {};
|
|
45
54
|
const options: Record<string, unknown> = {};
|
|
46
55
|
for (const [key, field] of Object.entries(schema)) {
|
|
47
|
-
const value = coerceValue(field, allValues[key]);
|
|
56
|
+
const value = normalizeEmpty(field, coerceValue(field, allValues[key]));
|
|
48
57
|
const target = field.target ?? "options";
|
|
49
58
|
if (target === "content") {
|
|
50
59
|
content[key] = value;
|
|
@@ -84,7 +93,82 @@ function RangeField({
|
|
|
84
93
|
);
|
|
85
94
|
}
|
|
86
95
|
|
|
87
|
-
|
|
96
|
+
function renderField(
|
|
97
|
+
key: string,
|
|
98
|
+
field: SettingsFieldDef,
|
|
99
|
+
value: unknown,
|
|
100
|
+
handleFieldChange: (key: string, newValue: unknown) => void,
|
|
101
|
+
) {
|
|
102
|
+
switch (field.type) {
|
|
103
|
+
case "text":
|
|
104
|
+
return (
|
|
105
|
+
<Input
|
|
106
|
+
key={key}
|
|
107
|
+
label={field.label}
|
|
108
|
+
value={String(value ?? "")}
|
|
109
|
+
placeholder={field.placeholder}
|
|
110
|
+
onChange={(v) => handleFieldChange(key, v)}
|
|
111
|
+
/>
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
case "number":
|
|
115
|
+
return (
|
|
116
|
+
<Input
|
|
117
|
+
key={key}
|
|
118
|
+
label={field.label}
|
|
119
|
+
type="number"
|
|
120
|
+
value={String(value ?? "")}
|
|
121
|
+
onChange={(v) => handleFieldChange(key, v === "" ? "" : Number(v))}
|
|
122
|
+
/>
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
case "checkbox":
|
|
126
|
+
return (
|
|
127
|
+
<Checkbox
|
|
128
|
+
key={key}
|
|
129
|
+
label={field.label}
|
|
130
|
+
checked={Boolean(value)}
|
|
131
|
+
onChange={(checked) => handleFieldChange(key, checked)}
|
|
132
|
+
/>
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
case "select":
|
|
136
|
+
return (
|
|
137
|
+
<Select
|
|
138
|
+
key={key}
|
|
139
|
+
label={field.label}
|
|
140
|
+
value={String(value ?? "")}
|
|
141
|
+
options={field.options}
|
|
142
|
+
onChange={(v) => handleFieldChange(key, v)}
|
|
143
|
+
/>
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
case "range":
|
|
147
|
+
return (
|
|
148
|
+
<RangeField
|
|
149
|
+
key={key}
|
|
150
|
+
field={field}
|
|
151
|
+
value={Number(value ?? field.min)}
|
|
152
|
+
onChange={(v) => handleFieldChange(key, v)}
|
|
153
|
+
/>
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
case "link":
|
|
157
|
+
return (
|
|
158
|
+
<LinkField
|
|
159
|
+
key={key}
|
|
160
|
+
label={field.label}
|
|
161
|
+
value={(value as LinkValue) ?? field.default}
|
|
162
|
+
onChange={(v) => handleFieldChange(key, v)}
|
|
163
|
+
/>
|
|
164
|
+
);
|
|
165
|
+
|
|
166
|
+
default:
|
|
167
|
+
return null;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export function SettingsForm({ schema, values, onChange, tabs }: SettingsFormProps) {
|
|
88
172
|
const [local, setLocal] = useState(() => resolveValues(schema, values));
|
|
89
173
|
|
|
90
174
|
function handleFieldChange(key: string, newValue: unknown) {
|
|
@@ -93,79 +177,22 @@ export function SettingsForm({ schema, values, onChange }: SettingsFormProps) {
|
|
|
93
177
|
onChange(splitByTarget(schema, next));
|
|
94
178
|
}
|
|
95
179
|
|
|
180
|
+
if (tabs) {
|
|
181
|
+
const fields = (keys: string[]) =>
|
|
182
|
+
keys.filter((key) => key in schema).map((key) => renderField(key, schema[key], local[key], handleFieldChange));
|
|
183
|
+
|
|
184
|
+
const listed = new Set(tabs.flatMap((t) => t.fields));
|
|
185
|
+
const otherKeys = Object.keys(schema).filter((key) => !listed.has(key));
|
|
186
|
+
const tabItems = [
|
|
187
|
+
...tabs.map((t) => ({ id: t.label, label: t.label, content: <div className="flex flex-col gap-4">{fields(t.fields)}</div> })),
|
|
188
|
+
...(otherKeys.length > 0 ? [{ id: "__other__", label: "Other", content: <div className="flex flex-col gap-4">{fields(otherKeys)}</div> }] : []),
|
|
189
|
+
];
|
|
190
|
+
return <Tabs tabs={tabItems} fullBleedTabBar />;
|
|
191
|
+
}
|
|
192
|
+
|
|
96
193
|
return (
|
|
97
194
|
<div className="flex flex-col gap-4">
|
|
98
|
-
{Object.entries(schema).map(([key, field]) =>
|
|
99
|
-
const value = local[key];
|
|
100
|
-
|
|
101
|
-
switch (field.type) {
|
|
102
|
-
case "text":
|
|
103
|
-
return (
|
|
104
|
-
<Input
|
|
105
|
-
key={key}
|
|
106
|
-
label={field.label}
|
|
107
|
-
value={String(value ?? "")}
|
|
108
|
-
placeholder={field.placeholder}
|
|
109
|
-
onChange={(v) => handleFieldChange(key, v)}
|
|
110
|
-
/>
|
|
111
|
-
);
|
|
112
|
-
|
|
113
|
-
case "number":
|
|
114
|
-
return (
|
|
115
|
-
<Input
|
|
116
|
-
key={key}
|
|
117
|
-
label={field.label}
|
|
118
|
-
type="number"
|
|
119
|
-
value={String(value ?? "")}
|
|
120
|
-
onChange={(v) => handleFieldChange(key, v === "" ? "" : Number(v))}
|
|
121
|
-
/>
|
|
122
|
-
);
|
|
123
|
-
|
|
124
|
-
case "checkbox":
|
|
125
|
-
return (
|
|
126
|
-
<Checkbox
|
|
127
|
-
key={key}
|
|
128
|
-
label={field.label}
|
|
129
|
-
checked={Boolean(value)}
|
|
130
|
-
onChange={(checked) => handleFieldChange(key, checked)}
|
|
131
|
-
/>
|
|
132
|
-
);
|
|
133
|
-
|
|
134
|
-
case "select":
|
|
135
|
-
return (
|
|
136
|
-
<Select
|
|
137
|
-
key={key}
|
|
138
|
-
label={field.label}
|
|
139
|
-
value={String(value ?? "")}
|
|
140
|
-
options={field.options}
|
|
141
|
-
onChange={(v) => handleFieldChange(key, v)}
|
|
142
|
-
/>
|
|
143
|
-
);
|
|
144
|
-
|
|
145
|
-
case "range":
|
|
146
|
-
return (
|
|
147
|
-
<RangeField
|
|
148
|
-
key={key}
|
|
149
|
-
field={field}
|
|
150
|
-
value={Number(value ?? field.min)}
|
|
151
|
-
onChange={(v) => handleFieldChange(key, v)}
|
|
152
|
-
/>
|
|
153
|
-
);
|
|
154
|
-
|
|
155
|
-
case "link":
|
|
156
|
-
return (
|
|
157
|
-
<LinkField
|
|
158
|
-
key={key}
|
|
159
|
-
label={field.label}
|
|
160
|
-
value={(value as LinkValue) ?? field.default}
|
|
161
|
-
onChange={(v) => handleFieldChange(key, v)}
|
|
162
|
-
/>
|
|
163
|
-
);
|
|
164
|
-
|
|
165
|
-
default:
|
|
166
|
-
return null;
|
|
167
|
-
}
|
|
168
|
-
})}
|
|
195
|
+
{Object.entries(schema).map(([key, field]) => renderField(key, field, local[key], handleFieldChange))}
|
|
169
196
|
</div>
|
|
170
197
|
);
|
|
171
198
|
}
|
|
@@ -3,7 +3,7 @@ import { ImageIcon } from "lucide-react";
|
|
|
3
3
|
import { AddIcon, DragHandle, DeleteIcon, SettingsIcon } from "../shared/icons";
|
|
4
4
|
import { IconButton } from "../shared/IconButton";
|
|
5
5
|
import { cn } from "../../lib/cn";
|
|
6
|
-
import {
|
|
6
|
+
import { containerGridClass } from "../../lib/container-grid";
|
|
7
7
|
import { useEditableCollection } from "./useEditableCollection";
|
|
8
8
|
|
|
9
9
|
interface EditableGridProps<T> {
|
|
@@ -68,47 +68,51 @@ export function EditableGrid<T>({
|
|
|
68
68
|
|
|
69
69
|
if (!isEditMode) {
|
|
70
70
|
return (
|
|
71
|
-
<div className=
|
|
72
|
-
{
|
|
73
|
-
|
|
74
|
-
{
|
|
75
|
-
|
|
76
|
-
|
|
71
|
+
<div className="@container">
|
|
72
|
+
<div className={cn("grid gap-4", containerGridClass[columns] || containerGridClass[1], className)}>
|
|
73
|
+
{wrappedItems.map((wrapped, index) => (
|
|
74
|
+
<div key={wrapped.id}>
|
|
75
|
+
{renderItem(wrapped.data, { isEditMode: false, index })}
|
|
76
|
+
</div>
|
|
77
|
+
))}
|
|
78
|
+
</div>
|
|
77
79
|
</div>
|
|
78
80
|
);
|
|
79
81
|
}
|
|
80
82
|
|
|
81
83
|
return (
|
|
82
|
-
<div className=
|
|
83
|
-
{
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
84
|
+
<div className="@container">
|
|
85
|
+
<div className={cn("group/grid relative grid gap-4", containerGridClass[columns] || containerGridClass[1], className)}>
|
|
86
|
+
{wrappedItems.map((wrapped, index) => (
|
|
87
|
+
<GridCell
|
|
88
|
+
key={wrapped.id}
|
|
89
|
+
id={wrapped.id}
|
|
90
|
+
index={index}
|
|
91
|
+
isLast={index === wrappedItems.length - 1}
|
|
92
|
+
isDragging={isDragging}
|
|
93
|
+
onReorder={onReorder}
|
|
94
|
+
onRemove={onRemove}
|
|
95
|
+
onInsert={onInsert}
|
|
96
|
+
onSettings={onItemSettings ? () => onItemSettings(index) : undefined}
|
|
97
|
+
onImageClick={onItemImageClick ? () => onItemImageClick(index) : undefined}
|
|
98
|
+
chromeTopClass={chromeTopClass}
|
|
99
|
+
dragState={dragState}
|
|
100
|
+
setDragState={setDragState}
|
|
101
|
+
>
|
|
102
|
+
{renderItem(wrapped.data, { isEditMode: true, index })}
|
|
103
|
+
</GridCell>
|
|
104
|
+
))}
|
|
102
105
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
106
|
+
{/* Add button — absolute, centered below the grid */}
|
|
107
|
+
<IconButton
|
|
108
|
+
icon={<AddIcon size={16} />}
|
|
109
|
+
label="Add item"
|
|
110
|
+
size="lg"
|
|
111
|
+
intent="primary"
|
|
112
|
+
onClick={onAdd}
|
|
113
|
+
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"
|
|
114
|
+
/>
|
|
115
|
+
</div>
|
|
112
116
|
</div>
|
|
113
117
|
);
|
|
114
118
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { useEffect, useRef } from "react";
|
|
1
|
+
import { useEffect, useRef, useState } from "react";
|
|
2
|
+
import { Check, X } from "lucide-react";
|
|
2
3
|
import { cn } from "../../lib/cn";
|
|
3
4
|
import { curatedIcons } from "../../lib/icons";
|
|
4
5
|
|
|
@@ -7,10 +8,26 @@ interface IconPickerProps {
|
|
|
7
8
|
onSelect: (iconId: string | null) => void;
|
|
8
9
|
onClose: () => void;
|
|
9
10
|
showRemove?: boolean;
|
|
11
|
+
/** Opt-in: render the None / Do-Don't mode toggle. Default false → grid-only. */
|
|
12
|
+
allowDoDont?: boolean;
|
|
13
|
+
/** Current per-item Do/Don't tag (only meaningful when allowDoDont). */
|
|
14
|
+
dodont?: "do" | "dont";
|
|
15
|
+
/** Commit a Do/Don't choice; pass undefined to clear the tag (None). */
|
|
16
|
+
onSelectDoDont?: (v: "do" | "dont" | undefined) => void;
|
|
10
17
|
}
|
|
11
18
|
|
|
12
|
-
export function IconPicker({
|
|
19
|
+
export function IconPicker({
|
|
20
|
+
selected,
|
|
21
|
+
onSelect,
|
|
22
|
+
onClose,
|
|
23
|
+
showRemove = true,
|
|
24
|
+
allowDoDont = false,
|
|
25
|
+
dodont,
|
|
26
|
+
onSelectDoDont,
|
|
27
|
+
}: IconPickerProps) {
|
|
13
28
|
const panelRef = useRef<HTMLDivElement>(null);
|
|
29
|
+
// Open in the mode matching the item's current state.
|
|
30
|
+
const [mode, setMode] = useState<"none" | "dodont">(dodont ? "dodont" : "none");
|
|
14
31
|
|
|
15
32
|
useEffect(() => {
|
|
16
33
|
function handleMouseDown(e: MouseEvent) {
|
|
@@ -22,38 +39,111 @@ export function IconPicker({ selected, onSelect, onClose, showRemove = true }: I
|
|
|
22
39
|
return () => document.removeEventListener("mousedown", handleMouseDown);
|
|
23
40
|
}, [onClose]);
|
|
24
41
|
|
|
42
|
+
const showDoDont = allowDoDont && mode === "dodont";
|
|
43
|
+
|
|
25
44
|
return (
|
|
26
45
|
<div
|
|
27
46
|
ref={panelRef}
|
|
28
47
|
className="absolute z-50 w-56 rounded-lg border border-base-200 bg-base p-2 shadow-lg"
|
|
29
48
|
>
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
49
|
+
{allowDoDont && (
|
|
50
|
+
<div role="tablist" aria-label="Icon mode" className="mb-2 flex gap-1 rounded-md bg-base-200 p-0.5">
|
|
51
|
+
<button
|
|
52
|
+
type="button"
|
|
53
|
+
role="tab"
|
|
54
|
+
aria-label="None"
|
|
55
|
+
aria-selected={mode === "none"}
|
|
56
|
+
className={cn(
|
|
57
|
+
"cursor-pointer flex-1 rounded px-2 py-1 text-xs font-medium transition-colors",
|
|
58
|
+
mode === "none"
|
|
59
|
+
? "bg-base text-base-contrast shadow-sm"
|
|
60
|
+
: "text-base-contrast-light hover:text-base-contrast",
|
|
61
|
+
)}
|
|
62
|
+
onClick={() => {
|
|
63
|
+
setMode("none");
|
|
64
|
+
onSelectDoDont?.(undefined);
|
|
65
|
+
}}
|
|
66
|
+
>
|
|
67
|
+
None
|
|
68
|
+
</button>
|
|
69
|
+
<button
|
|
70
|
+
type="button"
|
|
71
|
+
role="tab"
|
|
72
|
+
aria-label="Do / Don't"
|
|
73
|
+
aria-selected={mode === "dodont"}
|
|
74
|
+
className={cn(
|
|
75
|
+
"cursor-pointer flex-1 rounded px-2 py-1 text-xs font-medium transition-colors",
|
|
76
|
+
mode === "dodont"
|
|
77
|
+
? "bg-base text-base-contrast shadow-sm"
|
|
78
|
+
: "text-base-contrast-light hover:text-base-contrast",
|
|
79
|
+
)}
|
|
80
|
+
onClick={() => setMode("dodont")}
|
|
81
|
+
>
|
|
82
|
+
Do / Don't
|
|
83
|
+
</button>
|
|
84
|
+
</div>
|
|
85
|
+
)}
|
|
86
|
+
|
|
87
|
+
{showDoDont ? (
|
|
88
|
+
<div className="grid grid-cols-2 gap-2">
|
|
89
|
+
<button
|
|
90
|
+
type="button"
|
|
91
|
+
aria-label="Do"
|
|
92
|
+
className={cn(
|
|
93
|
+
"cursor-pointer flex flex-col items-center justify-center gap-1 rounded py-2 transition-colors",
|
|
94
|
+
"text-green-600 hover:bg-base-accent",
|
|
95
|
+
dodont === "do" && "ring-2 ring-primary bg-base-accent",
|
|
96
|
+
)}
|
|
97
|
+
onClick={() => onSelectDoDont?.("do")}
|
|
98
|
+
>
|
|
99
|
+
<Check size={20} />
|
|
100
|
+
<span className="text-xs font-medium">Do</span>
|
|
101
|
+
</button>
|
|
102
|
+
<button
|
|
103
|
+
type="button"
|
|
104
|
+
aria-label="Don't"
|
|
105
|
+
className={cn(
|
|
106
|
+
"cursor-pointer flex flex-col items-center justify-center gap-1 rounded py-2 transition-colors",
|
|
107
|
+
"text-red-600 hover:bg-base-accent",
|
|
108
|
+
dodont === "dont" && "ring-2 ring-primary bg-base-accent",
|
|
109
|
+
)}
|
|
110
|
+
onClick={() => onSelectDoDont?.("dont")}
|
|
111
|
+
>
|
|
112
|
+
<X size={20} />
|
|
113
|
+
<span className="text-xs font-medium">Don't</span>
|
|
114
|
+
</button>
|
|
115
|
+
</div>
|
|
116
|
+
) : (
|
|
117
|
+
<>
|
|
118
|
+
<div className="grid grid-cols-5 gap-1">
|
|
119
|
+
{curatedIcons.map((entry) => {
|
|
120
|
+
const Icon = entry.icon;
|
|
121
|
+
const isSelected = selected === entry.id;
|
|
122
|
+
return (
|
|
123
|
+
<button
|
|
124
|
+
key={entry.id}
|
|
125
|
+
aria-label={entry.label}
|
|
126
|
+
className={cn(
|
|
127
|
+
"cursor-pointer flex h-9 w-9 items-center justify-center rounded transition-colors",
|
|
128
|
+
"text-base-contrast-light hover:bg-base-accent hover:text-base-contrast",
|
|
129
|
+
isSelected && "ring-2 ring-primary bg-base-accent text-primary",
|
|
130
|
+
)}
|
|
131
|
+
onClick={() => onSelect(entry.id)}
|
|
132
|
+
>
|
|
133
|
+
<Icon size={18} />
|
|
134
|
+
</button>
|
|
135
|
+
);
|
|
136
|
+
})}
|
|
137
|
+
</div>
|
|
138
|
+
{showRemove && (
|
|
35
139
|
<button
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
className={cn(
|
|
39
|
-
"cursor-pointer flex h-9 w-9 items-center justify-center rounded transition-colors",
|
|
40
|
-
"text-base-contrast-light hover:bg-base-accent hover:text-base-contrast",
|
|
41
|
-
isSelected && "ring-2 ring-primary bg-base-accent text-primary",
|
|
42
|
-
)}
|
|
43
|
-
onClick={() => onSelect(entry.id)}
|
|
140
|
+
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"
|
|
141
|
+
onClick={() => onSelect(null)}
|
|
44
142
|
>
|
|
45
|
-
|
|
143
|
+
Remove icon
|
|
46
144
|
</button>
|
|
47
|
-
)
|
|
48
|
-
|
|
49
|
-
</div>
|
|
50
|
-
{showRemove && (
|
|
51
|
-
<button
|
|
52
|
-
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"
|
|
53
|
-
onClick={() => onSelect(null)}
|
|
54
|
-
>
|
|
55
|
-
Remove icon
|
|
56
|
-
</button>
|
|
145
|
+
)}
|
|
146
|
+
</>
|
|
57
147
|
)}
|
|
58
148
|
</div>
|
|
59
149
|
);
|