astro-tractstack 2.0.0-rc.8 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (141) hide show
  1. package/LICENSE +8 -97
  2. package/README.md +7 -5
  3. package/bin/create-tractstack.js +35 -11
  4. package/dist/index.js +106 -29
  5. package/package.json +10 -5
  6. package/templates/css/frontend.css +1 -1
  7. package/templates/custom/minimal/CodeHook.astro +13 -12
  8. package/templates/custom/minimal/CustomRoutes.astro +25 -31
  9. package/templates/custom/with-examples/CodeHook.astro +22 -11
  10. package/templates/custom/with-examples/CustomRoutes.astro +4 -8
  11. package/templates/custom/with-examples/ProductCard.astro +29 -0
  12. package/templates/custom/with-examples/ProductCardWrapper.astro +43 -0
  13. package/templates/custom/with-examples/ProductGrid.astro +64 -0
  14. package/templates/custom/with-examples/pages/Collections.astro +58 -98
  15. package/templates/gitignore +42 -0
  16. package/templates/prettierignore +5 -0
  17. package/templates/prettierrc +19 -0
  18. package/templates/src/client/app.js +127 -0
  19. package/templates/src/client/htmx.min.js +3519 -0
  20. package/templates/src/client/view.js +429 -0
  21. package/templates/src/components/Footer.astro +4 -9
  22. package/templates/src/components/Header.astro +67 -60
  23. package/templates/src/components/Menu.tsx +188 -52
  24. package/templates/src/components/codehooks/BunnyVideoSetup.tsx +2 -2
  25. package/templates/src/components/codehooks/EpinetDurationSelector.tsx +9 -13
  26. package/templates/src/components/codehooks/EpinetTableView.tsx +11 -7
  27. package/templates/src/components/codehooks/EpinetWrapper.tsx +1 -0
  28. package/templates/src/components/codehooks/FeaturedArticle.astro +105 -0
  29. package/templates/src/components/codehooks/FeaturedArticleSetup.tsx +318 -0
  30. package/templates/src/components/codehooks/ListContent.astro +32 -162
  31. package/templates/src/components/codehooks/ListContentSetup.tsx +43 -138
  32. package/templates/src/components/codehooks/ProductCardSetup.tsx +152 -0
  33. package/templates/src/components/codehooks/ProductGridSetup.tsx +274 -0
  34. package/templates/src/components/codehooks/SearchWidget.tsx +453 -0
  35. package/templates/src/components/compositor/Node.tsx +3 -6
  36. package/templates/src/components/compositor/PanelVisibilityWrapper.tsx +21 -11
  37. package/templates/src/components/compositor/elements/BunnyVideo.tsx +21 -20
  38. package/templates/src/components/compositor/nodes/Pane.tsx +51 -21
  39. package/templates/src/components/compositor/nodes/RenderChildren.tsx +6 -1
  40. package/templates/src/components/compositor/nodes/Widget.tsx +16 -2
  41. package/templates/src/components/compositor/preview/FeaturedArticlePreview.tsx +155 -0
  42. package/templates/src/components/compositor/preview/PaneSnapshotGenerator.tsx +20 -1
  43. package/templates/src/components/edit/Header.tsx +10 -4
  44. package/templates/src/components/edit/PanelSwitch.tsx +11 -7
  45. package/templates/src/components/edit/SettingsPanel.tsx +29 -18
  46. package/templates/src/components/edit/ToolBar.tsx +1 -28
  47. package/templates/src/components/edit/ToolMode.tsx +45 -32
  48. package/templates/src/components/edit/pane/AddPanePanel_break.tsx +12 -2
  49. package/templates/src/components/edit/pane/AddPanePanel_codehook.tsx +8 -2
  50. package/templates/src/components/edit/pane/AddPanePanel_newAICopy_modal.tsx +1 -1
  51. package/templates/src/components/edit/pane/ConfigPanePanel.tsx +17 -27
  52. package/templates/src/components/edit/pane/PageGenSelector.tsx +16 -16
  53. package/templates/src/components/edit/pane/PageGenSpecial.tsx +26 -49
  54. package/templates/src/components/edit/pane/PageGen_preview.tsx +17 -2
  55. package/templates/src/components/edit/pane/PanePanel_path.tsx +2 -4
  56. package/templates/src/components/edit/pane/PanePanel_title.tsx +243 -76
  57. package/templates/src/components/edit/panels/StyleBreakPanel.tsx +17 -19
  58. package/templates/src/components/edit/panels/StyleCodeHookPanel.tsx +48 -37
  59. package/templates/src/components/edit/panels/StyleElementPanel_add.tsx +60 -55
  60. package/templates/src/components/edit/panels/StyleImagePanel_add.tsx +56 -50
  61. package/templates/src/components/edit/panels/StyleLiElementPanel_add.tsx +54 -47
  62. package/templates/src/components/edit/panels/StyleLinkPanel_add.tsx +54 -44
  63. package/templates/src/components/edit/panels/StyleLinkPanel_config.tsx +113 -138
  64. package/templates/src/components/edit/panels/StyleParentPanel_add.tsx +54 -40
  65. package/templates/src/components/edit/panels/StyleWidgetPanel.tsx +3 -3
  66. package/templates/src/components/edit/panels/StyleWidgetPanel_add.tsx +56 -49
  67. package/templates/src/components/edit/panels/StyleWidgetPanel_config.tsx +14 -5
  68. package/templates/src/components/edit/state/SaveModal.tsx +316 -169
  69. package/templates/src/components/edit/storyfragment/StoryFragmentPanel_og.tsx +1 -1
  70. package/templates/src/components/edit/storyfragment/StoryFragmentPanel_slug.tsx +56 -55
  71. package/templates/src/components/edit/widgets/BunnyWidget.tsx +538 -59
  72. package/templates/src/components/edit/widgets/InteractiveDisclosureWidget.tsx +656 -0
  73. package/templates/src/components/edit/widgets/ToggleWidget.tsx +9 -16
  74. package/templates/src/components/fields/ArtpackImage.tsx +4 -1
  75. package/templates/src/components/fields/BackgroundImage.tsx +1 -1
  76. package/templates/src/components/fields/BackgroundImageWrapper.tsx +127 -35
  77. package/templates/src/components/fields/ColorPickerCombo.tsx +66 -62
  78. package/templates/src/components/fields/ImageUpload.tsx +1 -1
  79. package/templates/src/components/fields/ViewportComboBox.tsx +59 -42
  80. package/templates/src/components/form/ActionBuilderBeliefSelector.tsx +117 -0
  81. package/templates/src/components/form/ActionBuilderField.tsx +306 -87
  82. package/templates/src/components/search/SearchModal.tsx +420 -0
  83. package/templates/src/components/search/SearchResults.tsx +367 -0
  84. package/templates/src/components/search/SearchWrapper.tsx +46 -0
  85. package/templates/src/components/storykeep/Dashboard_Advanced.tsx +1 -1
  86. package/templates/src/components/storykeep/Dashboard_Analytics.tsx +34 -8
  87. package/templates/src/components/storykeep/Dashboard_Content.tsx +6 -0
  88. package/templates/src/components/storykeep/StoryKeepBackdrop.astro +87 -0
  89. package/templates/src/components/storykeep/controls/content/BeliefForm.tsx +37 -33
  90. package/templates/src/components/storykeep/controls/content/MenuForm.tsx +55 -7
  91. package/templates/src/components/storykeep/controls/content/ResourceForm.tsx +17 -2
  92. package/templates/src/components/storykeep/controls/content/StoryFragmentTable.tsx +5 -8
  93. package/templates/src/components/storykeep/state/FetchAnalytics.tsx +274 -228
  94. package/templates/src/components/storykeep/widgets/Wizard.tsx +14 -7
  95. package/templates/src/components/tenant/RegistrationForm.tsx +1 -1
  96. package/templates/src/components/widgets/ImpressionWrapper.tsx +0 -1
  97. package/templates/src/constants/shapes.ts +9 -0
  98. package/templates/src/constants.ts +2121 -16
  99. package/templates/src/hooks/useSearch.ts +228 -0
  100. package/templates/src/layouts/Layout.astro +213 -104
  101. package/templates/src/lib/storyData.ts +4 -1
  102. package/templates/src/pages/[...slug]/edit.astro +14 -14
  103. package/templates/src/pages/[...slug].astro +82 -21
  104. package/templates/src/pages/api/orphan-analysis.ts +0 -1
  105. package/templates/src/pages/api/tailwind.ts +23 -21
  106. package/templates/src/pages/context/[...contextSlug]/edit.astro +14 -14
  107. package/templates/src/pages/context/[...contextSlug].astro +7 -2
  108. package/templates/src/pages/storykeep/advanced.astro +5 -4
  109. package/templates/src/pages/storykeep/branding.astro +5 -4
  110. package/templates/src/pages/storykeep/content.astro +5 -4
  111. package/templates/src/pages/storykeep/init.astro +40 -1
  112. package/templates/src/pages/storykeep/login.astro +1 -1
  113. package/templates/src/pages/storykeep.astro +5 -4
  114. package/templates/src/stores/nodes.ts +59 -88
  115. package/templates/src/stores/orphanAnalysis.ts +19 -21
  116. package/templates/src/stores/storykeep.ts +7 -0
  117. package/templates/src/types/compositorTypes.ts +6 -0
  118. package/templates/src/types/tractstack.ts +17 -0
  119. package/templates/src/utils/actions/lispLexer.ts +2 -2
  120. package/templates/src/utils/actions/preParse_Action.ts +3 -0
  121. package/templates/src/utils/api/beliefHelpers.ts +12 -36
  122. package/templates/src/utils/api/menuHelpers.ts +2 -2
  123. package/templates/src/utils/api.ts +26 -0
  124. package/templates/src/utils/compositor/TemplateNodes.ts +7 -0
  125. package/templates/src/utils/compositor/allowInsert.ts +5 -3
  126. package/templates/src/utils/compositor/nodesHelper.ts +4 -0
  127. package/templates/src/utils/compositor/processMarkdown.ts +16 -2
  128. package/templates/src/utils/compositor/reduceNodesClassNames.ts +4 -0
  129. package/templates/src/utils/compositor/templateMarkdownStyles.ts +13 -13
  130. package/templates/src/utils/compositor/typeGuards.ts +1 -0
  131. package/templates/src/utils/customHelpers.ts +38 -0
  132. package/templates/src/utils/helpers.ts +2 -2
  133. package/templates/src/utils/layout.ts +65 -144
  134. package/utils/inject-files.ts +95 -18
  135. package/templates/src/client/analytics-events.js +0 -207
  136. package/templates/src/client/belief-events.js +0 -191
  137. package/templates/src/client/sse.js +0 -613
  138. package/templates/src/components/codehooks/FeaturedContent.astro +0 -273
  139. package/templates/src/components/codehooks/FeaturedContentSetup.tsx +0 -738
  140. package/templates/src/components/compositor/preview/FeaturedContentPreview.tsx +0 -128
  141. 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
+ }