@drawnagency/primitives 0.1.66 → 0.1.68
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/chunk-4YEOTFVU.js +14 -0
- package/dist/{chunk-VU3XR362.js → chunk-6UDYJQL6.js} +1 -1
- package/dist/{chunk-7IMQVDDJ.js → chunk-EWKCEPMW.js} +1 -1
- package/dist/{chunk-XNGG5AGI.js → chunk-KWBM27UR.js} +1 -1
- package/dist/{chunk-TEYTLQVH.js → chunk-QKOIEWZ4.js} +33 -1
- package/dist/chunk-TYYCKVAW.js +968 -0
- package/dist/{chunk-VEIDANEU.js → chunk-VVK6ZRCN.js} +128 -41
- package/dist/{chunk-ZYXLNJ5M.js → chunk-WNPMBFD6.js} +4 -4
- package/dist/{chunk-DQTKWKSK.js → chunk-WXVKX6PY.js} +12 -3
- package/dist/components/chat/ChatPanel.d.ts +31 -0
- package/dist/components/chat/ChatPanel.d.ts.map +1 -0
- package/dist/components/chat/ChatWidget.d.ts +11 -0
- package/dist/components/chat/ChatWidget.d.ts.map +1 -0
- package/dist/components/primitives/defineHeadingSection.d.ts +2 -0
- package/dist/components/primitives/defineHeadingSection.d.ts.map +1 -1
- package/dist/components/primitives/index.js +5 -5
- package/dist/components/sections/Button/index.d.ts.map +1 -1
- package/dist/components/sections/Colors/index.d.ts.map +1 -1
- package/dist/components/sections/Colors/schema.d.ts.map +1 -1
- package/dist/components/sections/Container/index.d.ts.map +1 -1
- package/dist/components/sections/DoDontMedia/index.d.ts.map +1 -1
- package/dist/components/sections/IconList/IconList.d.ts +2 -1
- package/dist/components/sections/IconList/IconList.d.ts.map +1 -1
- package/dist/components/sections/IconList/IconListSettings.d.ts +2 -1
- 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/LinkHeading/index.d.ts.map +1 -1
- package/dist/components/sections/Media/index.d.ts.map +1 -1
- package/dist/components/sections/Prose/index.d.ts.map +1 -1
- package/dist/components/sections/Spacer/index.d.ts.map +1 -1
- package/dist/components/sections/SubHeading/index.d.ts.map +1 -1
- package/dist/components/sections/SubSubHeading/index.d.ts.map +1 -1
- package/dist/components/sections/all-sections.d.ts +1 -0
- package/dist/components/sections/all-sections.d.ts.map +1 -1
- package/dist/components/sections/brand-guide.d.ts +1 -0
- package/dist/components/sections/brand-guide.d.ts.map +1 -1
- package/dist/components/sections/brand-guide.js +5 -5
- package/dist/components/sections/register-schemas.js +13 -943
- package/dist/components/sections/register.js +36 -0
- package/dist/components/shell/ChatbotConfigModal.d.ts +13 -0
- package/dist/components/shell/ChatbotConfigModal.d.ts.map +1 -0
- package/dist/components/shell/EditorShell.d.ts +2 -1
- package/dist/components/shell/EditorShell.d.ts.map +1 -1
- package/dist/index.js +31 -24
- package/dist/lib/index.d.ts +1 -0
- package/dist/lib/index.d.ts.map +1 -1
- package/dist/lib/index.js +7 -4
- package/dist/lib/insert-menu.d.ts +19 -0
- package/dist/lib/insert-menu.d.ts.map +1 -0
- package/dist/lib/platform-broker.d.ts +7 -0
- package/dist/lib/platform-broker.d.ts.map +1 -1
- package/dist/lib/platform-broker.js +27 -1
- package/dist/lib/registry.d.ts +23 -0
- package/dist/lib/registry.d.ts.map +1 -1
- package/dist/media/index.js +3 -3
- package/dist/schemas/index.js +6 -2
- package/dist/schemas/site-config.d.ts +30 -0
- package/dist/schemas/site-config.d.ts.map +1 -1
- package/dist/types/database.d.ts +296 -34
- package/dist/types/database.d.ts.map +1 -1
- package/package.json +5 -1
- package/src/components/chat/ChatPanel.tsx +152 -0
- package/src/components/chat/ChatWidget.tsx +38 -0
- package/src/components/primitives/defineHeadingSection.tsx +18 -2
- package/src/components/sections/Button/index.tsx +10 -3
- package/src/components/sections/Colors/index.tsx +15 -7
- package/src/components/sections/Colors/schema.ts +5 -2
- package/src/components/sections/Container/index.tsx +19 -3
- package/src/components/sections/DoDontMedia/index.tsx +18 -7
- package/src/components/sections/IconList/IconList.tsx +43 -19
- package/src/components/sections/IconList/IconListSettings.tsx +58 -11
- package/src/components/sections/IconList/index.tsx +51 -7
- package/src/components/sections/LinkHeading/index.tsx +2 -0
- package/src/components/sections/Media/index.tsx +15 -6
- package/src/components/sections/Prose/index.tsx +9 -1
- package/src/components/sections/Spacer/index.tsx +2 -0
- package/src/components/sections/SubHeading/index.tsx +2 -0
- package/src/components/sections/SubSubHeading/index.tsx +2 -0
- package/src/components/shell/ChatbotConfigModal.tsx +152 -0
- package/src/components/shell/EditorShell.tsx +41 -20
- package/src/lib/index.ts +1 -0
- package/src/lib/insert-menu.ts +42 -0
- package/src/lib/platform-broker.ts +36 -0
- package/src/lib/registry.ts +24 -0
- package/src/schemas/site-config.ts +36 -0
- package/src/types/database.ts +284 -36
- package/dist/chunk-ZGFAYZWB.js +0 -8
|
@@ -1,45 +1,89 @@
|
|
|
1
1
|
import { useState } from "react";
|
|
2
|
+
import { Check, X } from "lucide-react";
|
|
2
3
|
import { Checkbox } from "../../shared/Checkbox";
|
|
3
4
|
import { FormLabel } from "../../shared/FormLabel";
|
|
4
5
|
import { IconPicker } from "../../primitives/IconPicker";
|
|
5
6
|
import { getIcon } from "../../../lib/icons";
|
|
7
|
+
import { cn } from "../../../lib/cn";
|
|
6
8
|
|
|
7
9
|
export function IconListSettings({
|
|
8
10
|
icon: initialIcon,
|
|
9
11
|
showLabel: initialShowLabel,
|
|
10
12
|
stackText: initialStackText,
|
|
13
|
+
dodont: initialDodont,
|
|
11
14
|
onChange,
|
|
12
15
|
}: {
|
|
13
16
|
icon: string | null;
|
|
14
17
|
showLabel: boolean;
|
|
15
18
|
stackText: boolean;
|
|
19
|
+
dodont?: "do" | "dont";
|
|
16
20
|
onChange: (result: { content: Record<string, unknown>; options: Record<string, unknown> }) => void;
|
|
17
21
|
}) {
|
|
18
22
|
const [icon, setIcon] = useState(initialIcon);
|
|
19
23
|
const [showLabel, setShowLabel] = useState(initialShowLabel);
|
|
20
24
|
const [stackText, setStackText] = useState(initialStackText);
|
|
25
|
+
const [dodont, setDodont] = useState<"do" | "dont" | undefined>(initialDodont);
|
|
21
26
|
const [pickerOpen, setPickerOpen] = useState(false);
|
|
22
27
|
|
|
23
|
-
const emit = (
|
|
24
|
-
|
|
28
|
+
const emit = (
|
|
29
|
+
overrides: Partial<{ icon: string | null; showLabel: boolean; stackText: boolean; dodont: "do" | "dont" | undefined }>,
|
|
30
|
+
) => onChange({ content: {}, options: { icon, showLabel, stackText, dodont, ...overrides } });
|
|
25
31
|
|
|
26
|
-
const showIcons = icon !== null;
|
|
27
32
|
const iconEntry = icon ? getIcon(icon) : null;
|
|
33
|
+
const dodontActive = dodont === "do" || dodont === "dont";
|
|
34
|
+
|
|
35
|
+
const tile = (value: "do" | "dont" | undefined, label: string, aria: string, glyph: React.ReactNode, tint: string) => (
|
|
36
|
+
<button
|
|
37
|
+
type="button"
|
|
38
|
+
aria-label={aria}
|
|
39
|
+
aria-pressed={dodont === value}
|
|
40
|
+
className={cn(
|
|
41
|
+
"cursor-pointer flex flex-col items-center gap-1 rounded border py-2 transition-colors",
|
|
42
|
+
tint,
|
|
43
|
+
dodont === value ? "border-primary bg-base-accent" : "border-base-200",
|
|
44
|
+
)}
|
|
45
|
+
onClick={() => { setDodont(value); emit({ dodont: value }); }}
|
|
46
|
+
>
|
|
47
|
+
{glyph}
|
|
48
|
+
<span className="text-xs font-medium">{label}</span>
|
|
49
|
+
</button>
|
|
50
|
+
);
|
|
28
51
|
|
|
29
52
|
return (
|
|
30
53
|
<div className="space-y-4">
|
|
54
|
+
<div>
|
|
55
|
+
<FormLabel>Do / Don't list</FormLabel>
|
|
56
|
+
<div className="grid grid-cols-3 gap-2">
|
|
57
|
+
{tile(
|
|
58
|
+
undefined,
|
|
59
|
+
"None",
|
|
60
|
+
"No do/don't",
|
|
61
|
+
<span className="flex h-5 w-5 items-center justify-center rounded-sm border border-dashed border-base-contrast-light/40" />,
|
|
62
|
+
"text-base-contrast-light",
|
|
63
|
+
)}
|
|
64
|
+
{tile("do", "Do", "Do list", <Check size={20} />, "text-green-600")}
|
|
65
|
+
{tile("dont", "Don't", "Don't list", <X size={20} />, "text-red-600")}
|
|
66
|
+
</div>
|
|
67
|
+
</div>
|
|
68
|
+
|
|
31
69
|
<div className="relative">
|
|
32
70
|
<FormLabel htmlFor="icon-picker-btn">Default Icon</FormLabel>
|
|
33
71
|
<div className="flex items-center gap-2">
|
|
34
72
|
<button
|
|
35
73
|
id="icon-picker-btn"
|
|
36
|
-
|
|
37
|
-
|
|
74
|
+
disabled={dodontActive}
|
|
75
|
+
className={cn(
|
|
76
|
+
"flex items-center gap-2 rounded border border-base-200 px-3 py-1.5 text-sm text-base-contrast-light",
|
|
77
|
+
dodontActive
|
|
78
|
+
? "cursor-not-allowed opacity-50"
|
|
79
|
+
: "cursor-pointer hover:border-primary hover:text-primary",
|
|
80
|
+
)}
|
|
81
|
+
onClick={() => { if (!dodontActive) setPickerOpen((prev) => !prev); }}
|
|
38
82
|
>
|
|
39
83
|
{iconEntry && <iconEntry.icon size={16} />}
|
|
40
84
|
<span>{iconEntry?.label ?? "None"}</span>
|
|
41
85
|
</button>
|
|
42
|
-
{icon && (
|
|
86
|
+
{icon && !dodontActive && (
|
|
43
87
|
<button
|
|
44
88
|
className="cursor-pointer text-xs text-base-contrast-light hover:text-base-contrast"
|
|
45
89
|
onClick={() => { setIcon(null); emit({ icon: null }); }}
|
|
@@ -48,15 +92,17 @@ export function IconListSettings({
|
|
|
48
92
|
</button>
|
|
49
93
|
)}
|
|
50
94
|
</div>
|
|
51
|
-
{
|
|
95
|
+
{dodontActive && (
|
|
96
|
+
<p className="mt-1 text-xs text-base-contrast-light/70">
|
|
97
|
+
Whole-list Do/Don't overrides the default icon.
|
|
98
|
+
</p>
|
|
99
|
+
)}
|
|
100
|
+
{pickerOpen && !dodontActive && (
|
|
52
101
|
<div className="absolute top-full left-0 z-50 mt-1">
|
|
53
102
|
<IconPicker
|
|
54
103
|
selected={icon}
|
|
55
104
|
onSelect={(newIcon) => {
|
|
56
|
-
if (newIcon !== null) {
|
|
57
|
-
setIcon(newIcon);
|
|
58
|
-
emit({ icon: newIcon });
|
|
59
|
-
}
|
|
105
|
+
if (newIcon !== null) { setIcon(newIcon); emit({ icon: newIcon }); }
|
|
60
106
|
setPickerOpen(false);
|
|
61
107
|
}}
|
|
62
108
|
onClose={() => setPickerOpen(false)}
|
|
@@ -64,6 +110,7 @@ export function IconListSettings({
|
|
|
64
110
|
</div>
|
|
65
111
|
)}
|
|
66
112
|
</div>
|
|
113
|
+
|
|
67
114
|
<Checkbox
|
|
68
115
|
checked={showLabel}
|
|
69
116
|
onChange={(v) => { setShowLabel(v); emit({ showLabel: v }); }}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { defineSection } from "../../../lib/registry";
|
|
2
2
|
import { z } from "zod";
|
|
3
|
-
import { List } from "lucide-react";
|
|
3
|
+
import { List, Columns2 } from "lucide-react";
|
|
4
|
+
import type { SectionPreset } from "../../../lib/registry";
|
|
5
|
+
import type { SectionContent } from "../../../schemas/sections";
|
|
4
6
|
import IconList from "./IconList";
|
|
5
7
|
import { IconListSettings } from "./IconListSettings";
|
|
6
8
|
import { pluralize } from "../../../lib/text";
|
|
@@ -16,27 +18,68 @@ export type IconListItem = z.infer<typeof IconListItemSchema>;
|
|
|
16
18
|
const schema = z.object({
|
|
17
19
|
type: z.literal("icon_list"),
|
|
18
20
|
content: z.object({
|
|
19
|
-
items: z
|
|
21
|
+
items: z
|
|
22
|
+
.array(IconListItemSchema)
|
|
23
|
+
.describe(
|
|
24
|
+
"The list entries. Each item has a label, descriptive text, an optional icon, and an optional do/don't marker.",
|
|
25
|
+
),
|
|
20
26
|
}),
|
|
21
|
-
options: z
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
27
|
+
options: z
|
|
28
|
+
.object({
|
|
29
|
+
icon: z
|
|
30
|
+
.string()
|
|
31
|
+
.nullable()
|
|
32
|
+
.describe("Default icon name applied to items that don't specify their own; null for no icon.")
|
|
33
|
+
.optional(),
|
|
34
|
+
showLabel: z.boolean().describe("When true, render each item's label.").optional(),
|
|
35
|
+
stackText: z
|
|
36
|
+
.boolean()
|
|
37
|
+
.describe("When true, stack the label and text vertically instead of inline.")
|
|
38
|
+
.optional(),
|
|
39
|
+
dodont: z
|
|
40
|
+
.enum(["do", "dont"])
|
|
41
|
+
.describe("Whole-list do/don't mode: render every item with a green check (do) or red x (don't).")
|
|
42
|
+
.optional(),
|
|
43
|
+
})
|
|
44
|
+
.optional(),
|
|
26
45
|
});
|
|
27
46
|
|
|
47
|
+
// One-click "Do / Don't list": a 2-column container holding a green-check "Do"
|
|
48
|
+
// icon_list and a red-x "Don't" icon_list, using the whole-list options.dodont
|
|
49
|
+
// default. Same shape the legacy do_dont migration produces. Lives here (brand-guide
|
|
50
|
+
// layer); it reaches the core Add-menu only via SectionDefinition.presets.
|
|
51
|
+
const doDontListPreset: SectionPreset = {
|
|
52
|
+
id: "do_dont_list",
|
|
53
|
+
label: "Do / Don't list",
|
|
54
|
+
icon: <Columns2 size={18} />,
|
|
55
|
+
build: (newId): SectionContent => ({
|
|
56
|
+
type: "container",
|
|
57
|
+
content: {
|
|
58
|
+
columns: 2,
|
|
59
|
+
flow: "row",
|
|
60
|
+
children: [
|
|
61
|
+
{ id: newId(), type: "icon_list", content: { items: [{ label: "", text: "" }] }, options: { dodont: "do" } },
|
|
62
|
+
{ id: newId(), type: "icon_list", content: { items: [{ label: "", text: "" }] }, options: { dodont: "dont" } },
|
|
63
|
+
],
|
|
64
|
+
},
|
|
65
|
+
}),
|
|
66
|
+
};
|
|
67
|
+
|
|
28
68
|
export default defineSection({
|
|
29
69
|
type: "icon_list",
|
|
30
70
|
category: "brand-guide",
|
|
31
71
|
label: "Icon List",
|
|
32
72
|
icon: <List size={18} />,
|
|
33
73
|
schema,
|
|
74
|
+
authoringNote:
|
|
75
|
+
"A list of labeled items, optionally with per-item icons and do/don't markers. Use for feature lists, guidelines, or do/don't guidance.",
|
|
34
76
|
component: ({ content, options, onChange }) => (
|
|
35
77
|
<IconList
|
|
36
78
|
items={content.content.items}
|
|
37
79
|
icon={options?.icon as string | null}
|
|
38
80
|
showLabel={options?.showLabel as boolean}
|
|
39
81
|
stackText={options?.stackText as boolean}
|
|
82
|
+
dodont={options?.dodont as "do" | "dont" | undefined}
|
|
40
83
|
onChange={onChange ? (c) => onChange(c as typeof content) : undefined}
|
|
41
84
|
/>
|
|
42
85
|
),
|
|
@@ -46,4 +89,5 @@ export default defineSection({
|
|
|
46
89
|
}),
|
|
47
90
|
getLabel: (content) => pluralize(content.content.items.length, "icon"),
|
|
48
91
|
settingsForm: IconListSettings,
|
|
92
|
+
presets: [doDontListPreset],
|
|
49
93
|
});
|
|
@@ -9,4 +9,6 @@ export default defineHeadingSection({
|
|
|
9
9
|
tag: "h2",
|
|
10
10
|
placeholder: "Section heading",
|
|
11
11
|
defaultHeading: "New Section",
|
|
12
|
+
authoringNote:
|
|
13
|
+
"A top-level section heading (renders an <h2>, reported to navigation as h1). Use it to begin a major named section of the guide; its text becomes a navigation entry.",
|
|
12
14
|
});
|
|
@@ -8,15 +8,22 @@ import { MediaBlock } from "./MediaBlock";
|
|
|
8
8
|
const schema = z.object({
|
|
9
9
|
type: z.literal("media"),
|
|
10
10
|
content: z.object({
|
|
11
|
-
ref: SingleMediaReferenceSchema
|
|
12
|
-
|
|
11
|
+
ref: SingleMediaReferenceSchema.describe(
|
|
12
|
+
"The image or video asset to display (a media reference by id). Required.",
|
|
13
|
+
),
|
|
14
|
+
link: LinkValueSchema.describe(
|
|
15
|
+
"Optional link wrapping the media — an internal page or a safe external URL.",
|
|
16
|
+
).optional(),
|
|
13
17
|
}),
|
|
14
18
|
options: z
|
|
15
19
|
.object({
|
|
16
|
-
square: z.boolean().optional(),
|
|
17
|
-
showCaption: z.boolean().optional(),
|
|
18
|
-
border: z.boolean().optional(),
|
|
19
|
-
objectFit: z
|
|
20
|
+
square: z.boolean().describe("When true, crop the media to a 1:1 square aspect ratio.").optional(),
|
|
21
|
+
showCaption: z.boolean().describe("When true, render the asset's caption beneath it.").optional(),
|
|
22
|
+
border: z.boolean().describe("When true, draw a border around the media.").optional(),
|
|
23
|
+
objectFit: z
|
|
24
|
+
.enum(["cover", "contain"])
|
|
25
|
+
.describe("How the image fills its box: 'contain' (fit whole image) or 'cover' (fill and crop).")
|
|
26
|
+
.optional(),
|
|
20
27
|
})
|
|
21
28
|
.default({}),
|
|
22
29
|
});
|
|
@@ -26,6 +33,8 @@ export default defineSection({
|
|
|
26
33
|
label: "Media",
|
|
27
34
|
icon: <ImageIcon size={18} />,
|
|
28
35
|
schema,
|
|
36
|
+
authoringNote:
|
|
37
|
+
"A single image or video with optional caption and link. Use to show one asset; for several assets side by side, place multiple Media blocks inside a Container.",
|
|
29
38
|
component: ({ content, options, onChange }) => (
|
|
30
39
|
<MediaBlock
|
|
31
40
|
refValue={content.content.ref}
|
|
@@ -6,7 +6,13 @@ import { stripHtmlToPlainText, truncate } from "../../../lib/text";
|
|
|
6
6
|
|
|
7
7
|
const schema = z.object({
|
|
8
8
|
type: z.literal("prose"),
|
|
9
|
-
content: z.object({
|
|
9
|
+
content: z.object({
|
|
10
|
+
body: z
|
|
11
|
+
.string()
|
|
12
|
+
.describe(
|
|
13
|
+
"Rich-text HTML for the paragraph block. Author with the editor; stored as sanitized HTML.",
|
|
14
|
+
),
|
|
15
|
+
}),
|
|
10
16
|
});
|
|
11
17
|
|
|
12
18
|
export default defineSection({
|
|
@@ -14,6 +20,8 @@ export default defineSection({
|
|
|
14
20
|
label: "Prose",
|
|
15
21
|
icon: <AlignLeft size={18} />,
|
|
16
22
|
schema,
|
|
23
|
+
authoringNote:
|
|
24
|
+
"A block of rich-text body copy (paragraphs, lists, inline emphasis, links). Use for any explanatory prose; author with the editor, not raw HTML.",
|
|
17
25
|
richTextFields: ["body"],
|
|
18
26
|
component: ({ content, onChange }) => (
|
|
19
27
|
<Prose body={content.content.body} onChange={onChange ? (c) => onChange(c as typeof content) : undefined} />
|
|
@@ -13,6 +13,8 @@ export default defineSection({
|
|
|
13
13
|
label: "Spacer",
|
|
14
14
|
icon: <SquareDashed size={18} />,
|
|
15
15
|
schema,
|
|
16
|
+
authoringNote:
|
|
17
|
+
"Vertical whitespace between sections. Use sparingly to add breathing room where default section spacing is insufficient.",
|
|
16
18
|
component: () => <Spacer />,
|
|
17
19
|
defaults: () => ({ type: "spacer" as const, content: {} }),
|
|
18
20
|
});
|
|
@@ -10,4 +10,6 @@ export default defineHeadingSection({
|
|
|
10
10
|
placeholder: "Sub heading",
|
|
11
11
|
defaultHeading: "New Sub Heading",
|
|
12
12
|
navExclusion: true,
|
|
13
|
+
authoringNote:
|
|
14
|
+
"A second-level heading (renders <h3>, nav level h2). Use under a Link Heading to divide a section; toggle Exclude from navigation to keep it out of the nav tree.",
|
|
13
15
|
});
|
|
@@ -10,4 +10,6 @@ export default defineHeadingSection({
|
|
|
10
10
|
placeholder: "Sub sub heading",
|
|
11
11
|
defaultHeading: "New Sub Sub Heading",
|
|
12
12
|
navExclusion: true,
|
|
13
|
+
authoringNote:
|
|
14
|
+
"A third-level heading (renders <h4>, nav level h3). Use for the finest-grained labeled subsection; toggle Exclude from navigation to omit it from the nav tree.",
|
|
13
15
|
});
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
import { EditorModal } from "./EditorModal";
|
|
3
|
+
import { Button } from "../shared/Button";
|
|
4
|
+
import { cn } from "../../lib/cn";
|
|
5
|
+
import ChatPanel from "../chat/ChatPanel";
|
|
6
|
+
import type { SiteConfig, ChatbotConfig, ChatbotTask } from "../../schemas/site-config";
|
|
7
|
+
import type { Audience } from "../../auth/types";
|
|
8
|
+
|
|
9
|
+
interface Props {
|
|
10
|
+
isOpen: boolean;
|
|
11
|
+
onClose: () => void;
|
|
12
|
+
siteConfig: SiteConfig;
|
|
13
|
+
onSiteConfigChange: (config: SiteConfig) => void;
|
|
14
|
+
audiences: Audience[];
|
|
15
|
+
platformEnabled: boolean;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function slugify(label: string): string {
|
|
19
|
+
return label.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 32) || "task";
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function AudienceChips({ audiences, selected, onToggle }: { audiences: Audience[]; selected: string[]; onToggle: (name: string) => void }) {
|
|
23
|
+
return (
|
|
24
|
+
<div className="flex flex-wrap gap-1">
|
|
25
|
+
{audiences.map((a) => (
|
|
26
|
+
<button key={a.name} type="button" onClick={() => onToggle(a.name)}
|
|
27
|
+
className={cn("rounded-full border px-2 py-0.5 text-xs",
|
|
28
|
+
selected.includes(a.name) ? "border-primary bg-primary text-primary-contrast" : "border-base-200 bg-base-accent text-base-contrast")}
|
|
29
|
+
style={a.color && selected.includes(a.name) ? { backgroundColor: a.color, borderColor: a.color } : undefined}>
|
|
30
|
+
{a.displayName}
|
|
31
|
+
</button>
|
|
32
|
+
))}
|
|
33
|
+
{audiences.length === 0 && <span className="text-xs text-base-contrast-light">No audiences configured</span>}
|
|
34
|
+
</div>
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function ChatbotConfigModal({ isOpen, onClose, siteConfig, onSiteConfigChange, audiences, platformEnabled }: Props) {
|
|
39
|
+
const chatbot = siteConfig.chatbot;
|
|
40
|
+
const [newTaskLabel, setNewTaskLabel] = useState("");
|
|
41
|
+
const [showTest, setShowTest] = useState(false);
|
|
42
|
+
|
|
43
|
+
const patch = (p: Partial<ChatbotConfig>) =>
|
|
44
|
+
onSiteConfigChange({ ...siteConfig, chatbot: { ...chatbot, ...p } });
|
|
45
|
+
|
|
46
|
+
const patchTask = (i: number, p: Partial<ChatbotTask>) =>
|
|
47
|
+
patch({ tasks: chatbot.tasks.map((t, idx) => (idx === i ? { ...t, ...p } : t)) });
|
|
48
|
+
|
|
49
|
+
function addTask() {
|
|
50
|
+
const label = newTaskLabel.trim();
|
|
51
|
+
if (!label || chatbot.tasks.length >= 20) return;
|
|
52
|
+
let id = slugify(label);
|
|
53
|
+
if (chatbot.tasks.some((t) => t.id === id)) {
|
|
54
|
+
for (let n = 2; ; n++) {
|
|
55
|
+
const suffix = `-${n}`;
|
|
56
|
+
const candidate = `${id.slice(0, 32 - suffix.length)}${suffix}`;
|
|
57
|
+
if (!chatbot.tasks.some((t) => t.id === candidate)) {
|
|
58
|
+
id = candidate;
|
|
59
|
+
break;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
patch({ tasks: [...chatbot.tasks, { id, label, prompt: "", access: [] }] });
|
|
64
|
+
setNewTaskLabel("");
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const toggleName = (list: string[], name: string) =>
|
|
68
|
+
list.includes(name) ? list.filter((n) => n !== name) : [...list, name];
|
|
69
|
+
|
|
70
|
+
return (
|
|
71
|
+
<EditorModal isOpen={isOpen} onClose={onClose} title="Chatbot" size="settings" noPadding>
|
|
72
|
+
<div className="flex flex-1 flex-col gap-4 overflow-y-auto px-6 py-4">
|
|
73
|
+
{!platformEnabled && (
|
|
74
|
+
<div className="rounded border border-base-200 bg-base-accent px-3 py-2 text-sm text-base-contrast">
|
|
75
|
+
Chat is not enabled for this site — contact Drawn Agency. You can still edit the
|
|
76
|
+
configuration below; it activates when chat is switched on.
|
|
77
|
+
</div>
|
|
78
|
+
)}
|
|
79
|
+
|
|
80
|
+
<label className="flex items-center gap-2 text-sm text-base-contrast">
|
|
81
|
+
<input type="checkbox" aria-label="Enable chatbot" checked={chatbot.enabled}
|
|
82
|
+
onChange={(e) => patch({ enabled: e.target.checked })} />
|
|
83
|
+
Show the chat widget to viewers
|
|
84
|
+
</label>
|
|
85
|
+
|
|
86
|
+
<div>
|
|
87
|
+
<p className="mb-1 text-sm font-medium text-base-contrast">Audiences</p>
|
|
88
|
+
<p className="mb-2 text-xs text-base-contrast-light">Who sees the widget. None selected = all audiences.</p>
|
|
89
|
+
<AudienceChips audiences={audiences} selected={chatbot.audiences} onToggle={(n) => patch({ audiences: toggleName(chatbot.audiences, n) })} />
|
|
90
|
+
</div>
|
|
91
|
+
|
|
92
|
+
<div>
|
|
93
|
+
<label htmlFor="chatbot-system-prompt" className="mb-1 block text-sm font-medium text-base-contrast">System prompt</label>
|
|
94
|
+
<textarea id="chatbot-system-prompt" aria-label="System prompt" rows={5} maxLength={8000}
|
|
95
|
+
value={chatbot.systemPrompt}
|
|
96
|
+
onChange={(e) => patch({ systemPrompt: e.target.value })}
|
|
97
|
+
placeholder="Describe the brand's voice and how the assistant should behave — e.g. “Speak warmly and concisely; always reference our color names, never hex codes.”"
|
|
98
|
+
className="w-full rounded border border-base-200 bg-base px-2 py-1 text-sm text-base-contrast" />
|
|
99
|
+
<p className="text-right text-xs text-base-contrast-light">{chatbot.systemPrompt.length}/8000</p>
|
|
100
|
+
</div>
|
|
101
|
+
|
|
102
|
+
<div>
|
|
103
|
+
<p className="mb-1 text-sm font-medium text-base-contrast">Tasks</p>
|
|
104
|
+
<p className="mb-2 text-xs text-base-contrast-light">Goal presets shown as a dropdown in the widget. Each task layers its own instructions onto the system prompt.</p>
|
|
105
|
+
<div className="space-y-3">
|
|
106
|
+
{chatbot.tasks.map((t, i) => (
|
|
107
|
+
<div key={t.id} className="rounded border border-base-200 p-2">
|
|
108
|
+
<div className="mb-1 flex items-center justify-between gap-2">
|
|
109
|
+
<input aria-label={`Task ${t.id} label`} value={t.label} maxLength={60}
|
|
110
|
+
onChange={(e) => patchTask(i, { label: e.target.value })}
|
|
111
|
+
className="min-w-0 flex-1 rounded border border-base-200 bg-base px-2 py-1 text-sm" />
|
|
112
|
+
<Button type="button" variant="destructive"
|
|
113
|
+
onClick={() => patch({ tasks: chatbot.tasks.filter((_, idx) => idx !== i) })}>
|
|
114
|
+
Remove
|
|
115
|
+
</Button>
|
|
116
|
+
</div>
|
|
117
|
+
<textarea aria-label={`Task ${t.id} prompt`} rows={2} maxLength={4000} value={t.prompt}
|
|
118
|
+
onChange={(e) => patchTask(i, { prompt: e.target.value })}
|
|
119
|
+
placeholder="Instructions for this task…"
|
|
120
|
+
className="mb-1 w-full rounded border border-base-200 bg-base px-2 py-1 text-sm" />
|
|
121
|
+
<AudienceChips audiences={audiences} selected={t.access} onToggle={(n) => patchTask(i, { access: toggleName(t.access, n) })} />
|
|
122
|
+
</div>
|
|
123
|
+
))}
|
|
124
|
+
</div>
|
|
125
|
+
<div className="mt-2 flex gap-2">
|
|
126
|
+
<input aria-label="New task label" value={newTaskLabel} maxLength={60}
|
|
127
|
+
onChange={(e) => setNewTaskLabel(e.target.value)}
|
|
128
|
+
placeholder="e.g. Draft a social post"
|
|
129
|
+
className="min-w-0 flex-1 rounded border border-base-200 bg-base px-2 py-1 text-sm" />
|
|
130
|
+
<Button type="button" onClick={addTask}>Add task</Button>
|
|
131
|
+
</div>
|
|
132
|
+
</div>
|
|
133
|
+
|
|
134
|
+
<div>
|
|
135
|
+
<Button type="button" onClick={() => setShowTest((s) => !s)} disabled={!platformEnabled}>
|
|
136
|
+
{showTest ? "Hide test chat" : "Test with current draft"}
|
|
137
|
+
</Button>
|
|
138
|
+
<p className="mt-1 text-xs text-base-contrast-light">
|
|
139
|
+
Tests your unsaved prompt and tasks. Answers ground on published content.
|
|
140
|
+
</p>
|
|
141
|
+
{showTest && platformEnabled && (
|
|
142
|
+
<ChatPanel
|
|
143
|
+
open onClose={() => setShowTest(false)} siteName={siteConfig.siteName}
|
|
144
|
+
tasks={chatbot.tasks.map((t) => ({ id: t.id, label: t.label }))}
|
|
145
|
+
isEditor draftChatbot={chatbot}
|
|
146
|
+
/>
|
|
147
|
+
)}
|
|
148
|
+
</div>
|
|
149
|
+
</div>
|
|
150
|
+
</EditorModal>
|
|
151
|
+
);
|
|
152
|
+
}
|
|
@@ -19,6 +19,7 @@ import type { BlockMove } from "../../lib/block-move";
|
|
|
19
19
|
import { EditorModalProvider, useEditorModal } from "./EditorModalContext";
|
|
20
20
|
import { EditorModal } from "./EditorModal";
|
|
21
21
|
import { SiteSettingsModal } from "./SiteSettingsModal";
|
|
22
|
+
import { ChatbotConfigModal } from "./ChatbotConfigModal";
|
|
22
23
|
import { MediaLibraryModal } from "./MediaLibraryModal";
|
|
23
24
|
import { MediaLibraryContext } from "./MediaLibraryContext";
|
|
24
25
|
import { ProcessingIndicator } from "./ProcessingIndicator";
|
|
@@ -27,6 +28,7 @@ import { SectionTypePicker } from "./SectionTypePicker";
|
|
|
27
28
|
import { ensureSectionsRegistered } from "../sections/register";
|
|
28
29
|
import { getSection, getAllSections } from "../../lib/registry";
|
|
29
30
|
import { childInsertableTypes, insertChildAt } from "../../lib/container-ops";
|
|
31
|
+
import { buildInsertMenuOptions, resolveInsertedContent } from "../../lib/insert-menu";
|
|
30
32
|
|
|
31
33
|
ensureSectionsRegistered();
|
|
32
34
|
import { BugReportFAB } from "./BugReportFAB";
|
|
@@ -71,7 +73,7 @@ import { IconButton } from "../shared/IconButton";
|
|
|
71
73
|
import { SegmentedControl } from "../shared/SegmentedControl";
|
|
72
74
|
import { computeDisplaySections } from "./view-branch";
|
|
73
75
|
import { SettingsIcon } from "../shared/icons";
|
|
74
|
-
import { ImageIcon, ListOrderedIcon, FilesIcon, X } from "lucide-react";
|
|
76
|
+
import { ImageIcon, ListOrderedIcon, FilesIcon, MessageCircleIcon, X } from "lucide-react";
|
|
75
77
|
import { ErrorBoundary } from "../shared/ErrorBoundary";
|
|
76
78
|
import { HistoryToolbar } from "./HistoryToolbar";
|
|
77
79
|
import { RestoreModal } from "./RestoreModal";
|
|
@@ -103,6 +105,10 @@ interface Props {
|
|
|
103
105
|
passwordToggle: boolean;
|
|
104
106
|
};
|
|
105
107
|
currentUser: { email: string; role: "owner" | "editor" } | null;
|
|
108
|
+
// Whether the platform (admin) has enabled the chatbot for this site.
|
|
109
|
+
// Controls the configurator's banner + test panel; the config UI itself
|
|
110
|
+
// always works so editors can prepare before activation.
|
|
111
|
+
chatbotPlatformEnabled?: boolean;
|
|
106
112
|
}
|
|
107
113
|
|
|
108
114
|
export default function EditorShell({
|
|
@@ -112,6 +118,7 @@ export default function EditorShell({
|
|
|
112
118
|
audiences: initialAudiences,
|
|
113
119
|
capabilities,
|
|
114
120
|
currentUser,
|
|
121
|
+
chatbotPlatformEnabled = false,
|
|
115
122
|
}: Props) {
|
|
116
123
|
const [shellState, setShellState] = useState<ShellState>({ phase: "loading-content" });
|
|
117
124
|
const [sections, setSections] = useState<LoadedSection[]>([]);
|
|
@@ -142,6 +149,7 @@ export default function EditorShell({
|
|
|
142
149
|
const [showRestoreModal, setShowRestoreModal] = useState(false);
|
|
143
150
|
const [showOrderingModal, setShowOrderingModal] = useState(false);
|
|
144
151
|
const [showPagesModal, setShowPagesModal] = useState(false);
|
|
152
|
+
const [showChatbotConfig, setShowChatbotConfig] = useState(false);
|
|
145
153
|
const [isRestoring, setIsRestoring] = useState(false);
|
|
146
154
|
const [rejectedUploads, setRejectedUploads] = useState<{ name: string; reason: "size" | "type" }[]>([]);
|
|
147
155
|
const [activePageId, setActivePageId] = useState<string | null>(null);
|
|
@@ -591,13 +599,16 @@ export default function EditorShell({
|
|
|
591
599
|
);
|
|
592
600
|
|
|
593
601
|
const onAddSection = useCallback(
|
|
594
|
-
(insertIndex: number,
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
const
|
|
602
|
+
(insertIndex: number, selection: string) => {
|
|
603
|
+
if (!resolvedActivePage) return;
|
|
604
|
+
const newId = () =>
|
|
605
|
+
typeof crypto !== "undefined" && typeof crypto.randomUUID === "function"
|
|
606
|
+
? crypto.randomUUID()
|
|
607
|
+
: `_id_${Date.now()}_${Math.random().toString(36).slice(2)}`;
|
|
608
|
+
const content = resolveInsertedContent(selection, getAllSections(), newId);
|
|
609
|
+
if (!content) return;
|
|
610
|
+
const id = newId();
|
|
611
|
+
const meta = { type: content.type, status: "draft" as const, access: [] as string[] };
|
|
601
612
|
const newSection: LoadedSection = { section: { id, ...content } as LoadedSection["section"], meta };
|
|
602
613
|
|
|
603
614
|
setSections((prev) => [...prev, newSection]);
|
|
@@ -883,6 +894,7 @@ export default function EditorShell({
|
|
|
883
894
|
onRestoreClick={() => setShowRestoreModal(true)}
|
|
884
895
|
onOrderingClick={() => setShowOrderingModal(true)}
|
|
885
896
|
onPagesClick={() => setShowPagesModal(true)}
|
|
897
|
+
onChatbotClick={() => setShowChatbotConfig(true)}
|
|
886
898
|
/>
|
|
887
899
|
|
|
888
900
|
{/* Bug reports persist via Supabase. On the password-only (zero-Supabase)
|
|
@@ -933,6 +945,16 @@ export default function EditorShell({
|
|
|
933
945
|
capabilities={capabilities}
|
|
934
946
|
currentUser={currentUser}
|
|
935
947
|
/>
|
|
948
|
+
{siteConfig && (
|
|
949
|
+
<ChatbotConfigModal
|
|
950
|
+
isOpen={showChatbotConfig}
|
|
951
|
+
onClose={() => setShowChatbotConfig(false)}
|
|
952
|
+
siteConfig={siteConfig}
|
|
953
|
+
onSiteConfigChange={handleSiteConfigChange}
|
|
954
|
+
audiences={audiences}
|
|
955
|
+
platformEnabled={chatbotPlatformEnabled}
|
|
956
|
+
/>
|
|
957
|
+
)}
|
|
936
958
|
<EditorModal
|
|
937
959
|
isOpen={pendingDeleteSectionId !== null}
|
|
938
960
|
onClose={() => setPendingDeleteSectionId(null)}
|
|
@@ -1189,7 +1211,7 @@ function EditorContent({
|
|
|
1189
1211
|
dirtySectionIds: Set<string>;
|
|
1190
1212
|
isPublishing: boolean;
|
|
1191
1213
|
onSectionChange: (sectionId: string, content: SectionContent) => void;
|
|
1192
|
-
onAddSection: (insertIndex: number,
|
|
1214
|
+
onAddSection: (insertIndex: number, selection: string) => void;
|
|
1193
1215
|
onDeleteSection: (sectionId: string) => void;
|
|
1194
1216
|
onReorderSections: (fromIndex: number, toIndex: number) => void;
|
|
1195
1217
|
onBlockMove: (move: BlockMove) => void;
|
|
@@ -1205,17 +1227,7 @@ function EditorContent({
|
|
|
1205
1227
|
const [pendingInsertIndex, setPendingInsertIndex] = useState<number | null>(null);
|
|
1206
1228
|
const dismissPendingInsert = useCallback(() => setPendingInsertIndex(null), []);
|
|
1207
1229
|
|
|
1208
|
-
const typeOptions = useMemo(
|
|
1209
|
-
() =>
|
|
1210
|
-
getAllSections()
|
|
1211
|
-
.filter((def) => def.type !== "spacer")
|
|
1212
|
-
.map((def) => ({
|
|
1213
|
-
type: def.type,
|
|
1214
|
-
label: def.label,
|
|
1215
|
-
icon: def.icon,
|
|
1216
|
-
})),
|
|
1217
|
-
[],
|
|
1218
|
-
);
|
|
1230
|
+
const typeOptions = useMemo(() => buildInsertMenuOptions(getAllSections()), []);
|
|
1219
1231
|
|
|
1220
1232
|
const editingEnabled = isEditMode && !isPublishing;
|
|
1221
1233
|
|
|
@@ -1528,6 +1540,7 @@ function EditorToolbar({
|
|
|
1528
1540
|
onRestoreClick,
|
|
1529
1541
|
onOrderingClick,
|
|
1530
1542
|
onPagesClick,
|
|
1543
|
+
onChatbotClick,
|
|
1531
1544
|
}: {
|
|
1532
1545
|
buttonState: "synced" | "publish" | "saveAndPublish";
|
|
1533
1546
|
localChangesExist: boolean;
|
|
@@ -1546,6 +1559,7 @@ function EditorToolbar({
|
|
|
1546
1559
|
onRestoreClick: () => void;
|
|
1547
1560
|
onOrderingClick: () => void;
|
|
1548
1561
|
onPagesClick: () => void;
|
|
1562
|
+
onChatbotClick: () => void;
|
|
1549
1563
|
}) {
|
|
1550
1564
|
const { isEditMode, viewBranch, setViewBranch, toggleEditMode, historyState, setHistoryState } = useEditorContext();
|
|
1551
1565
|
|
|
@@ -1631,6 +1645,13 @@ function EditorToolbar({
|
|
|
1631
1645
|
onClick={onPagesClick}
|
|
1632
1646
|
className="border border-base-200 bg-base-accent"
|
|
1633
1647
|
/>
|
|
1648
|
+
<IconButton
|
|
1649
|
+
icon={<MessageCircleIcon size={16} />}
|
|
1650
|
+
label="Chatbot"
|
|
1651
|
+
size="md"
|
|
1652
|
+
onClick={onChatbotClick}
|
|
1653
|
+
className="border border-base-200 bg-base-accent"
|
|
1654
|
+
/>
|
|
1634
1655
|
<IconButton
|
|
1635
1656
|
icon={<SettingsIcon size={16} />}
|
|
1636
1657
|
label="Site settings"
|
package/src/lib/index.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export { env } from "./env";
|
|
2
2
|
export { cn } from "./cn";
|
|
3
|
+
export { audiencePasses } from "./pages";
|
|
3
4
|
export { generateNavLinks, toSectionId, type NavItem } from "./nav";
|
|
4
5
|
export { deriveContrast } from "./contrast";
|
|
5
6
|
export { sanitizeHtml, ensureSanitizer } from "./sanitize";
|