astro-tractstack 2.0.0-rc.9 → 2.0.1
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/LICENSE +8 -97
- package/README.md +7 -5
- package/bin/create-tractstack.js +31 -8
- package/dist/index.js +106 -29
- package/package.json +10 -5
- package/templates/css/frontend.css +1 -1
- package/templates/custom/minimal/CodeHook.astro +13 -12
- package/templates/custom/minimal/CustomRoutes.astro +25 -31
- package/templates/custom/with-examples/CodeHook.astro +22 -11
- package/templates/custom/with-examples/CustomRoutes.astro +4 -8
- package/templates/custom/with-examples/ProductCard.astro +29 -0
- package/templates/custom/with-examples/ProductCardWrapper.astro +43 -0
- package/templates/custom/with-examples/ProductGrid.astro +64 -0
- package/templates/custom/with-examples/pages/Collections.astro +58 -98
- package/templates/gitignore +42 -0
- package/templates/prettierignore +5 -0
- package/templates/prettierrc +19 -0
- package/templates/src/client/app.js +127 -0
- package/templates/src/client/htmx.min.js +3519 -0
- package/templates/src/client/view.js +429 -0
- package/templates/src/components/Footer.astro +4 -9
- package/templates/src/components/Header.astro +67 -60
- package/templates/src/components/Menu.tsx +188 -52
- package/templates/src/components/codehooks/BunnyVideoSetup.tsx +2 -2
- package/templates/src/components/codehooks/EpinetDurationSelector.tsx +9 -13
- package/templates/src/components/codehooks/EpinetTableView.tsx +11 -7
- package/templates/src/components/codehooks/EpinetWrapper.tsx +10 -9
- package/templates/src/components/codehooks/FeaturedArticle.astro +105 -0
- package/templates/src/components/codehooks/FeaturedArticleSetup.tsx +318 -0
- package/templates/src/components/codehooks/ListContent.astro +32 -162
- package/templates/src/components/codehooks/ListContentSetup.tsx +43 -138
- package/templates/src/components/codehooks/ProductCardSetup.tsx +152 -0
- package/templates/src/components/codehooks/ProductGridSetup.tsx +274 -0
- package/templates/src/components/codehooks/SearchWidget.tsx +453 -0
- package/templates/src/components/compositor/Node.tsx +3 -6
- package/templates/src/components/compositor/PanelVisibilityWrapper.tsx +21 -11
- package/templates/src/components/compositor/elements/BunnyVideo.tsx +21 -20
- package/templates/src/components/compositor/nodes/Pane.tsx +51 -21
- package/templates/src/components/compositor/nodes/RenderChildren.tsx +6 -1
- package/templates/src/components/compositor/nodes/Widget.tsx +16 -2
- package/templates/src/components/compositor/preview/FeaturedArticlePreview.tsx +155 -0
- package/templates/src/components/compositor/preview/PaneSnapshotGenerator.tsx +20 -1
- package/templates/src/components/edit/Header.tsx +10 -4
- package/templates/src/components/edit/PanelSwitch.tsx +11 -7
- package/templates/src/components/edit/SettingsPanel.tsx +29 -18
- package/templates/src/components/edit/ToolBar.tsx +1 -28
- package/templates/src/components/edit/ToolMode.tsx +45 -32
- package/templates/src/components/edit/pane/AddPanePanel_break.tsx +12 -2
- package/templates/src/components/edit/pane/AddPanePanel_codehook.tsx +8 -2
- package/templates/src/components/edit/pane/AddPanePanel_newAICopy_modal.tsx +1 -1
- package/templates/src/components/edit/pane/ConfigPanePanel.tsx +17 -27
- package/templates/src/components/edit/pane/PageGenSelector.tsx +16 -16
- package/templates/src/components/edit/pane/PageGenSpecial.tsx +26 -49
- package/templates/src/components/edit/pane/PageGen_preview.tsx +17 -2
- package/templates/src/components/edit/pane/PanePanel_path.tsx +2 -4
- package/templates/src/components/edit/pane/PanePanel_title.tsx +243 -76
- package/templates/src/components/edit/panels/StyleBreakPanel.tsx +17 -19
- package/templates/src/components/edit/panels/StyleCodeHookPanel.tsx +48 -37
- package/templates/src/components/edit/panels/StyleElementPanel_add.tsx +60 -55
- package/templates/src/components/edit/panels/StyleImagePanel_add.tsx +56 -50
- package/templates/src/components/edit/panels/StyleLiElementPanel_add.tsx +54 -47
- package/templates/src/components/edit/panels/StyleLinkPanel_add.tsx +54 -44
- package/templates/src/components/edit/panels/StyleLinkPanel_config.tsx +113 -138
- package/templates/src/components/edit/panels/StyleParentPanel_add.tsx +54 -40
- package/templates/src/components/edit/panels/StyleWidgetPanel.tsx +3 -3
- package/templates/src/components/edit/panels/StyleWidgetPanel_add.tsx +56 -49
- package/templates/src/components/edit/panels/StyleWidgetPanel_config.tsx +14 -5
- package/templates/src/components/edit/state/SaveModal.tsx +316 -169
- package/templates/src/components/edit/storyfragment/StoryFragmentPanel_og.tsx +1 -1
- package/templates/src/components/edit/storyfragment/StoryFragmentPanel_slug.tsx +56 -55
- package/templates/src/components/edit/widgets/BunnyWidget.tsx +538 -59
- package/templates/src/components/edit/widgets/InteractiveDisclosureWidget.tsx +656 -0
- package/templates/src/components/edit/widgets/ToggleWidget.tsx +9 -16
- package/templates/src/components/fields/ArtpackImage.tsx +4 -1
- package/templates/src/components/fields/BackgroundImage.tsx +1 -1
- package/templates/src/components/fields/BackgroundImageWrapper.tsx +127 -35
- package/templates/src/components/fields/ColorPickerCombo.tsx +66 -62
- package/templates/src/components/fields/ImageUpload.tsx +1 -1
- package/templates/src/components/fields/ViewportComboBox.tsx +59 -42
- package/templates/src/components/form/ActionBuilderBeliefSelector.tsx +117 -0
- package/templates/src/components/form/ActionBuilderField.tsx +306 -87
- package/templates/src/components/search/SearchModal.tsx +420 -0
- package/templates/src/components/search/SearchResults.tsx +367 -0
- package/templates/src/components/search/SearchWrapper.tsx +46 -0
- package/templates/src/components/storykeep/Dashboard_Advanced.tsx +1 -1
- package/templates/src/components/storykeep/Dashboard_Analytics.tsx +34 -8
- package/templates/src/components/storykeep/Dashboard_Branding.tsx +1 -1
- package/templates/src/components/storykeep/Dashboard_Content.tsx +6 -0
- package/templates/src/components/storykeep/StoryKeepBackdrop.astro +87 -0
- package/templates/src/components/storykeep/controls/content/BeliefForm.tsx +38 -34
- package/templates/src/components/storykeep/controls/content/KnownResourceForm.tsx +1 -1
- package/templates/src/components/storykeep/controls/content/MenuForm.tsx +56 -8
- package/templates/src/components/storykeep/controls/content/ResourceForm.tsx +18 -3
- package/templates/src/components/storykeep/controls/content/StoryFragmentTable.tsx +5 -8
- package/templates/src/components/storykeep/state/FetchAnalytics.tsx +274 -228
- package/templates/src/components/storykeep/widgets/Wizard.tsx +14 -7
- package/templates/src/components/widgets/ImpressionWrapper.tsx +0 -1
- package/templates/src/constants/shapes.ts +9 -0
- package/templates/src/constants.ts +2121 -16
- package/templates/src/hooks/useSearch.ts +228 -0
- package/templates/src/layouts/Layout.astro +213 -104
- package/templates/src/lib/storyData.ts +4 -1
- package/templates/src/pages/[...slug]/edit.astro +14 -14
- package/templates/src/pages/[...slug].astro +82 -21
- package/templates/src/pages/api/orphan-analysis.ts +0 -1
- package/templates/src/pages/api/tailwind.ts +23 -21
- package/templates/src/pages/context/[...contextSlug]/edit.astro +14 -14
- package/templates/src/pages/context/[...contextSlug].astro +7 -2
- package/templates/src/pages/storykeep/advanced.astro +5 -4
- package/templates/src/pages/storykeep/branding.astro +5 -4
- package/templates/src/pages/storykeep/content.astro +5 -4
- package/templates/src/pages/storykeep/init.astro +40 -1
- package/templates/src/pages/storykeep/login.astro +1 -1
- package/templates/src/pages/storykeep.astro +5 -4
- package/templates/src/stores/nodes.ts +59 -88
- package/templates/src/stores/orphanAnalysis.ts +19 -21
- package/templates/src/stores/storykeep.ts +7 -0
- package/templates/src/types/compositorTypes.ts +6 -0
- package/templates/src/types/tractstack.ts +17 -0
- package/templates/src/utils/actions/lispLexer.ts +2 -2
- package/templates/src/utils/actions/preParse_Action.ts +3 -0
- package/templates/src/utils/api/beliefHelpers.ts +12 -36
- package/templates/src/utils/api/menuHelpers.ts +2 -2
- package/templates/src/utils/api.ts +26 -0
- package/templates/src/utils/compositor/TemplateNodes.ts +7 -0
- package/templates/src/utils/compositor/allowInsert.ts +5 -3
- package/templates/src/utils/compositor/nodesHelper.ts +4 -0
- package/templates/src/utils/compositor/processMarkdown.ts +16 -2
- package/templates/src/utils/compositor/reduceNodesClassNames.ts +4 -0
- package/templates/src/utils/compositor/templateMarkdownStyles.ts +13 -13
- package/templates/src/utils/compositor/typeGuards.ts +1 -0
- package/templates/src/utils/customHelpers.ts +38 -0
- package/templates/src/utils/helpers.ts +2 -2
- package/templates/src/utils/layout.ts +65 -144
- package/utils/inject-files.ts +95 -18
- package/templates/src/client/analytics-events.js +0 -207
- package/templates/src/client/belief-events.js +0 -191
- package/templates/src/client/sse.js +0 -613
- package/templates/src/components/codehooks/FeaturedContent.astro +0 -273
- package/templates/src/components/codehooks/FeaturedContentSetup.tsx +0 -738
- package/templates/src/components/compositor/preview/FeaturedContentPreview.tsx +0 -128
- package/templates/src/components/edit/pane/PanePanel_slug.tsx +0 -219
|
@@ -0,0 +1,656 @@
|
|
|
1
|
+
import { useState, useEffect, useMemo } from 'react';
|
|
2
|
+
import { TractStackAPI } from '@/utils/api';
|
|
3
|
+
import { fullContentMapStore } from '@/stores/storykeep';
|
|
4
|
+
import { heldBeliefsScales } from '@/constants/beliefs';
|
|
5
|
+
import { biIcons } from '@/constants';
|
|
6
|
+
import type { BrandConfig } from '@/types/tractstack';
|
|
7
|
+
import type { FlatNode, BeliefNode } from '@/types/compositorTypes';
|
|
8
|
+
import SingleParam from '@/components/fields/SingleParam';
|
|
9
|
+
import ColorPickerCombo from '@/components/fields/ColorPickerCombo';
|
|
10
|
+
import ActionBuilderField from '@/components/form/ActionBuilderField';
|
|
11
|
+
import { Dialog } from '@ark-ui/react/dialog';
|
|
12
|
+
import { Portal } from '@ark-ui/react/portal';
|
|
13
|
+
import { Combobox } from '@ark-ui/react/combobox';
|
|
14
|
+
import { createListCollection } from '@ark-ui/react/collection';
|
|
15
|
+
import ChevronDownIcon from '@heroicons/react/24/outline/ChevronDownIcon';
|
|
16
|
+
import TrashIcon from '@heroicons/react/24/outline/TrashIcon';
|
|
17
|
+
import ArrowUturnLeftIcon from '@heroicons/react/24/outline/ArrowUturnLeftIcon';
|
|
18
|
+
import XMarkIcon from '@heroicons/react/24/outline/XMarkIcon';
|
|
19
|
+
import CheckIcon from '@heroicons/react/24/outline/CheckIcon';
|
|
20
|
+
import ChevronUpDownIcon from '@heroicons/react/24/outline/ChevronUpDownIcon';
|
|
21
|
+
import PlusIcon from '@heroicons/react/24/outline/PlusIcon';
|
|
22
|
+
import ArrowUpIcon from '@heroicons/react/24/outline/ArrowUpIcon';
|
|
23
|
+
import ArrowDownIcon from '@heroicons/react/24/outline/ArrowDownIcon';
|
|
24
|
+
|
|
25
|
+
interface DisclosureItem {
|
|
26
|
+
id: string;
|
|
27
|
+
beliefValue: string;
|
|
28
|
+
isCustom: boolean;
|
|
29
|
+
title: string;
|
|
30
|
+
description?: string;
|
|
31
|
+
icon: string;
|
|
32
|
+
actionLisp: string;
|
|
33
|
+
isDisabled?: boolean;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
interface WidgetStyles {
|
|
37
|
+
textColor: string;
|
|
38
|
+
bgColor: string;
|
|
39
|
+
bgOpacity: number;
|
|
40
|
+
}
|
|
41
|
+
type StoredDisclosureItem = Omit<DisclosureItem, 'id' | 'isDisabled'>;
|
|
42
|
+
interface InteractiveDisclosureWidgetProps {
|
|
43
|
+
node: FlatNode;
|
|
44
|
+
onUpdate: (params: string[]) => void;
|
|
45
|
+
config: BrandConfig;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const generateId = (): string => Math.random().toString(36).substring(2, 9);
|
|
49
|
+
|
|
50
|
+
const quoteIfNecessary = (command: string, value: string): string => {
|
|
51
|
+
if (command === 'identifyAs' && value.includes(' ')) {
|
|
52
|
+
return `"${value}"`;
|
|
53
|
+
}
|
|
54
|
+
return value;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const IconSelector = ({
|
|
58
|
+
value,
|
|
59
|
+
onChange,
|
|
60
|
+
}: {
|
|
61
|
+
value: string;
|
|
62
|
+
onChange: (value: string) => void;
|
|
63
|
+
}) => {
|
|
64
|
+
const [query, setQuery] = useState('');
|
|
65
|
+
const filteredIcons = useMemo(
|
|
66
|
+
() =>
|
|
67
|
+
biIcons.filter((icon) =>
|
|
68
|
+
icon.toLowerCase().includes(query.toLowerCase())
|
|
69
|
+
),
|
|
70
|
+
[query]
|
|
71
|
+
);
|
|
72
|
+
const collection = useMemo(
|
|
73
|
+
() => createListCollection({ items: filteredIcons }),
|
|
74
|
+
[filteredIcons]
|
|
75
|
+
);
|
|
76
|
+
const iconSelectorStyles = `.icon-item .icon-indicator { display: none; } .icon-item[data-state="checked"] .icon-indicator { display: flex; }`;
|
|
77
|
+
return (
|
|
78
|
+
<div>
|
|
79
|
+
<style>{iconSelectorStyles}</style>
|
|
80
|
+
<label className="block text-xs font-bold text-gray-600">Icon</label>
|
|
81
|
+
<Combobox.Root
|
|
82
|
+
collection={collection}
|
|
83
|
+
value={[value]}
|
|
84
|
+
onValueChange={(details) => onChange(details.value[0] || '')}
|
|
85
|
+
onInputValueChange={(details) => setQuery(details.inputValue)}
|
|
86
|
+
>
|
|
87
|
+
<Combobox.Control className="relative mt-1">
|
|
88
|
+
<Combobox.Input
|
|
89
|
+
className="w-full rounded-md border-gray-300 py-1.5 pl-3 pr-10 shadow-sm"
|
|
90
|
+
placeholder="Search icons..."
|
|
91
|
+
autoFocus
|
|
92
|
+
/>
|
|
93
|
+
<Combobox.Trigger className="absolute inset-y-0 right-0 flex items-center pr-2">
|
|
94
|
+
<i className={`bi bi-${value} mr-2 text-lg`}></i>
|
|
95
|
+
<ChevronUpDownIcon className="h-5 w-5 text-gray-400" />
|
|
96
|
+
</Combobox.Trigger>
|
|
97
|
+
</Combobox.Control>
|
|
98
|
+
<Portal>
|
|
99
|
+
<Combobox.Positioner style={{ zIndex: 9010, minWidth: '250px' }}>
|
|
100
|
+
<Combobox.Content className="max-h-60 w-full overflow-y-auto rounded-md bg-white shadow-lg">
|
|
101
|
+
{filteredIcons.map((icon) => (
|
|
102
|
+
<Combobox.Item
|
|
103
|
+
key={icon}
|
|
104
|
+
item={icon}
|
|
105
|
+
className="icon-item relative cursor-pointer select-none py-2 pl-10 pr-4 text-gray-900 data-[highlighted]:bg-cyan-600 data-[highlighted]:text-white"
|
|
106
|
+
>
|
|
107
|
+
<Combobox.ItemText>
|
|
108
|
+
<i className={`bi bi-${icon} mr-2 text-lg`}></i> {icon}
|
|
109
|
+
</Combobox.ItemText>
|
|
110
|
+
<Combobox.ItemIndicator className="icon-indicator absolute inset-y-0 left-0 flex items-center pl-3 text-cyan-600 data-[highlighted]:text-white">
|
|
111
|
+
<CheckIcon className="h-5 w-5" />
|
|
112
|
+
</Combobox.ItemIndicator>
|
|
113
|
+
</Combobox.Item>
|
|
114
|
+
))}
|
|
115
|
+
</Combobox.Content>
|
|
116
|
+
</Combobox.Positioner>
|
|
117
|
+
</Portal>
|
|
118
|
+
</Combobox.Root>
|
|
119
|
+
</div>
|
|
120
|
+
);
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
const DisclosureItemEditor = ({
|
|
124
|
+
item,
|
|
125
|
+
onUpdate,
|
|
126
|
+
onToggle,
|
|
127
|
+
config,
|
|
128
|
+
onMoveUp,
|
|
129
|
+
onMoveDown,
|
|
130
|
+
isFirst,
|
|
131
|
+
isLast,
|
|
132
|
+
}: {
|
|
133
|
+
item: DisclosureItem;
|
|
134
|
+
onUpdate: (updates: Partial<DisclosureItem>) => void;
|
|
135
|
+
onToggle: () => void;
|
|
136
|
+
config: BrandConfig;
|
|
137
|
+
onMoveUp: () => void;
|
|
138
|
+
onMoveDown: () => void;
|
|
139
|
+
isFirst: boolean;
|
|
140
|
+
isLast: boolean;
|
|
141
|
+
}) => {
|
|
142
|
+
const [isEditingIcon, setIsEditingIcon] = useState(false);
|
|
143
|
+
|
|
144
|
+
const handleIconChange = (newIcon: string) => {
|
|
145
|
+
onUpdate({ icon: newIcon });
|
|
146
|
+
setIsEditingIcon(false);
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
return (
|
|
150
|
+
<div
|
|
151
|
+
className={`space-y-4 rounded-lg border bg-white p-4 shadow-sm transition-opacity ${
|
|
152
|
+
item.isDisabled ? 'border-gray-100 opacity-40' : 'border-gray-200'
|
|
153
|
+
}`}
|
|
154
|
+
>
|
|
155
|
+
<div className="flex items-center justify-between">
|
|
156
|
+
<div className="flex items-center gap-2">
|
|
157
|
+
<div className="flex flex-col">
|
|
158
|
+
<button
|
|
159
|
+
type="button"
|
|
160
|
+
onClick={onMoveUp}
|
|
161
|
+
disabled={isFirst}
|
|
162
|
+
className="rounded p-0.5 text-gray-500 hover:bg-gray-100 disabled:opacity-25"
|
|
163
|
+
>
|
|
164
|
+
<ArrowUpIcon className="h-4 w-4" />
|
|
165
|
+
</button>
|
|
166
|
+
<button
|
|
167
|
+
type="button"
|
|
168
|
+
onClick={onMoveDown}
|
|
169
|
+
disabled={isLast}
|
|
170
|
+
className="rounded p-0.5 text-gray-500 hover:bg-gray-100 disabled:opacity-25"
|
|
171
|
+
>
|
|
172
|
+
<ArrowDownIcon className="h-4 w-4" />
|
|
173
|
+
</button>
|
|
174
|
+
</div>
|
|
175
|
+
<h4 className="font-bold text-gray-800">
|
|
176
|
+
{item.title}{' '}
|
|
177
|
+
{!item.isCustom && (
|
|
178
|
+
<span className="text-xs font-normal text-gray-500">
|
|
179
|
+
(Key: {item.beliefValue})
|
|
180
|
+
</span>
|
|
181
|
+
)}
|
|
182
|
+
</h4>
|
|
183
|
+
</div>
|
|
184
|
+
<button
|
|
185
|
+
type="button"
|
|
186
|
+
onClick={onToggle}
|
|
187
|
+
className={`rounded p-1 hover:bg-gray-100 ${
|
|
188
|
+
item.isDisabled ? 'text-blue-600' : 'text-red-600'
|
|
189
|
+
}`}
|
|
190
|
+
>
|
|
191
|
+
{item.isDisabled ? (
|
|
192
|
+
<ArrowUturnLeftIcon className="h-4 w-4" />
|
|
193
|
+
) : (
|
|
194
|
+
<TrashIcon className="h-4 w-4" />
|
|
195
|
+
)}
|
|
196
|
+
</button>
|
|
197
|
+
</div>
|
|
198
|
+
<fieldset disabled={item.isDisabled} className="space-y-4">
|
|
199
|
+
<SingleParam
|
|
200
|
+
label="Display Title"
|
|
201
|
+
value={item.title}
|
|
202
|
+
onChange={(value) => onUpdate({ title: value })}
|
|
203
|
+
/>
|
|
204
|
+
<SingleParam
|
|
205
|
+
label="Description (Optional)"
|
|
206
|
+
value={item.description || ''}
|
|
207
|
+
onChange={(value) => onUpdate({ description: value })}
|
|
208
|
+
/>
|
|
209
|
+
|
|
210
|
+
{isEditingIcon ? (
|
|
211
|
+
<IconSelector value={item.icon} onChange={handleIconChange} />
|
|
212
|
+
) : (
|
|
213
|
+
<div>
|
|
214
|
+
<label className="block text-xs font-bold text-gray-600">
|
|
215
|
+
Icon
|
|
216
|
+
</label>
|
|
217
|
+
<div className="mt-1 flex items-center justify-between rounded-md border border-gray-300 bg-white px-3 py-1.5 shadow-sm">
|
|
218
|
+
<div className="flex items-center gap-2">
|
|
219
|
+
<i className={`bi bi-${item.icon} text-lg`}></i>
|
|
220
|
+
<span className="text-sm">{item.icon}</span>
|
|
221
|
+
</div>
|
|
222
|
+
<button
|
|
223
|
+
type="button"
|
|
224
|
+
onClick={() => setIsEditingIcon(true)}
|
|
225
|
+
className="text-sm font-bold text-cyan-600 hover:text-cyan-800"
|
|
226
|
+
>
|
|
227
|
+
Change
|
|
228
|
+
</button>
|
|
229
|
+
</div>
|
|
230
|
+
</div>
|
|
231
|
+
)}
|
|
232
|
+
|
|
233
|
+
{item.isCustom ? (
|
|
234
|
+
<div className="relative rounded-md border p-3">
|
|
235
|
+
<ActionBuilderField
|
|
236
|
+
value={item.actionLisp}
|
|
237
|
+
onChange={(value) => onUpdate({ actionLisp: value })}
|
|
238
|
+
contentMap={fullContentMapStore.get()}
|
|
239
|
+
/>
|
|
240
|
+
</div>
|
|
241
|
+
) : (
|
|
242
|
+
<div>
|
|
243
|
+
<label className="block text-xs font-bold text-gray-600">
|
|
244
|
+
Action (Locked)
|
|
245
|
+
</label>
|
|
246
|
+
<div className="mt-1 rounded-md border border-gray-200 bg-gray-50 p-2 font-mono text-xs text-gray-500">
|
|
247
|
+
{item.actionLisp}
|
|
248
|
+
</div>
|
|
249
|
+
</div>
|
|
250
|
+
)}
|
|
251
|
+
</fieldset>
|
|
252
|
+
</div>
|
|
253
|
+
);
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
export default function InteractiveDisclosureWidget({
|
|
257
|
+
node,
|
|
258
|
+
onUpdate,
|
|
259
|
+
config,
|
|
260
|
+
}: InteractiveDisclosureWidgetProps) {
|
|
261
|
+
const [beliefs, setBeliefs] = useState<BeliefNode[]>([]);
|
|
262
|
+
const [selectedBeliefTag, setSelectedBeliefTag] = useState<string>('');
|
|
263
|
+
const [disclosures, setDisclosures] = useState<DisclosureItem[]>([]);
|
|
264
|
+
const [widgetStyles, setWidgetStyles] = useState<WidgetStyles>({
|
|
265
|
+
textColor: '#000000',
|
|
266
|
+
bgColor: '#ffffff',
|
|
267
|
+
bgOpacity: 100,
|
|
268
|
+
});
|
|
269
|
+
const [isModalOpen, setIsModalOpen] = useState(false);
|
|
270
|
+
const [isDataLoaded, setIsDataLoaded] = useState(false);
|
|
271
|
+
|
|
272
|
+
const selectedBelief = beliefs.find((b) => b.slug === selectedBeliefTag);
|
|
273
|
+
const hasRealSelection = !!selectedBelief;
|
|
274
|
+
|
|
275
|
+
useEffect(() => {
|
|
276
|
+
const beliefTag = String(node.codeHookParams?.[0] || '');
|
|
277
|
+
const payloadJson = String(node.codeHookParams?.[1] || '');
|
|
278
|
+
|
|
279
|
+
if (beliefs.length === 0 && beliefTag && beliefTag !== 'BELIEF') {
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
setSelectedBeliefTag(beliefTag && beliefTag !== 'BELIEF' ? beliefTag : '');
|
|
284
|
+
const currentBelief = beliefs.find((b) => b.slug === beliefTag);
|
|
285
|
+
|
|
286
|
+
if (payloadJson && currentBelief) {
|
|
287
|
+
try {
|
|
288
|
+
const parsed = JSON.parse(payloadJson);
|
|
289
|
+
setWidgetStyles(
|
|
290
|
+
parsed.styles || {
|
|
291
|
+
textColor: '#000000',
|
|
292
|
+
bgColor: '#ffffff',
|
|
293
|
+
bgOpacity: 100,
|
|
294
|
+
}
|
|
295
|
+
);
|
|
296
|
+
const loadedDisclosures =
|
|
297
|
+
(parsed.disclosures as StoredDisclosureItem[]) || [];
|
|
298
|
+
|
|
299
|
+
const scaleKeys =
|
|
300
|
+
currentBelief.scale === 'custom'
|
|
301
|
+
? (currentBelief.customValues || []).map((v) => ({
|
|
302
|
+
slug: v,
|
|
303
|
+
name: v,
|
|
304
|
+
}))
|
|
305
|
+
: heldBeliefsScales[
|
|
306
|
+
currentBelief.scale as keyof typeof heldBeliefsScales
|
|
307
|
+
] || [];
|
|
308
|
+
|
|
309
|
+
const actionCommand =
|
|
310
|
+
currentBelief.scale === 'custom' ? 'identifyAs' : 'declare';
|
|
311
|
+
const finalDisclosures: DisclosureItem[] = loadedDisclosures.map(
|
|
312
|
+
(loadedItem) => {
|
|
313
|
+
const isFromScale = scaleKeys.some(
|
|
314
|
+
(sk) => sk.slug === loadedItem.beliefValue
|
|
315
|
+
);
|
|
316
|
+
|
|
317
|
+
return {
|
|
318
|
+
...loadedItem,
|
|
319
|
+
id: generateId(),
|
|
320
|
+
isCustom: !isFromScale,
|
|
321
|
+
actionLisp: isFromScale
|
|
322
|
+
? `(${actionCommand} ${beliefTag} ${quoteIfNecessary(actionCommand, loadedItem.beliefValue)})`
|
|
323
|
+
: loadedItem.actionLisp,
|
|
324
|
+
isDisabled: false,
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
);
|
|
328
|
+
scaleKeys.forEach(({ slug, name }) => {
|
|
329
|
+
if (!finalDisclosures.some((d) => d.beliefValue === slug)) {
|
|
330
|
+
finalDisclosures.push({
|
|
331
|
+
id: generateId(),
|
|
332
|
+
beliefValue: slug,
|
|
333
|
+
title: name,
|
|
334
|
+
description: '',
|
|
335
|
+
icon: 'chat-heart-fill',
|
|
336
|
+
actionLisp: `(${actionCommand} ${beliefTag} ${quoteIfNecessary(actionCommand, slug)})`,
|
|
337
|
+
isCustom: false,
|
|
338
|
+
isDisabled: true,
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
});
|
|
342
|
+
setDisclosures(finalDisclosures);
|
|
343
|
+
} catch (e) {
|
|
344
|
+
console.error('Error parsing disclosure payload:', e);
|
|
345
|
+
}
|
|
346
|
+
} else {
|
|
347
|
+
setDisclosures([]);
|
|
348
|
+
setWidgetStyles({
|
|
349
|
+
textColor: '#000000',
|
|
350
|
+
bgColor: '#ffffff',
|
|
351
|
+
bgOpacity: 100,
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
setIsDataLoaded(true);
|
|
355
|
+
}, [node, beliefs]);
|
|
356
|
+
|
|
357
|
+
useEffect(() => {
|
|
358
|
+
const fetchData = async () => {
|
|
359
|
+
try {
|
|
360
|
+
const api = new TractStackAPI();
|
|
361
|
+
const {
|
|
362
|
+
data: { beliefIds },
|
|
363
|
+
} = await api.get('/api/v1/nodes/beliefs');
|
|
364
|
+
if (!beliefIds?.length) {
|
|
365
|
+
setBeliefs([]);
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
const {
|
|
369
|
+
data: { beliefs },
|
|
370
|
+
} = await api.post('/api/v1/nodes/beliefs', { beliefIds });
|
|
371
|
+
setBeliefs(beliefs || []);
|
|
372
|
+
} catch (error) {
|
|
373
|
+
console.error('Error fetching beliefs:', error);
|
|
374
|
+
setBeliefs([]);
|
|
375
|
+
}
|
|
376
|
+
};
|
|
377
|
+
fetchData();
|
|
378
|
+
}, [node]);
|
|
379
|
+
|
|
380
|
+
const handleUpdate = () => {
|
|
381
|
+
const disclosuresToStore: StoredDisclosureItem[] = disclosures
|
|
382
|
+
.filter((d) => !d.isDisabled)
|
|
383
|
+
.map(({ id, isDisabled, ...rest }) => rest);
|
|
384
|
+
const payload = { styles: widgetStyles, disclosures: disclosuresToStore };
|
|
385
|
+
onUpdate([selectedBeliefTag, JSON.stringify(payload)]);
|
|
386
|
+
};
|
|
387
|
+
|
|
388
|
+
const handleBeliefChange = (tag: string) => {
|
|
389
|
+
setSelectedBeliefTag(tag);
|
|
390
|
+
setWidgetStyles({
|
|
391
|
+
textColor: '#000000',
|
|
392
|
+
bgColor: '#ffffff',
|
|
393
|
+
bgOpacity: 100,
|
|
394
|
+
});
|
|
395
|
+
const belief = beliefs.find((b) => b.slug === tag);
|
|
396
|
+
let newDisclosures: DisclosureItem[] = [];
|
|
397
|
+
if (belief) {
|
|
398
|
+
const actionCommand =
|
|
399
|
+
belief.scale === 'custom' ? 'identifyAs' : 'declare';
|
|
400
|
+
const keys =
|
|
401
|
+
belief.scale === 'custom'
|
|
402
|
+
? (belief.customValues || []).map((v) => ({ slug: v, name: v }))
|
|
403
|
+
: heldBeliefsScales[belief.scale as keyof typeof heldBeliefsScales] ||
|
|
404
|
+
[];
|
|
405
|
+
|
|
406
|
+
newDisclosures = keys.map(({ slug, name }) => ({
|
|
407
|
+
id: generateId(),
|
|
408
|
+
beliefValue: slug,
|
|
409
|
+
title: name,
|
|
410
|
+
description: '',
|
|
411
|
+
icon: 'chat-heart-fill',
|
|
412
|
+
actionLisp: `(${actionCommand} ${tag} ${quoteIfNecessary(actionCommand, slug)})`,
|
|
413
|
+
isCustom: false,
|
|
414
|
+
isDisabled: false,
|
|
415
|
+
}));
|
|
416
|
+
}
|
|
417
|
+
setDisclosures(newDisclosures);
|
|
418
|
+
};
|
|
419
|
+
|
|
420
|
+
const moveDisclosure = (id: string, direction: 'up' | 'down') => {
|
|
421
|
+
const index = disclosures.findIndex((d) => d.id === id);
|
|
422
|
+
if (index === -1) return;
|
|
423
|
+
const newIndex = direction === 'up' ? index - 1 : index + 1;
|
|
424
|
+
if (newIndex < 0 || newIndex >= disclosures.length) return;
|
|
425
|
+
|
|
426
|
+
const newDisclosures = [...disclosures];
|
|
427
|
+
const [movedItem] = newDisclosures.splice(index, 1);
|
|
428
|
+
newDisclosures.splice(newIndex, 0, movedItem);
|
|
429
|
+
setDisclosures(newDisclosures);
|
|
430
|
+
};
|
|
431
|
+
|
|
432
|
+
const addCustomDisclosure = () => {
|
|
433
|
+
const newItem: DisclosureItem = {
|
|
434
|
+
id: generateId(),
|
|
435
|
+
beliefValue: `custom-${Date.now()}-${Math.random()
|
|
436
|
+
.toString(36)
|
|
437
|
+
.substring(2, 6)}`,
|
|
438
|
+
title: 'New Custom Item',
|
|
439
|
+
description: '',
|
|
440
|
+
icon: 'chat-heart-fill',
|
|
441
|
+
actionLisp: '',
|
|
442
|
+
isCustom: true,
|
|
443
|
+
isDisabled: false,
|
|
444
|
+
};
|
|
445
|
+
setDisclosures([...disclosures, newItem]);
|
|
446
|
+
};
|
|
447
|
+
|
|
448
|
+
const updateDisclosure = (id: string, updates: Partial<DisclosureItem>) =>
|
|
449
|
+
setDisclosures(
|
|
450
|
+
disclosures.map((d) => (d.id === id ? { ...d, ...updates } : d))
|
|
451
|
+
);
|
|
452
|
+
|
|
453
|
+
const updateWidgetStyles = (updates: Partial<WidgetStyles>) =>
|
|
454
|
+
setWidgetStyles((prev) => ({ ...prev, ...updates }));
|
|
455
|
+
|
|
456
|
+
const toggleDisclosure = (id: string) => {
|
|
457
|
+
const itemToToggle = disclosures.find((d) => d.id === id);
|
|
458
|
+
if (!itemToToggle) return;
|
|
459
|
+
|
|
460
|
+
if (itemToToggle.isCustom) {
|
|
461
|
+
setDisclosures(disclosures.filter((d) => d.id !== id));
|
|
462
|
+
} else {
|
|
463
|
+
setDisclosures(
|
|
464
|
+
disclosures.map((d) =>
|
|
465
|
+
d.id === id ? { ...d, isDisabled: !d.isDisabled } : d
|
|
466
|
+
)
|
|
467
|
+
);
|
|
468
|
+
}
|
|
469
|
+
};
|
|
470
|
+
|
|
471
|
+
const handleColorChange = (
|
|
472
|
+
key: 'textColor' | 'bgColor',
|
|
473
|
+
hex: string | null
|
|
474
|
+
) => {
|
|
475
|
+
updateWidgetStyles({ [key]: hex || '' });
|
|
476
|
+
};
|
|
477
|
+
|
|
478
|
+
return (
|
|
479
|
+
<div className="space-y-4">
|
|
480
|
+
<div className="flex items-center gap-2">
|
|
481
|
+
<select
|
|
482
|
+
value={selectedBeliefTag}
|
|
483
|
+
onChange={(e) => handleBeliefChange(e.target.value)}
|
|
484
|
+
className="flex-1 rounded-md border-gray-300 shadow-sm"
|
|
485
|
+
disabled={hasRealSelection}
|
|
486
|
+
>
|
|
487
|
+
<option value="">Select a Belief...</option>
|
|
488
|
+
{beliefs.map((b) => (
|
|
489
|
+
<option key={b.slug} value={b.slug}>
|
|
490
|
+
{b.title} ({b.scale})
|
|
491
|
+
</option>
|
|
492
|
+
))}
|
|
493
|
+
</select>
|
|
494
|
+
{hasRealSelection && (
|
|
495
|
+
<button
|
|
496
|
+
type="button"
|
|
497
|
+
onClick={() => {
|
|
498
|
+
setSelectedBeliefTag('');
|
|
499
|
+
setDisclosures([]);
|
|
500
|
+
onUpdate(['BELIEF', '{}']);
|
|
501
|
+
}}
|
|
502
|
+
className="rounded p-1 text-red-600 hover:bg-gray-100"
|
|
503
|
+
>
|
|
504
|
+
<XMarkIcon className="h-5 w-5" />
|
|
505
|
+
</button>
|
|
506
|
+
)}
|
|
507
|
+
</div>
|
|
508
|
+
{hasRealSelection && (
|
|
509
|
+
<div className="mt-4 border-t border-gray-200 pt-4">
|
|
510
|
+
<button
|
|
511
|
+
type="button"
|
|
512
|
+
onClick={() => setIsModalOpen(true)}
|
|
513
|
+
className="flex w-full items-center justify-center rounded-md bg-gray-100 px-3 py-2 text-sm font-bold text-gray-700 hover:bg-gray-200"
|
|
514
|
+
>
|
|
515
|
+
<ChevronDownIcon className="mr-2 h-5 w-5" />
|
|
516
|
+
Configure {disclosures.filter((d) => !d.isDisabled).length} of{' '}
|
|
517
|
+
{disclosures.length} Disclosure(s) & Styles
|
|
518
|
+
</button>
|
|
519
|
+
</div>
|
|
520
|
+
)}
|
|
521
|
+
|
|
522
|
+
<Dialog.Root
|
|
523
|
+
open={isModalOpen}
|
|
524
|
+
onOpenChange={(details) => {
|
|
525
|
+
if (!details.open) {
|
|
526
|
+
handleUpdate();
|
|
527
|
+
setIsModalOpen(false);
|
|
528
|
+
}
|
|
529
|
+
}}
|
|
530
|
+
modal={true}
|
|
531
|
+
preventScroll={true}
|
|
532
|
+
>
|
|
533
|
+
<Portal>
|
|
534
|
+
<Dialog.Backdrop
|
|
535
|
+
className="fixed inset-0 bg-black bg-opacity-75 backdrop-blur-sm"
|
|
536
|
+
style={{ zIndex: 1001 }}
|
|
537
|
+
/>
|
|
538
|
+
<Dialog.Positioner
|
|
539
|
+
className="fixed inset-0 flex items-center justify-center p-4"
|
|
540
|
+
style={{ zIndex: 1001 }}
|
|
541
|
+
>
|
|
542
|
+
<Dialog.Content
|
|
543
|
+
className="w-full max-w-4xl overflow-hidden rounded-lg bg-slate-50 shadow-xl"
|
|
544
|
+
style={{ height: '80vh' }}
|
|
545
|
+
>
|
|
546
|
+
<div className="flex h-full flex-col">
|
|
547
|
+
<div className="flex-shrink-0 border-b border-gray-200 bg-white px-6 py-3">
|
|
548
|
+
<Dialog.Title className="text-lg font-bold text-gray-900">
|
|
549
|
+
Disclosure Configuration: {selectedBelief?.title}
|
|
550
|
+
</Dialog.Title>
|
|
551
|
+
</div>
|
|
552
|
+
<div className="flex-1 space-y-6 overflow-y-auto p-4">
|
|
553
|
+
{isDataLoaded ? (
|
|
554
|
+
<>
|
|
555
|
+
<div className="space-y-4 rounded-lg border border-gray-200 bg-white p-4 shadow-sm">
|
|
556
|
+
<h3 className="font-bold text-gray-800">
|
|
557
|
+
Widget Styles
|
|
558
|
+
</h3>
|
|
559
|
+
<div className="grid grid-cols-1 gap-4 sm:grid-cols-3">
|
|
560
|
+
<div>
|
|
561
|
+
<ColorPickerCombo
|
|
562
|
+
title="Background Color"
|
|
563
|
+
defaultColor={widgetStyles.bgColor}
|
|
564
|
+
onColorChange={(hex) =>
|
|
565
|
+
handleColorChange('bgColor', hex)
|
|
566
|
+
}
|
|
567
|
+
config={config}
|
|
568
|
+
allowNull={true}
|
|
569
|
+
skipTailwind={false}
|
|
570
|
+
/>
|
|
571
|
+
</div>
|
|
572
|
+
<div>
|
|
573
|
+
<ColorPickerCombo
|
|
574
|
+
title="Text Color"
|
|
575
|
+
defaultColor={widgetStyles.textColor}
|
|
576
|
+
onColorChange={(hex) =>
|
|
577
|
+
handleColorChange('textColor', hex)
|
|
578
|
+
}
|
|
579
|
+
config={config}
|
|
580
|
+
allowNull={true}
|
|
581
|
+
skipTailwind={false}
|
|
582
|
+
/>
|
|
583
|
+
</div>
|
|
584
|
+
<div>
|
|
585
|
+
<label className="block text-xs font-bold text-gray-600">
|
|
586
|
+
BG Opacity (%)
|
|
587
|
+
</label>
|
|
588
|
+
<div className="mt-1 flex items-center gap-2">
|
|
589
|
+
<input
|
|
590
|
+
type="range"
|
|
591
|
+
min="0"
|
|
592
|
+
max="100"
|
|
593
|
+
value={widgetStyles.bgOpacity}
|
|
594
|
+
onChange={(e) =>
|
|
595
|
+
updateWidgetStyles({
|
|
596
|
+
bgOpacity: parseInt(e.target.value),
|
|
597
|
+
})
|
|
598
|
+
}
|
|
599
|
+
className="w-full"
|
|
600
|
+
/>
|
|
601
|
+
<span className="w-12 text-center font-mono text-sm">
|
|
602
|
+
{widgetStyles.bgOpacity}%
|
|
603
|
+
</span>
|
|
604
|
+
</div>
|
|
605
|
+
</div>
|
|
606
|
+
</div>
|
|
607
|
+
</div>
|
|
608
|
+
|
|
609
|
+
{disclosures.map((item, index) => (
|
|
610
|
+
<DisclosureItemEditor
|
|
611
|
+
key={item.id}
|
|
612
|
+
item={item}
|
|
613
|
+
onUpdate={(updates) =>
|
|
614
|
+
updateDisclosure(item.id, updates)
|
|
615
|
+
}
|
|
616
|
+
onToggle={() => toggleDisclosure(item.id)}
|
|
617
|
+
config={config}
|
|
618
|
+
onMoveUp={() => moveDisclosure(item.id, 'up')}
|
|
619
|
+
onMoveDown={() => moveDisclosure(item.id, 'down')}
|
|
620
|
+
isFirst={index === 0}
|
|
621
|
+
isLast={index === disclosures.length - 1}
|
|
622
|
+
/>
|
|
623
|
+
))}
|
|
624
|
+
|
|
625
|
+
<div className="pt-4">
|
|
626
|
+
<button
|
|
627
|
+
type="button"
|
|
628
|
+
onClick={addCustomDisclosure}
|
|
629
|
+
className="flex w-full items-center justify-center rounded-md border-2 border-dashed border-gray-300 bg-white px-3 py-2 text-sm font-bold text-gray-500 hover:border-cyan-600 hover:text-cyan-600"
|
|
630
|
+
>
|
|
631
|
+
<PlusIcon className="mr-2 h-5 w-5" />
|
|
632
|
+
Add Custom Disclosure
|
|
633
|
+
</button>
|
|
634
|
+
</div>
|
|
635
|
+
</>
|
|
636
|
+
) : (
|
|
637
|
+
<div className="p-8 text-center">
|
|
638
|
+
Loading configuration...
|
|
639
|
+
</div>
|
|
640
|
+
)}
|
|
641
|
+
</div>
|
|
642
|
+
<div className="flex-shrink-0 justify-end border-t border-gray-200 bg-white px-6 py-3">
|
|
643
|
+
<Dialog.CloseTrigger asChild>
|
|
644
|
+
<button className="rounded bg-gray-600 px-4 py-2 text-sm font-bold text-white hover:bg-gray-700">
|
|
645
|
+
Close
|
|
646
|
+
</button>
|
|
647
|
+
</Dialog.CloseTrigger>
|
|
648
|
+
</div>
|
|
649
|
+
</div>
|
|
650
|
+
</Dialog.Content>
|
|
651
|
+
</Dialog.Positioner>
|
|
652
|
+
</Portal>
|
|
653
|
+
</Dialog.Root>
|
|
654
|
+
</div>
|
|
655
|
+
);
|
|
656
|
+
}
|