astro-tractstack 2.0.0-rc.53 → 2.0.0-rc.55

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 (23) hide show
  1. package/dist/index.js +12 -8
  2. package/package.json +1 -1
  3. package/templates/custom/minimal/CodeHook.astro +8 -6
  4. package/templates/custom/with-examples/CodeHook.astro +8 -4
  5. package/templates/src/components/codehooks/FeaturedArticle.astro +109 -0
  6. package/templates/src/components/codehooks/FeaturedArticleSetup.tsx +283 -0
  7. package/templates/src/components/codehooks/ListContent.astro +21 -162
  8. package/templates/src/components/codehooks/ListContentSetup.tsx +1 -4
  9. package/templates/src/components/codehooks/SearchWidget.tsx +444 -0
  10. package/templates/src/components/compositor/elements/BunnyVideo.tsx +8 -2
  11. package/templates/src/components/compositor/nodes/Pane.tsx +12 -14
  12. package/templates/src/components/compositor/preview/FeaturedArticlePreview.tsx +155 -0
  13. package/templates/src/components/edit/pane/AddPanePanel_codehook.tsx +6 -1
  14. package/templates/src/components/edit/pane/PageGenSelector.tsx +16 -16
  15. package/templates/src/components/edit/pane/PageGenSpecial.tsx +4 -44
  16. package/templates/src/components/edit/storyfragment/StoryFragmentPanel_og.tsx +1 -1
  17. package/templates/src/components/search/SearchModal.tsx +19 -3
  18. package/templates/src/types/tractstack.ts +1 -1
  19. package/templates/src/utils/helpers.ts +2 -2
  20. package/utils/inject-files.ts +10 -6
  21. package/templates/src/components/codehooks/FeaturedContent.astro +0 -273
  22. package/templates/src/components/codehooks/FeaturedContentSetup.tsx +0 -738
  23. package/templates/src/components/compositor/preview/FeaturedContentPreview.tsx +0 -128
package/dist/index.js CHANGED
@@ -549,9 +549,9 @@ async function w(t, e, c) {
549
549
  },
550
550
  {
551
551
  src: t(
552
- "../templates/src/components/compositor/preview/FeaturedContentPreview.tsx"
552
+ "../templates/src/components/compositor/preview/FeaturedArticlePreview.tsx"
553
553
  ),
554
- dest: "src/components/compositor/preview/FeaturedContentPreview.tsx"
554
+ dest: "src/components/compositor/preview/FeaturedArticlePreview.tsx"
555
555
  },
556
556
  // Server side stores
557
557
  {
@@ -1216,17 +1216,21 @@ async function w(t, e, c) {
1216
1216
  src: t("../templates/src/components/codehooks/SankeyDiagram.tsx"),
1217
1217
  dest: "src/components/codehooks/SankeyDiagram.tsx"
1218
1218
  },
1219
+ {
1220
+ src: t("../templates/src/components/codehooks/SearchWidget.tsx"),
1221
+ dest: "src/components/codehooks/SearchWidget.tsx"
1222
+ },
1219
1223
  {
1220
1224
  src: t(
1221
- "../templates/src/components/codehooks/FeaturedContent.astro"
1225
+ "../templates/src/components/codehooks/FeaturedArticle.astro"
1222
1226
  ),
1223
- dest: "src/components/codehooks/FeaturedContent.astro"
1227
+ dest: "src/components/codehooks/FeaturedArticle.astro"
1224
1228
  },
1225
1229
  {
1226
1230
  src: t(
1227
- "../templates/src/components/codehooks/FeaturedContentSetup.tsx"
1231
+ "../templates/src/components/codehooks/FeaturedArticleSetup.tsx"
1228
1232
  ),
1229
- dest: "src/components/codehooks/FeaturedContentSetup.tsx"
1233
+ dest: "src/components/codehooks/FeaturedArticleSetup.tsx"
1230
1234
  },
1231
1235
  {
1232
1236
  src: t("../templates/src/components/codehooks/ListContent.astro"),
@@ -2087,7 +2091,7 @@ export default function Placeholder() {
2087
2091
  }` : t.endsWith(".ts") ? `// TractStack placeholder utility
2088
2092
  export const placeholder = "${t}";` : `# TractStack placeholder: ${t}`;
2089
2093
  }
2090
- function S(t = {}) {
2094
+ function C(t = {}) {
2091
2095
  const { resolve: e } = b(import.meta.url);
2092
2096
  return {
2093
2097
  name: "astro-tractstack",
@@ -2169,5 +2173,5 @@ function S(t = {}) {
2169
2173
  };
2170
2174
  }
2171
2175
  export {
2172
- S as default
2176
+ C as default
2173
2177
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "astro-tractstack",
3
- "version": "2.0.0-rc.53",
3
+ "version": "2.0.0-rc.55",
4
4
  "description": "Astro integration for TractStack - redeeming the web from boring experiences",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -1,6 +1,7 @@
1
1
  ---
2
- import FeaturedContent from '@/components/codehooks/FeaturedContent.astro';
2
+ import FeaturedArticle from '@/components/codehooks/FeaturedArticle.astro';
3
3
  import ListContent from '@/components/codehooks/ListContent.astro';
4
+ import SearchWidget from '@/components/codehooks/SearchWidget.tsx';
4
5
  import BunnyVideoWrapper from '@/components/codehooks/BunnyVideoWrapper.astro';
5
6
  import EpinetWrapper from '@/components/codehooks/EpinetWrapper';
6
7
  import type { FullContentMapItem } from '@/types/tractstack';
@@ -21,8 +22,10 @@ export interface Props {
21
22
  const { target, options, fullContentMap /*, resourcesPayload */ } = Astro.props;
22
23
 
23
24
  export const components = {
25
+ 'featured-article': true,
24
26
  'featured-content': true,
25
27
  'list-content': true,
28
+ 'search-widget': true,
26
29
  'bunny-video': import.meta.env.PUBLIC_ENABLE_BUNNY === 'true',
27
30
  epinet: true,
28
31
  // "custom-hero": true, // Uncomment when you create CustomHero.astro
@@ -32,8 +35,10 @@ export const components = {
32
35
  {
33
36
  target === 'list-content' ? (
34
37
  <ListContent options={options} contentMap={fullContentMap} />
35
- ) : target === 'featured-content' ? (
36
- <FeaturedContent options={options} contentMap={fullContentMap} />
38
+ ) : target === 'featured-article' ? (
39
+ <FeaturedArticle options={options} contentMap={fullContentMap} />
40
+ ) : target === 'search-widget' ? (
41
+ <SearchWidget fullContentMap={fullContentMap} client:load />
37
42
  ) : target === 'bunny-video' && import.meta.env.PUBLIC_ENABLE_BUNNY ? (
38
43
  <BunnyVideoWrapper options={options} />
39
44
  ) : target === 'epinet' ? (
@@ -44,9 +49,6 @@ export const components = {
44
49
  ) : (
45
50
  <div class="rounded-lg bg-gray-50 p-8 text-center">
46
51
  <p class="text-gray-600">CodeHook target "{target}" not found</p>
47
- <p class="mt-2 text-sm text-gray-500">
48
- Available: list-content, featured-content, bunny-video, epinet
49
- </p>
50
52
  </div>
51
53
  )
52
54
  }
@@ -1,6 +1,7 @@
1
1
  ---
2
2
  import CustomHero from './CustomHero.astro';
3
- import FeaturedContent from '@/components/codehooks/FeaturedContent.astro';
3
+ import SearchWidget from '@/components/codehooks/SearchWidget.tsx';
4
+ import FeaturedArticle from '@/components/codehooks/FeaturedArticle.astro';
4
5
  import ListContent from '@/components/codehooks/ListContent.astro';
5
6
  import BunnyVideoWrapper from '@/components/codehooks/BunnyVideoWrapper.astro';
6
7
  import EpinetWrapper from '@/components/codehooks/EpinetWrapper';
@@ -22,8 +23,9 @@ const { target, options, fullContentMap /*, resourcesPayload */ } = Astro.props;
22
23
 
23
24
  export const components = {
24
25
  'custom-hero': true,
25
- 'featured-content': true,
26
+ 'featured-article': true,
26
27
  'list-content': true,
28
+ 'search-widget': true,
27
29
  'bunny-video': import.meta.env.PUBLIC_ENABLE_BUNNY === 'true',
28
30
  epinet: true,
29
31
  };
@@ -32,8 +34,10 @@ export const components = {
32
34
  {
33
35
  target === 'list-content' ? (
34
36
  <ListContent options={options} contentMap={fullContentMap} />
35
- ) : target === 'featured-content' ? (
36
- <FeaturedContent options={options} contentMap={fullContentMap} />
37
+ ) : target === 'featured-article' ? (
38
+ <FeaturedArticle options={options} contentMap={fullContentMap} />
39
+ ) : target === 'search-widget' ? (
40
+ <SearchWidget fullContentMap={fullContentMap} client:load />
37
41
  ) : target === 'bunny-video' && import.meta.env.PUBLIC_ENABLE_BUNNY ? (
38
42
  <BunnyVideoWrapper options={options} />
39
43
  ) : target === 'custom-hero' ? (
@@ -0,0 +1,109 @@
1
+ ---
2
+ import type { FullContentMapItem } from '@/types/tractstack';
3
+
4
+ export interface Props {
5
+ options?: {
6
+ params?: {
7
+ options?: string;
8
+ };
9
+ };
10
+ contentMap: FullContentMapItem[];
11
+ }
12
+
13
+ const { options, contentMap } = Astro.props;
14
+
15
+ // Parse component options
16
+ let parsedOptions;
17
+ try {
18
+ parsedOptions = JSON.parse(options?.params?.options || '{}');
19
+ } catch (e) {
20
+ console.error('Invalid options for FeaturedArticle', e);
21
+ parsedOptions = { slug: '' };
22
+ }
23
+
24
+ const slug = parsedOptions.slug || '';
25
+
26
+ // Find the featured story from the contentMap
27
+ // It must have a description, panes, AND a thumbnail image to be valid
28
+ const featuredStory = contentMap.find(
29
+ (item: FullContentMapItem) =>
30
+ item.slug === slug &&
31
+ item.type === 'StoryFragment' &&
32
+ item.description &&
33
+ item.panes &&
34
+ item.panes.length > 0 &&
35
+ item.thumbSrc
36
+ );
37
+ ---
38
+
39
+ {
40
+ featuredStory ? (
41
+ <div class="mx-auto w-full max-w-7xl px-8 py-24">
42
+ {/* A robust grid layout that ensures spacing and alignment */}
43
+ <div class="grid grid-cols-1 items-center gap-x-16 gap-y-12 md:grid-cols-2">
44
+ {/* --- TEXT COLUMN --- */}
45
+ <div class="w-full">
46
+ <a href={`/${featuredStory.slug}`} class="block">
47
+ {/* Constrained width for readability */}
48
+ <div class="max-w-lg pr-12">
49
+ <p class="font-action mb-4 text-lg font-bold uppercase text-gray-500">
50
+ Featured Article
51
+ </p>
52
+ <div class="space-y-6">
53
+ <h2 class="py-2 text-3xl font-bold leading-snug text-black transition-colors md:text-4xl xl:text-5xl">
54
+ {featuredStory.title}
55
+ </h2>
56
+ {featuredStory.description && (
57
+ <p class="text-sm leading-relaxed text-gray-700 md:text-lg xl:text-xl">
58
+ {featuredStory.description}
59
+ </p>
60
+ )}
61
+ {featuredStory.topics && featuredStory.topics.length > 0 && (
62
+ <div class="flex flex-wrap gap-2 pb-6 pt-2">
63
+ {featuredStory.topics.map((topic: string) => (
64
+ <span class="inline-flex items-center rounded-full bg-cyan-100 px-3 py-1 text-sm font-bold text-cyan-800">
65
+ {topic}
66
+ </span>
67
+ ))}
68
+ </div>
69
+ )}
70
+ </div>
71
+ </div>
72
+ </a>
73
+ </div>
74
+
75
+ {/* --- IMAGE COLUMN --- */}
76
+ <div class="w-full py-6">
77
+ {/* Max width constraint on the image container */}
78
+ <div class="mx-auto max-w-2xl">
79
+ <a href={`/${featuredStory.slug}`}>
80
+ <img
81
+ src={featuredStory.thumbSrc}
82
+ srcset={featuredStory.thumbSrcSet}
83
+ sizes="(min-width: 768px) 50vw, 100vw"
84
+ alt={`Preview of ${featuredStory.title}`}
85
+ class="w-full rounded-lg shadow-lg"
86
+ style="aspect-ratio: 1200 / 630;"
87
+ />
88
+ </a>
89
+ </div>
90
+ </div>
91
+ <div class="md:py-6">
92
+ <a
93
+ href={`/${featuredStory.slug}`}
94
+ class="inline-block w-fit rounded-md bg-gray-100 px-5 py-3 text-sm font-bold text-gray-900 transition-colors hover:bg-gray-200"
95
+ >
96
+ Read More
97
+ </a>
98
+ </div>
99
+ </div>
100
+ </div>
101
+ ) : (
102
+ <div class="mx-auto max-w-7xl px-4 py-16">
103
+ <p class="italic text-cyan-600">
104
+ Featured article not found or is missing required content (description,
105
+ panes, thumbnail).
106
+ </p>
107
+ </div>
108
+ )
109
+ }
@@ -0,0 +1,283 @@
1
+ import { useState, useEffect, useMemo, useRef } from 'react';
2
+ import { useStore } from '@nanostores/react';
3
+ import { Combobox } from '@ark-ui/react';
4
+ import { createListCollection } from '@ark-ui/react/collection';
5
+ import { ChevronUpDownIcon, CheckIcon } from '@heroicons/react/20/solid';
6
+ import { fullContentMapStore, viewportKeyStore } from '@/stores/storykeep';
7
+ import { getCtx } from '@/stores/nodes';
8
+ import { cloneDeep } from '@/utils/helpers';
9
+ import type { PaneNode } from '@/types/compositorTypes';
10
+
11
+ interface FeaturedArticleSetupProps {
12
+ params?: Record<string, string>;
13
+ nodeId: string;
14
+ }
15
+
16
+ const comboboxItemStyles = `
17
+ .combo-item .check-indicator {
18
+ display: none;
19
+ }
20
+ .combo-item[data-state="checked"] .check-indicator {
21
+ display: flex;
22
+ }
23
+ `;
24
+
25
+ const FeaturedArticleSetup = ({
26
+ params,
27
+ nodeId,
28
+ }: FeaturedArticleSetupProps) => {
29
+ const $contentMap = useStore(fullContentMapStore);
30
+ const $viewportKey = useStore(viewportKeyStore);
31
+ const isInitialMount = useRef(true);
32
+ const ctx = getCtx();
33
+
34
+ const availableStories = useMemo(
35
+ () =>
36
+ $contentMap.filter(
37
+ (item) =>
38
+ item.type === 'StoryFragment' &&
39
+ item.description &&
40
+ item.panes &&
41
+ item.panes.length > 0 &&
42
+ item.thumbSrc
43
+ ),
44
+ [$contentMap]
45
+ );
46
+
47
+ const initialSlug = params?.slug || '';
48
+ const initialStory = availableStories.find(
49
+ (story) => story.slug === initialSlug
50
+ );
51
+
52
+ const [isPanelOpen, setIsPanelOpen] = useState(false);
53
+ const [selectedSlug, setSelectedSlug] = useState(initialSlug);
54
+ const [query, setQuery] = useState(initialStory?.title || '');
55
+
56
+ const selectedStory = useMemo(
57
+ () => availableStories.find((story) => story.slug === selectedSlug),
58
+ [availableStories, selectedSlug]
59
+ );
60
+
61
+ const collection = useMemo(() => {
62
+ const filtered =
63
+ query === '' || query === selectedStory?.title
64
+ ? availableStories
65
+ : availableStories.filter((story) =>
66
+ story.title.toLowerCase().includes(query.toLowerCase())
67
+ );
68
+ return createListCollection({
69
+ items: filtered,
70
+ itemToValue: (item) => item.slug,
71
+ itemToString: (item) => item.title,
72
+ });
73
+ }, [availableStories, query, selectedStory]);
74
+
75
+ const updatePaneNode = () => {
76
+ if (!nodeId) return;
77
+ const allNodes = ctx.allNodes.get();
78
+ const paneNode = cloneDeep(allNodes.get(nodeId)) as PaneNode;
79
+ if (paneNode) {
80
+ const updatedNode = {
81
+ ...paneNode,
82
+ codeHookTarget: 'featured-article',
83
+ codeHookPayload: {
84
+ options: JSON.stringify({ slug: selectedSlug }),
85
+ },
86
+ isChanged: true,
87
+ };
88
+ ctx.modifyNodes([updatedNode]);
89
+ }
90
+ };
91
+
92
+ useEffect(() => {
93
+ if (isInitialMount.current) {
94
+ isInitialMount.current = false;
95
+ return;
96
+ }
97
+ const timeoutId = setTimeout(updatePaneNode, 500);
98
+ return () => clearTimeout(timeoutId);
99
+ }, [selectedSlug]);
100
+
101
+ const handleSelection = (details: { value: string[] }) => {
102
+ const slug = details.value[0] || '';
103
+ setSelectedSlug(slug);
104
+ const story = availableStories.find((s) => s.slug === slug);
105
+ if (story) {
106
+ setQuery(story.title);
107
+ } else {
108
+ setQuery('');
109
+ }
110
+ };
111
+
112
+ const renderPreview = () => {
113
+ if (!selectedStory) return null;
114
+
115
+ const topics = selectedStory.topics && selectedStory.topics.length > 0 && (
116
+ <div className="flex flex-wrap gap-2 pt-2">
117
+ {selectedStory.topics.map((topic) => (
118
+ <span
119
+ key={topic}
120
+ className="inline-flex items-center rounded-full bg-cyan-100 px-3 py-1 text-sm font-bold text-cyan-800"
121
+ >
122
+ {topic}
123
+ </span>
124
+ ))}
125
+ </div>
126
+ );
127
+
128
+ // Mobile is the default, single-column layout
129
+ if ($viewportKey.value === 'mobile') {
130
+ return (
131
+ <div className="flex flex-col gap-8 pt-4">
132
+ <div className="w-full">
133
+ <p className="font-action text-md mb-4 font-bold uppercase text-gray-500">
134
+ Featured Article
135
+ </p>
136
+ <div className="space-y-6">
137
+ <h2 className="text-4xl font-bold leading-snug">
138
+ {selectedStory.title}
139
+ </h2>
140
+ <p className="text-lg leading-loose text-gray-700">
141
+ {selectedStory.description}
142
+ </p>
143
+ {topics}
144
+ </div>
145
+ </div>
146
+ <div className="w-full">
147
+ <img
148
+ src={selectedStory.thumbSrc}
149
+ srcSet={selectedStory.thumbSrcSet}
150
+ alt={`Preview of ${selectedStory.title}`}
151
+ className="w-full rounded-lg shadow-lg"
152
+ style={{ aspectRatio: '1200 / 630' }}
153
+ />
154
+ </div>
155
+ </div>
156
+ );
157
+ }
158
+
159
+ // Tablet and Desktop share the same two-column layout
160
+ return (
161
+ <div className="flex flex-row items-center gap-12 pt-4">
162
+ <div className="w-3/5">
163
+ <p className="font-action text-md mb-4 font-bold uppercase text-gray-500">
164
+ Featured Article
165
+ </p>
166
+ <div className="space-y-6">
167
+ <h2 className="text-5xl font-bold leading-snug">
168
+ {selectedStory.title}
169
+ </h2>
170
+ <p className="text-lg leading-loose text-gray-700">
171
+ {selectedStory.description}
172
+ </p>
173
+ {topics}
174
+ </div>
175
+ </div>
176
+ <div className="w-2/5">
177
+ <img
178
+ src={selectedStory.thumbSrc}
179
+ srcSet={selectedStory.thumbSrcSet}
180
+ alt={`Preview of ${selectedStory.title}`}
181
+ className="w-full rounded-lg shadow-lg"
182
+ style={{ aspectRatio: '1200 / 630' }}
183
+ />
184
+ </div>
185
+ </div>
186
+ );
187
+ };
188
+
189
+ if (!isPanelOpen) {
190
+ return (
191
+ <div className="flex min-h-[200px] w-full flex-col items-center justify-center space-y-6 rounded-lg bg-slate-50 p-6">
192
+ <button
193
+ onClick={() => setIsPanelOpen(true)}
194
+ className="rounded-lg bg-cyan-600 px-6 py-3 font-bold text-white shadow-md transition-colors hover:bg-cyan-700"
195
+ >
196
+ {selectedStory
197
+ ? 'Edit Featured Article'
198
+ : 'Configure Featured Article'}
199
+ </button>
200
+ {selectedStory && (
201
+ <div className="mt-3 text-center text-sm text-gray-600">
202
+ Currently featuring:
203
+ <br />
204
+ <span className="font-bold">{selectedStory.title}</span>
205
+ </div>
206
+ )}
207
+ </div>
208
+ );
209
+ }
210
+
211
+ return (
212
+ <div className="w-full space-y-6 bg-slate-50 p-6">
213
+ <style>{comboboxItemStyles}</style>
214
+ <div className="flex items-center justify-between">
215
+ <h2 className="text-xl font-bold text-gray-900">
216
+ Configure Featured Article
217
+ </h2>
218
+ <button
219
+ onClick={() => setIsPanelOpen(false)}
220
+ className="rounded bg-gray-200 px-4 py-2 font-bold text-gray-800 transition-colors hover:bg-gray-300"
221
+ >
222
+ Close
223
+ </button>
224
+ </div>
225
+
226
+ <div className="rounded-lg bg-white p-4 shadow">
227
+ <label className="block text-sm font-bold text-gray-700">
228
+ Select an Article
229
+ </label>
230
+ <p className="mt-1 text-xs text-gray-500">
231
+ Only articles with a description, content, and thumbnail will be
232
+ shown.
233
+ </p>
234
+ <Combobox.Root
235
+ collection={collection}
236
+ value={selectedSlug ? [selectedSlug] : []}
237
+ inputValue={query}
238
+ onValueChange={handleSelection}
239
+ onInputValueChange={(details) => setQuery(details.inputValue)}
240
+ className="mt-2"
241
+ >
242
+ <div className="relative">
243
+ <Combobox.Input
244
+ className="w-full rounded-md border border-gray-300 py-2 pl-3 pr-10 text-sm focus:border-cyan-500 focus:outline-none focus:ring-1 focus:ring-cyan-500"
245
+ placeholder="Search for an article..."
246
+ />
247
+ <Combobox.Trigger className="absolute inset-y-0 right-0 flex items-center pr-2">
248
+ <ChevronUpDownIcon
249
+ className="h-5 w-5 text-gray-400"
250
+ aria-hidden="true"
251
+ />
252
+ </Combobox.Trigger>
253
+ </div>
254
+ <Combobox.Content className="absolute z-50 mt-1 max-h-60 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm">
255
+ {collection.items.map((item) => (
256
+ <Combobox.Item
257
+ key={item.slug}
258
+ item={item}
259
+ className="combo-item relative cursor-default select-none py-2 pl-10 pr-4 text-gray-900 data-[highlighted]:bg-cyan-600 data-[highlighted]:text-white"
260
+ >
261
+ <span className="block truncate">{item.title}</span>
262
+ <span className="check-indicator absolute inset-y-0 left-0 flex items-center pl-3 text-cyan-600">
263
+ <CheckIcon className="h-5 w-5" aria-hidden="true" />
264
+ </span>
265
+ </Combobox.Item>
266
+ ))}
267
+ </Combobox.Content>
268
+ </Combobox.Root>
269
+ </div>
270
+
271
+ {selectedStory && (
272
+ <div className="rounded-lg bg-white p-4 shadow">
273
+ <h3 className="border-b border-gray-200 pb-2 text-lg font-bold">
274
+ Live Preview
275
+ </h3>
276
+ {renderPreview()}
277
+ </div>
278
+ )}
279
+ </div>
280
+ );
281
+ };
282
+
283
+ export default FeaturedArticleSetup;