dineway 0.1.3 → 0.1.4

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 (191) hide show
  1. package/package.json +6 -3
  2. package/src/astro/routes/PluginRegistry.tsx +21 -0
  3. package/src/astro/routes/admin.astro +83 -0
  4. package/src/astro/routes/api/admin/allowed-domains/[domain].ts +112 -0
  5. package/src/astro/routes/api/admin/allowed-domains/index.ts +108 -0
  6. package/src/astro/routes/api/admin/api-tokens/[id].ts +40 -0
  7. package/src/astro/routes/api/admin/api-tokens/index.ts +68 -0
  8. package/src/astro/routes/api/admin/bylines/[id]/index.ts +87 -0
  9. package/src/astro/routes/api/admin/bylines/index.ts +72 -0
  10. package/src/astro/routes/api/admin/comments/[id]/status.ts +120 -0
  11. package/src/astro/routes/api/admin/comments/[id].ts +64 -0
  12. package/src/astro/routes/api/admin/comments/bulk.ts +42 -0
  13. package/src/astro/routes/api/admin/comments/counts.ts +30 -0
  14. package/src/astro/routes/api/admin/comments/index.ts +46 -0
  15. package/src/astro/routes/api/admin/hooks/exclusive/[hookName].ts +91 -0
  16. package/src/astro/routes/api/admin/hooks/exclusive/index.ts +51 -0
  17. package/src/astro/routes/api/admin/oauth-clients/[id].ts +110 -0
  18. package/src/astro/routes/api/admin/oauth-clients/index.ts +71 -0
  19. package/src/astro/routes/api/admin/plugins/[id]/disable.ts +39 -0
  20. package/src/astro/routes/api/admin/plugins/[id]/enable.ts +39 -0
  21. package/src/astro/routes/api/admin/plugins/[id]/index.ts +38 -0
  22. package/src/astro/routes/api/admin/plugins/[id]/uninstall.ts +48 -0
  23. package/src/astro/routes/api/admin/plugins/[id]/update.ts +59 -0
  24. package/src/astro/routes/api/admin/plugins/index.ts +32 -0
  25. package/src/astro/routes/api/admin/plugins/marketplace/[id]/icon.ts +62 -0
  26. package/src/astro/routes/api/admin/plugins/marketplace/[id]/index.ts +33 -0
  27. package/src/astro/routes/api/admin/plugins/marketplace/[id]/install.ts +64 -0
  28. package/src/astro/routes/api/admin/plugins/marketplace/index.ts +38 -0
  29. package/src/astro/routes/api/admin/plugins/updates.ts +28 -0
  30. package/src/astro/routes/api/admin/themes/marketplace/[id]/index.ts +33 -0
  31. package/src/astro/routes/api/admin/themes/marketplace/[id]/thumbnail.ts +62 -0
  32. package/src/astro/routes/api/admin/themes/marketplace/index.ts +45 -0
  33. package/src/astro/routes/api/admin/users/[id]/disable.ts +69 -0
  34. package/src/astro/routes/api/admin/users/[id]/enable.ts +48 -0
  35. package/src/astro/routes/api/admin/users/[id]/index.ts +146 -0
  36. package/src/astro/routes/api/admin/users/[id]/send-recovery.ts +72 -0
  37. package/src/astro/routes/api/admin/users/index.ts +66 -0
  38. package/src/astro/routes/api/auth/dev-bypass.ts +139 -0
  39. package/src/astro/routes/api/auth/invite/accept.ts +52 -0
  40. package/src/astro/routes/api/auth/invite/complete.ts +86 -0
  41. package/src/astro/routes/api/auth/invite/index.ts +99 -0
  42. package/src/astro/routes/api/auth/logout.ts +40 -0
  43. package/src/astro/routes/api/auth/magic-link/send.ts +89 -0
  44. package/src/astro/routes/api/auth/magic-link/verify.ts +71 -0
  45. package/src/astro/routes/api/auth/me.ts +60 -0
  46. package/src/astro/routes/api/auth/oauth/[provider]/callback.ts +221 -0
  47. package/src/astro/routes/api/auth/oauth/[provider].ts +120 -0
  48. package/src/astro/routes/api/auth/passkey/[id].ts +124 -0
  49. package/src/astro/routes/api/auth/passkey/index.ts +54 -0
  50. package/src/astro/routes/api/auth/passkey/options.ts +84 -0
  51. package/src/astro/routes/api/auth/passkey/register/options.ts +88 -0
  52. package/src/astro/routes/api/auth/passkey/register/verify.ts +119 -0
  53. package/src/astro/routes/api/auth/passkey/verify.ts +68 -0
  54. package/src/astro/routes/api/auth/signup/complete.ts +87 -0
  55. package/src/astro/routes/api/auth/signup/request.ts +77 -0
  56. package/src/astro/routes/api/auth/signup/verify.ts +53 -0
  57. package/src/astro/routes/api/comments/[collection]/[contentId]/index.ts +311 -0
  58. package/src/astro/routes/api/content/[collection]/[id]/compare.ts +28 -0
  59. package/src/astro/routes/api/content/[collection]/[id]/discard-draft.ts +54 -0
  60. package/src/astro/routes/api/content/[collection]/[id]/duplicate.ts +61 -0
  61. package/src/astro/routes/api/content/[collection]/[id]/permanent.ts +33 -0
  62. package/src/astro/routes/api/content/[collection]/[id]/preview-url.ts +107 -0
  63. package/src/astro/routes/api/content/[collection]/[id]/publish.ts +56 -0
  64. package/src/astro/routes/api/content/[collection]/[id]/restore.ts +54 -0
  65. package/src/astro/routes/api/content/[collection]/[id]/revisions.ts +31 -0
  66. package/src/astro/routes/api/content/[collection]/[id]/schedule.ts +105 -0
  67. package/src/astro/routes/api/content/[collection]/[id]/terms/[taxonomy].ts +140 -0
  68. package/src/astro/routes/api/content/[collection]/[id]/translations.ts +30 -0
  69. package/src/astro/routes/api/content/[collection]/[id]/unpublish.ts +56 -0
  70. package/src/astro/routes/api/content/[collection]/[id].ts +137 -0
  71. package/src/astro/routes/api/content/[collection]/index.ts +59 -0
  72. package/src/astro/routes/api/content/[collection]/trash.ts +33 -0
  73. package/src/astro/routes/api/dashboard.ts +32 -0
  74. package/src/astro/routes/api/dev/emails.ts +36 -0
  75. package/src/astro/routes/api/import/probe.ts +47 -0
  76. package/src/astro/routes/api/import/wordpress/analyze.ts +531 -0
  77. package/src/astro/routes/api/import/wordpress/execute.ts +296 -0
  78. package/src/astro/routes/api/import/wordpress/media.ts +338 -0
  79. package/src/astro/routes/api/import/wordpress/prepare.ts +181 -0
  80. package/src/astro/routes/api/import/wordpress/rewrite-urls.ts +393 -0
  81. package/src/astro/routes/api/import/wordpress-plugin/analyze.ts +111 -0
  82. package/src/astro/routes/api/import/wordpress-plugin/callback.ts +58 -0
  83. package/src/astro/routes/api/import/wordpress-plugin/execute.ts +357 -0
  84. package/src/astro/routes/api/manifest.ts +63 -0
  85. package/src/astro/routes/api/mcp.ts +124 -0
  86. package/src/astro/routes/api/media/[id]/confirm.ts +93 -0
  87. package/src/astro/routes/api/media/[id].ts +145 -0
  88. package/src/astro/routes/api/media/file/[...key].ts +79 -0
  89. package/src/astro/routes/api/media/providers/[providerId]/[itemId].ts +86 -0
  90. package/src/astro/routes/api/media/providers/[providerId]/index.ts +111 -0
  91. package/src/astro/routes/api/media/providers/index.ts +30 -0
  92. package/src/astro/routes/api/media/upload-url.ts +137 -0
  93. package/src/astro/routes/api/media.ts +202 -0
  94. package/src/astro/routes/api/menus/[name]/items.ts +87 -0
  95. package/src/astro/routes/api/menus/[name]/reorder.ts +33 -0
  96. package/src/astro/routes/api/menus/[name].ts +65 -0
  97. package/src/astro/routes/api/menus/index.ts +47 -0
  98. package/src/astro/routes/api/oauth/authorize.ts +417 -0
  99. package/src/astro/routes/api/oauth/device/authorize.ts +45 -0
  100. package/src/astro/routes/api/oauth/device/code.ts +55 -0
  101. package/src/astro/routes/api/oauth/device/token.ts +69 -0
  102. package/src/astro/routes/api/oauth/token/refresh.ts +38 -0
  103. package/src/astro/routes/api/oauth/token/revoke.ts +38 -0
  104. package/src/astro/routes/api/oauth/token.ts +184 -0
  105. package/src/astro/routes/api/openapi.json.ts +32 -0
  106. package/src/astro/routes/api/plugins/[pluginId]/[...path].ts +92 -0
  107. package/src/astro/routes/api/redirects/404s/index.ts +72 -0
  108. package/src/astro/routes/api/redirects/404s/summary.ts +33 -0
  109. package/src/astro/routes/api/redirects/[id].ts +84 -0
  110. package/src/astro/routes/api/redirects/index.ts +52 -0
  111. package/src/astro/routes/api/revisions/[revisionId]/index.ts +29 -0
  112. package/src/astro/routes/api/revisions/[revisionId]/restore.ts +62 -0
  113. package/src/astro/routes/api/schema/collections/[slug]/fields/[fieldSlug].ts +76 -0
  114. package/src/astro/routes/api/schema/collections/[slug]/fields/index.ts +52 -0
  115. package/src/astro/routes/api/schema/collections/[slug]/fields/reorder.ts +32 -0
  116. package/src/astro/routes/api/schema/collections/[slug]/index.ts +80 -0
  117. package/src/astro/routes/api/schema/collections/index.ts +47 -0
  118. package/src/astro/routes/api/schema/index.ts +109 -0
  119. package/src/astro/routes/api/schema/orphans/[slug].ts +36 -0
  120. package/src/astro/routes/api/schema/orphans/index.ts +26 -0
  121. package/src/astro/routes/api/search/enable.ts +64 -0
  122. package/src/astro/routes/api/search/index.ts +51 -0
  123. package/src/astro/routes/api/search/rebuild.ts +72 -0
  124. package/src/astro/routes/api/search/stats.ts +35 -0
  125. package/src/astro/routes/api/search/suggest.ts +49 -0
  126. package/src/astro/routes/api/sections/[slug].ts +84 -0
  127. package/src/astro/routes/api/sections/index.ts +52 -0
  128. package/src/astro/routes/api/settings/email.ts +150 -0
  129. package/src/astro/routes/api/settings.ts +67 -0
  130. package/src/astro/routes/api/setup/admin-verify.ts +102 -0
  131. package/src/astro/routes/api/setup/admin.ts +96 -0
  132. package/src/astro/routes/api/setup/dev-bypass.ts +200 -0
  133. package/src/astro/routes/api/setup/dev-reset.ts +40 -0
  134. package/src/astro/routes/api/setup/index.ts +127 -0
  135. package/src/astro/routes/api/setup/status.ts +122 -0
  136. package/src/astro/routes/api/snapshot.ts +76 -0
  137. package/src/astro/routes/api/taxonomies/[name]/terms/[slug].ts +95 -0
  138. package/src/astro/routes/api/taxonomies/[name]/terms/index.ts +69 -0
  139. package/src/astro/routes/api/taxonomies/index.ts +59 -0
  140. package/src/astro/routes/api/themes/preview.ts +78 -0
  141. package/src/astro/routes/api/typegen.ts +114 -0
  142. package/src/astro/routes/api/well-known/auth.ts +69 -0
  143. package/src/astro/routes/api/well-known/oauth-authorization-server.ts +45 -0
  144. package/src/astro/routes/api/well-known/oauth-protected-resource.ts +38 -0
  145. package/src/astro/routes/api/widget-areas/[name]/reorder.ts +72 -0
  146. package/src/astro/routes/api/widget-areas/[name]/widgets/[id].ts +127 -0
  147. package/src/astro/routes/api/widget-areas/[name]/widgets.ts +80 -0
  148. package/src/astro/routes/api/widget-areas/[name].ts +87 -0
  149. package/src/astro/routes/api/widget-areas/index.ts +99 -0
  150. package/src/astro/routes/api/widget-components.ts +22 -0
  151. package/src/astro/routes/robots.txt.ts +81 -0
  152. package/src/astro/routes/sitemap-[collection].xml.ts +104 -0
  153. package/src/astro/routes/sitemap.xml.ts +92 -0
  154. package/src/components/Break.astro +45 -0
  155. package/src/components/Button.astro +71 -0
  156. package/src/components/Buttons.astro +49 -0
  157. package/src/components/Code.astro +59 -0
  158. package/src/components/Columns.astro +59 -0
  159. package/src/components/CommentForm.astro +315 -0
  160. package/src/components/Comments.astro +232 -0
  161. package/src/components/Cover.astro +128 -0
  162. package/src/components/DinewayBodyEnd.astro +32 -0
  163. package/src/components/DinewayBodyStart.astro +32 -0
  164. package/src/components/DinewayHead.astro +53 -0
  165. package/src/components/DinewayImage.astro +178 -0
  166. package/src/components/DinewayMedia.astro +167 -0
  167. package/src/components/Embed.astro +128 -0
  168. package/src/components/File.astro +122 -0
  169. package/src/components/Gallery.astro +93 -0
  170. package/src/components/HtmlBlock.astro +33 -0
  171. package/src/components/Image.astro +178 -0
  172. package/src/components/InlineEditor.astro +27 -0
  173. package/src/components/InlinePortableTextEditor.tsx +1937 -0
  174. package/src/components/LiveSearch.astro +614 -0
  175. package/src/components/PortableText.astro +51 -0
  176. package/src/components/Pullquote.astro +51 -0
  177. package/src/components/Table.astro +108 -0
  178. package/src/components/WidgetArea.astro +22 -0
  179. package/src/components/WidgetRenderer.astro +72 -0
  180. package/src/components/index.ts +116 -0
  181. package/src/components/marks/Link.astro +31 -0
  182. package/src/components/marks/StrikeThrough.astro +7 -0
  183. package/src/components/marks/Subscript.astro +7 -0
  184. package/src/components/marks/Superscript.astro +7 -0
  185. package/src/components/marks/Underline.astro +7 -0
  186. package/src/components/widgets/Archives.astro +65 -0
  187. package/src/components/widgets/Categories.astro +35 -0
  188. package/src/components/widgets/RecentPosts.astro +51 -0
  189. package/src/components/widgets/Search.astro +18 -0
  190. package/src/components/widgets/Tags.astro +38 -0
  191. package/src/ui.ts +75 -0
@@ -0,0 +1,614 @@
1
+ ---
2
+ /**
3
+ * LiveSearch - A live search component for Dineway content
4
+ *
5
+ * ```astro
6
+ * ---
7
+ * import LiveSearch from "dineway/ui/search";
8
+ * ---
9
+ * <LiveSearch placeholder="Search articles..." collections={["posts", "pages"]} />
10
+ * ```
11
+ *
12
+ * Customize the result rendering with slots:
13
+ *
14
+ * ```astro
15
+ * <LiveSearch>
16
+ * <span slot="no-results">Nothing found</span>
17
+ * <a slot="result" let:result href={`/${result.collection}/${result.slug}`}>
18
+ * {result.title}
19
+ * </a>
20
+ * </LiveSearch>
21
+ * ```
22
+ */
23
+
24
+ export interface Props {
25
+ /** Placeholder text for the search input */
26
+ placeholder?: string;
27
+ /** Collections to search (defaults to all searchable collections) */
28
+ collections?: string[];
29
+ /** Minimum characters before searching (defaults to 2) */
30
+ minChars?: number;
31
+ /** Debounce delay in milliseconds (defaults to 300) */
32
+ debounce?: number;
33
+ /** Maximum results to show (defaults to 10) */
34
+ limit?: number;
35
+ /** CSS class for the container */
36
+ class?: string;
37
+ /** CSS class for the input */
38
+ inputClass?: string;
39
+ /** CSS class for the results container */
40
+ resultsClass?: string;
41
+ /** CSS class for each result item */
42
+ resultClass?: string;
43
+ /** Show snippets in results (defaults to true) */
44
+ showSnippets?: boolean;
45
+ /** Autofocus the input */
46
+ autofocus?: boolean;
47
+ /** Use suggestions endpoint for autocomplete (defaults to false for full search) */
48
+ suggestMode?: boolean;
49
+ /** Expand input width on focus (provide collapsed and expanded widths) */
50
+ expandOnFocus?: { collapsed: string; expanded: string };
51
+ }
52
+
53
+ const {
54
+ placeholder = "Search...",
55
+ collections,
56
+ minChars = 2,
57
+ debounce = 300,
58
+ limit = 10,
59
+ class: className = "",
60
+ inputClass = "",
61
+ resultsClass = "",
62
+ resultClass = "",
63
+ showSnippets = true,
64
+ autofocus = false,
65
+ suggestMode = false,
66
+ expandOnFocus,
67
+ } = Astro.props;
68
+
69
+ const config = {
70
+ collections: collections?.join(",") ?? "",
71
+ minChars,
72
+ debounce,
73
+ limit,
74
+ showSnippets,
75
+ suggestMode,
76
+ expandOnFocus: expandOnFocus ?? null,
77
+ };
78
+ ---
79
+
80
+ <dineway-live-search
81
+ class:list={["dineway-live-search", className]}
82
+ data-config={JSON.stringify(config)}
83
+ >
84
+ <input
85
+ type="search"
86
+ placeholder={placeholder}
87
+ class:list={["dineway-live-search-input", inputClass]}
88
+ autocomplete="off"
89
+ autofocus={autofocus}
90
+ />
91
+ <div class:list={["dineway-live-search-results", resultsClass]} hidden>
92
+ <slot name="loading">
93
+ <div class="dineway-live-search-loading">Searching...</div>
94
+ </slot>
95
+ <slot name="no-results">
96
+ <div class="dineway-live-search-no-results">No results found</div>
97
+ </slot>
98
+ <template class="dineway-live-search-result-template">
99
+ <slot name="result">
100
+ <a class:list={["dineway-live-search-result", resultClass]} href="">
101
+ <span class="dineway-live-search-result-title"></span>
102
+ <span class="dineway-live-search-result-collection"></span>
103
+ <span class="dineway-live-search-result-snippet"></span>
104
+ </a>
105
+ </slot>
106
+ </template>
107
+ <div class="dineway-live-search-results-list"></div>
108
+ </div>
109
+ </dineway-live-search>
110
+
111
+ <script>
112
+ // Sanitization patterns for search snippets (allow only <mark> tags from FTS5)
113
+ const SNIPPET_AMP_RE = /&/g;
114
+ const SNIPPET_LT_RE = /</g;
115
+ const SNIPPET_GT_RE = />/g;
116
+ const SNIPPET_MARK_OPEN_RE = /&lt;mark&gt;/g;
117
+ const SNIPPET_MARK_CLOSE_RE = /&lt;\/mark&gt;/g;
118
+
119
+ interface SearchResult {
120
+ collection: string;
121
+ id: string;
122
+ slug: string | null;
123
+ title?: string;
124
+ snippet?: string;
125
+ score: number;
126
+ }
127
+
128
+ interface Suggestion {
129
+ collection: string;
130
+ id: string;
131
+ title: string;
132
+ }
133
+
134
+ interface Config {
135
+ collections: string;
136
+ minChars: number;
137
+ debounce: number;
138
+ limit: number;
139
+ showSnippets: boolean;
140
+ suggestMode: boolean;
141
+ expandOnFocus: { collapsed: string; expanded: string } | null;
142
+ }
143
+
144
+ class DinewayLiveSearch extends HTMLElement {
145
+ private input: HTMLInputElement | null = null;
146
+ private resultsContainer: HTMLElement | null = null;
147
+ private resultsList: HTMLElement | null = null;
148
+ private loadingEl: HTMLElement | null = null;
149
+ private noResultsEl: HTMLElement | null = null;
150
+ private template: HTMLTemplateElement | null = null;
151
+ private config: Config = {
152
+ collections: "",
153
+ minChars: 2,
154
+ debounce: 300,
155
+ limit: 10,
156
+ showSnippets: true,
157
+ suggestMode: false,
158
+ expandOnFocus: null,
159
+ };
160
+ private debounceTimer: ReturnType<typeof setTimeout> | null = null;
161
+ private abortController: AbortController | null = null;
162
+
163
+ connectedCallback() {
164
+ // Parse config
165
+ const configStr = this.dataset.config;
166
+ if (configStr) {
167
+ try {
168
+ this.config = { ...this.config, ...JSON.parse(configStr) };
169
+ } catch {
170
+ // Use defaults
171
+ }
172
+ }
173
+
174
+ // Get elements
175
+ this.input = this.querySelector(".dineway-live-search-input");
176
+ this.resultsContainer = this.querySelector(
177
+ ".dineway-live-search-results"
178
+ );
179
+ this.resultsList = this.querySelector(
180
+ ".dineway-live-search-results-list"
181
+ );
182
+ this.loadingEl = this.querySelector(".dineway-live-search-loading");
183
+ this.noResultsEl = this.querySelector(".dineway-live-search-no-results");
184
+ this.template = this.querySelector(
185
+ ".dineway-live-search-result-template"
186
+ );
187
+
188
+ if (!this.input) return;
189
+
190
+ // Event listeners
191
+ this.input.addEventListener("input", this.handleInput.bind(this));
192
+ this.input.addEventListener("keydown", this.handleKeydown.bind(this));
193
+
194
+ // Close on click outside
195
+ document.addEventListener("click", (e) => {
196
+ if (!this.contains(e.target as Node)) {
197
+ this.hideResults();
198
+ }
199
+ });
200
+
201
+ // Show results on focus if we have a query
202
+ this.input.addEventListener("focus", () => {
203
+ if (this.input && this.input.value.length >= this.config.minChars) {
204
+ this.showResults();
205
+ }
206
+ // Handle expand on focus
207
+ if (this.config.expandOnFocus && this.input) {
208
+ this.input.style.width = this.config.expandOnFocus.expanded;
209
+ }
210
+ });
211
+
212
+ // Handle collapse on blur
213
+ this.input.addEventListener("blur", () => {
214
+ // Delay to allow clicking on results
215
+ setTimeout(() => {
216
+ if (
217
+ this.config.expandOnFocus &&
218
+ this.input &&
219
+ document.activeElement !== this.input
220
+ ) {
221
+ // Only collapse if not focused on a result
222
+ const activeInResults = this.resultsContainer?.contains(
223
+ document.activeElement
224
+ );
225
+ if (!activeInResults) {
226
+ this.input.style.width = this.config.expandOnFocus.collapsed;
227
+ }
228
+ }
229
+ }, 150);
230
+ });
231
+
232
+ // Set initial width if expandOnFocus is enabled
233
+ if (this.config.expandOnFocus) {
234
+ this.input.style.width = this.config.expandOnFocus.collapsed;
235
+ // Apply transition after initial width is set so the collapsed
236
+ // width doesn't animate in on page load
237
+ requestAnimationFrame(() => {
238
+ if (this.input) {
239
+ this.input.style.transition = "width 0.2s ease";
240
+ }
241
+ });
242
+ }
243
+ }
244
+
245
+ private handleInput() {
246
+ if (!this.input) return;
247
+
248
+ const query = this.input.value.trim();
249
+
250
+ // Clear any pending request
251
+ if (this.debounceTimer) {
252
+ clearTimeout(this.debounceTimer);
253
+ }
254
+
255
+ if (query.length < this.config.minChars) {
256
+ this.hideResults();
257
+ return;
258
+ }
259
+
260
+ // Debounce the search
261
+ this.debounceTimer = setTimeout(() => {
262
+ this.search(query);
263
+ }, this.config.debounce);
264
+ }
265
+
266
+ private handleKeydown(e: KeyboardEvent) {
267
+ if (e.key === "Escape") {
268
+ this.hideResults();
269
+ this.input?.blur();
270
+ } else if (e.key === "ArrowDown" || e.key === "ArrowUp") {
271
+ e.preventDefault();
272
+ this.navigateResults(e.key === "ArrowDown" ? 1 : -1);
273
+ } else if (e.key === "Enter") {
274
+ const focused = this.resultsList?.querySelector(
275
+ ".dineway-live-search-result:focus, .dineway-live-search-result.focused"
276
+ ) as HTMLAnchorElement | null;
277
+ if (focused?.href) {
278
+ window.location.href = focused.href;
279
+ }
280
+ }
281
+ }
282
+
283
+ private navigateResults(direction: number) {
284
+ if (!this.resultsList) return;
285
+
286
+ const results = [
287
+ ...this.resultsList.querySelectorAll(".dineway-live-search-result"),
288
+ ] as HTMLElement[];
289
+ if (results.length === 0) return;
290
+
291
+ const currentIndex = results.findIndex(
292
+ (r) => r === document.activeElement || r.classList.contains("focused")
293
+ );
294
+
295
+ // Remove current focus
296
+ results.forEach((r) => r.classList.remove("focused"));
297
+
298
+ let nextIndex: number;
299
+ if (currentIndex === -1) {
300
+ nextIndex = direction > 0 ? 0 : results.length - 1;
301
+ } else {
302
+ nextIndex = currentIndex + direction;
303
+ if (nextIndex < 0) nextIndex = results.length - 1;
304
+ if (nextIndex >= results.length) nextIndex = 0;
305
+ }
306
+
307
+ const nextResult = results[nextIndex];
308
+ if (nextResult) {
309
+ nextResult.classList.add("focused");
310
+ nextResult.focus();
311
+ }
312
+ }
313
+
314
+ private async search(query: string) {
315
+ // Cancel any in-flight request
316
+ if (this.abortController) {
317
+ this.abortController.abort();
318
+ }
319
+ this.abortController = new AbortController();
320
+
321
+ this.showLoading();
322
+
323
+ try {
324
+ const endpoint = this.config.suggestMode
325
+ ? "/_dineway/api/search/suggest"
326
+ : "/_dineway/api/search";
327
+
328
+ const params = new URLSearchParams({
329
+ q: query,
330
+ limit: String(this.config.limit),
331
+ });
332
+
333
+ if (this.config.collections) {
334
+ params.set("collections", this.config.collections);
335
+ }
336
+
337
+ const response = await fetch(`${endpoint}?${params}`, {
338
+ signal: this.abortController.signal,
339
+ });
340
+
341
+ if (!response.ok) {
342
+ throw new Error("Search failed");
343
+ }
344
+
345
+ const responseData = await response.json();
346
+ // Handle API response envelope: { data: { items: [...] } }
347
+ const data = responseData.data || responseData;
348
+
349
+ if (this.config.suggestMode) {
350
+ this.renderSuggestions(data.items || data.suggestions || []);
351
+ } else {
352
+ this.renderResults(data.items || data.results || []);
353
+ }
354
+ } catch (error) {
355
+ if ((error as Error).name === "AbortError") {
356
+ // Request was cancelled, ignore
357
+ return;
358
+ }
359
+ console.error("Search error:", error);
360
+ this.showNoResults();
361
+ }
362
+ }
363
+
364
+ private renderResults(results: SearchResult[]) {
365
+ if (!this.resultsList || !this.template) return;
366
+
367
+ this.resultsList.innerHTML = "";
368
+
369
+ if (results.length === 0) {
370
+ this.showNoResults();
371
+ return;
372
+ }
373
+
374
+ for (const result of results) {
375
+ const clone = this.template.content.cloneNode(true) as DocumentFragment;
376
+ const link = clone.querySelector("a");
377
+
378
+ if (link) {
379
+ // Build URL - use slug if available, otherwise id
380
+ const path = result.slug ?? result.id;
381
+ link.href = `/${result.collection}/${path}`;
382
+
383
+ // Fill in title
384
+ const titleEl = link.querySelector(
385
+ ".dineway-live-search-result-title"
386
+ );
387
+ if (titleEl) {
388
+ titleEl.textContent = result.title ?? result.slug ?? result.id;
389
+ }
390
+
391
+ // Fill in collection
392
+ const collectionEl = link.querySelector(
393
+ ".dineway-live-search-result-collection"
394
+ );
395
+ if (collectionEl) {
396
+ collectionEl.textContent = result.collection;
397
+ }
398
+
399
+ // Fill in snippet (sanitize to allow only <mark> tags from FTS5 highlighting)
400
+ const snippetEl = link.querySelector(
401
+ ".dineway-live-search-result-snippet"
402
+ );
403
+ if (snippetEl && this.config.showSnippets && result.snippet) {
404
+ snippetEl.innerHTML = result.snippet
405
+ .replace(SNIPPET_AMP_RE, "&amp;")
406
+ .replace(SNIPPET_LT_RE, "&lt;")
407
+ .replace(SNIPPET_GT_RE, "&gt;")
408
+ .replace(SNIPPET_MARK_OPEN_RE, "<mark>")
409
+ .replace(SNIPPET_MARK_CLOSE_RE, "</mark>");
410
+ } else if (snippetEl) {
411
+ snippetEl.remove();
412
+ }
413
+
414
+ // Store data on element for custom handling
415
+ link.dataset.id = result.id;
416
+ link.dataset.collection = result.collection;
417
+ link.dataset.slug = result.slug ?? "";
418
+ link.dataset.score = String(result.score);
419
+ }
420
+
421
+ this.resultsList.appendChild(clone);
422
+ }
423
+
424
+ this.showResultsList();
425
+ }
426
+
427
+ private renderSuggestions(suggestions: Suggestion[]) {
428
+ if (!this.resultsList || !this.template) return;
429
+
430
+ this.resultsList.innerHTML = "";
431
+
432
+ if (suggestions.length === 0) {
433
+ this.showNoResults();
434
+ return;
435
+ }
436
+
437
+ for (const suggestion of suggestions) {
438
+ const clone = this.template.content.cloneNode(true) as DocumentFragment;
439
+ const link = clone.querySelector("a");
440
+
441
+ if (link) {
442
+ link.href = `/${suggestion.collection}/${suggestion.id}`;
443
+
444
+ const titleEl = link.querySelector(
445
+ ".dineway-live-search-result-title"
446
+ );
447
+ if (titleEl) {
448
+ titleEl.textContent = suggestion.title;
449
+ }
450
+
451
+ const collectionEl = link.querySelector(
452
+ ".dineway-live-search-result-collection"
453
+ );
454
+ if (collectionEl) {
455
+ collectionEl.textContent = suggestion.collection;
456
+ }
457
+
458
+ // Remove snippet for suggestions
459
+ const snippetEl = link.querySelector(
460
+ ".dineway-live-search-result-snippet"
461
+ );
462
+ if (snippetEl) {
463
+ snippetEl.remove();
464
+ }
465
+
466
+ link.dataset.id = suggestion.id;
467
+ link.dataset.collection = suggestion.collection;
468
+ }
469
+
470
+ this.resultsList.appendChild(clone);
471
+ }
472
+
473
+ this.showResultsList();
474
+ }
475
+
476
+ private showResults() {
477
+ if (this.resultsContainer) {
478
+ this.resultsContainer.hidden = false;
479
+ }
480
+ }
481
+
482
+ private hideResults() {
483
+ if (this.resultsContainer) {
484
+ this.resultsContainer.hidden = true;
485
+ }
486
+ }
487
+
488
+ private showLoading() {
489
+ this.showResults();
490
+ if (this.loadingEl) this.loadingEl.hidden = false;
491
+ if (this.noResultsEl) this.noResultsEl.hidden = true;
492
+ if (this.resultsList) this.resultsList.hidden = true;
493
+ }
494
+
495
+ private showNoResults() {
496
+ this.showResults();
497
+ if (this.loadingEl) this.loadingEl.hidden = true;
498
+ if (this.noResultsEl) this.noResultsEl.hidden = false;
499
+ if (this.resultsList) this.resultsList.hidden = true;
500
+ }
501
+
502
+ private showResultsList() {
503
+ this.showResults();
504
+ if (this.loadingEl) this.loadingEl.hidden = true;
505
+ if (this.noResultsEl) this.noResultsEl.hidden = true;
506
+ if (this.resultsList) this.resultsList.hidden = false;
507
+ }
508
+ }
509
+
510
+ customElements.define("dineway-live-search", DinewayLiveSearch);
511
+ </script>
512
+
513
+ <style>
514
+ /*
515
+ * LiveSearch uses CSS custom properties for theming.
516
+ * Override these in your site's CSS to match your design:
517
+ *
518
+ * --dineway-search-bg: Background color for the results dropdown
519
+ * --dineway-search-text: Text color
520
+ * --dineway-search-muted: Muted/secondary text color
521
+ * --dineway-search-border: Border color
522
+ * --dineway-search-hover: Hover/focus background color
523
+ * --dineway-search-highlight: Highlighted match text color
524
+ */
525
+
526
+ .dineway-live-search {
527
+ position: relative;
528
+ display: inline-block;
529
+ }
530
+
531
+ .dineway-live-search-input {
532
+ width: 100%;
533
+ padding: 0.5rem 1rem;
534
+ font-size: 1rem;
535
+ border: 1px solid var(--dineway-search-border, #ccc);
536
+ border-radius: 0.25rem;
537
+ background: var(--dineway-search-bg, white);
538
+ color: var(--dineway-search-text, inherit);
539
+ }
540
+
541
+ .dineway-live-search-input:focus {
542
+ outline: none;
543
+ border-color: var(--dineway-search-border-focus, #666);
544
+ }
545
+
546
+ .dineway-live-search-results {
547
+ position: absolute;
548
+ top: 100%;
549
+ left: 0;
550
+ right: 0;
551
+ margin-top: 0.25rem;
552
+ background: var(--dineway-search-bg, white);
553
+ border: 1px solid var(--dineway-search-border, #ccc);
554
+ border-radius: 0.25rem;
555
+ box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
556
+ max-height: 400px;
557
+ overflow-y: auto;
558
+ z-index: 1000;
559
+ }
560
+
561
+ .dineway-live-search-loading,
562
+ .dineway-live-search-no-results {
563
+ padding: 1rem;
564
+ text-align: center;
565
+ color: var(--dineway-search-muted, #666);
566
+ }
567
+
568
+ .dineway-live-search-result {
569
+ display: block;
570
+ padding: 0.75rem 1rem;
571
+ text-decoration: none;
572
+ color: var(--dineway-search-text, inherit);
573
+ border-bottom: 1px solid var(--dineway-search-border, #eee);
574
+ }
575
+
576
+ .dineway-live-search-result:last-child {
577
+ border-bottom: none;
578
+ }
579
+
580
+ .dineway-live-search-result:hover,
581
+ .dineway-live-search-result:focus,
582
+ .dineway-live-search-result.focused {
583
+ background: var(--dineway-search-hover, #f5f5f5);
584
+ outline: none;
585
+ }
586
+
587
+ .dineway-live-search-result-title {
588
+ display: block;
589
+ font-weight: 500;
590
+ }
591
+
592
+ .dineway-live-search-result-collection {
593
+ display: block;
594
+ font-size: 0.75rem;
595
+ color: var(--dineway-search-muted, #888);
596
+ text-transform: uppercase;
597
+ letter-spacing: 0.05em;
598
+ margin-top: 0.125rem;
599
+ }
600
+
601
+ .dineway-live-search-result-snippet {
602
+ display: block;
603
+ font-size: 0.875rem;
604
+ color: var(--dineway-search-muted, #666);
605
+ margin-top: 0.25rem;
606
+ }
607
+
608
+ /* Highlight matches in snippets (FTS5 uses <mark> tags) */
609
+ .dineway-live-search-result-snippet :global(mark) {
610
+ font-weight: 600;
611
+ background: none;
612
+ color: var(--dineway-search-highlight, var(--dineway-search-text, #000));
613
+ }
614
+ </style>
@@ -0,0 +1,51 @@
1
+ ---
2
+ /**
3
+ * Dineway Portable Text wrapper
4
+ *
5
+ * Pre-configured with Dineway's built-in components for images, code blocks,
6
+ * embeds, galleries, tables, etc.
7
+ *
8
+ * In edit mode, renders a TipTap-based inline editor instead of static HTML.
9
+ * The edit metadata is detected automatically from the value — no extra props needed.
10
+ *
11
+ * Plugin block rendering components are auto-merged from plugins that declare
12
+ * a `componentsEntry`. Plugin components are merged between Dineway defaults
13
+ * and user overrides, so users can still override individual block types.
14
+ */
15
+ import {
16
+ PortableText as BasePortableText,
17
+ mergeComponents,
18
+ type PortableTextProps,
19
+ } from "astro-portabletext";
20
+ import { getEditMeta } from "../query.js";
21
+ // @ts-ignore - virtual module
22
+ import { pluginBlockComponents } from "virtual:dineway/block-components";
23
+ import { dinewayComponents } from "./index.js";
24
+ import InlineEditor from "./InlineEditor.astro";
25
+
26
+ export interface Props extends Omit<PortableTextProps, "value"> {
27
+ value: PortableTextProps["value"];
28
+ }
29
+
30
+ const { value, components: userComponents, ...rest } = Astro.props;
31
+
32
+ // Check for edit metadata (attached as non-enumerable property by query functions)
33
+ const editMeta = getEditMeta(value);
34
+
35
+ // Merge: Dineway defaults < Plugin block components < User components
36
+ const withPlugins = mergeComponents(dinewayComponents, { type: pluginBlockComponents });
37
+ const mergedComponents = userComponents
38
+ ? mergeComponents(withPlugins, userComponents)
39
+ : withPlugins;
40
+ ---
41
+
42
+ {editMeta ? (
43
+ <InlineEditor
44
+ value={value}
45
+ collection={editMeta.collection}
46
+ entryId={editMeta.id}
47
+ field={editMeta.field}
48
+ />
49
+ ) : (
50
+ <BasePortableText value={value} components={mergedComponents} {...rest} />
51
+ )}
@@ -0,0 +1,51 @@
1
+ ---
2
+ /**
3
+ * Portable Text Pullquote block component
4
+ *
5
+ * Renders a styled pullquote from WordPress imports.
6
+ * Pullquotes are designed to stand out more than regular blockquotes.
7
+ */
8
+ export interface Props {
9
+ node: {
10
+ _type: "pullquote";
11
+ _key: string;
12
+ text: string;
13
+ citation?: string;
14
+ };
15
+ }
16
+
17
+ const { node } = Astro.props;
18
+ const { text, citation } = node ?? {};
19
+ ---
20
+
21
+ <figure class="dineway-pullquote">
22
+ <blockquote class="dineway-pullquote__text">
23
+ {text}
24
+ </blockquote>
25
+ {citation && <figcaption class="dineway-pullquote__citation">&mdash; {citation}</figcaption>}
26
+ </figure>
27
+
28
+ <style>
29
+ .dineway-pullquote {
30
+ margin: 2rem 0;
31
+ padding: 1.5rem 2rem;
32
+ border-top: 4px solid var(--dineway-pullquote-border, #0073aa);
33
+ border-bottom: 4px solid var(--dineway-pullquote-border, #0073aa);
34
+ text-align: center;
35
+ }
36
+
37
+ .dineway-pullquote__text {
38
+ font-size: 1.5rem;
39
+ font-style: italic;
40
+ line-height: 1.4;
41
+ color: var(--dineway-pullquote-color, #333);
42
+ margin: 0;
43
+ }
44
+
45
+ .dineway-pullquote__citation {
46
+ margin-top: 1rem;
47
+ font-size: 0.9rem;
48
+ font-style: normal;
49
+ color: var(--dineway-pullquote-citation-color, #666);
50
+ }
51
+ </style>