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.
- package/package.json +6 -3
- package/src/astro/routes/PluginRegistry.tsx +21 -0
- package/src/astro/routes/admin.astro +83 -0
- package/src/astro/routes/api/admin/allowed-domains/[domain].ts +112 -0
- package/src/astro/routes/api/admin/allowed-domains/index.ts +108 -0
- package/src/astro/routes/api/admin/api-tokens/[id].ts +40 -0
- package/src/astro/routes/api/admin/api-tokens/index.ts +68 -0
- package/src/astro/routes/api/admin/bylines/[id]/index.ts +87 -0
- package/src/astro/routes/api/admin/bylines/index.ts +72 -0
- package/src/astro/routes/api/admin/comments/[id]/status.ts +120 -0
- package/src/astro/routes/api/admin/comments/[id].ts +64 -0
- package/src/astro/routes/api/admin/comments/bulk.ts +42 -0
- package/src/astro/routes/api/admin/comments/counts.ts +30 -0
- package/src/astro/routes/api/admin/comments/index.ts +46 -0
- package/src/astro/routes/api/admin/hooks/exclusive/[hookName].ts +91 -0
- package/src/astro/routes/api/admin/hooks/exclusive/index.ts +51 -0
- package/src/astro/routes/api/admin/oauth-clients/[id].ts +110 -0
- package/src/astro/routes/api/admin/oauth-clients/index.ts +71 -0
- package/src/astro/routes/api/admin/plugins/[id]/disable.ts +39 -0
- package/src/astro/routes/api/admin/plugins/[id]/enable.ts +39 -0
- package/src/astro/routes/api/admin/plugins/[id]/index.ts +38 -0
- package/src/astro/routes/api/admin/plugins/[id]/uninstall.ts +48 -0
- package/src/astro/routes/api/admin/plugins/[id]/update.ts +59 -0
- package/src/astro/routes/api/admin/plugins/index.ts +32 -0
- package/src/astro/routes/api/admin/plugins/marketplace/[id]/icon.ts +62 -0
- package/src/astro/routes/api/admin/plugins/marketplace/[id]/index.ts +33 -0
- package/src/astro/routes/api/admin/plugins/marketplace/[id]/install.ts +64 -0
- package/src/astro/routes/api/admin/plugins/marketplace/index.ts +38 -0
- package/src/astro/routes/api/admin/plugins/updates.ts +28 -0
- package/src/astro/routes/api/admin/themes/marketplace/[id]/index.ts +33 -0
- package/src/astro/routes/api/admin/themes/marketplace/[id]/thumbnail.ts +62 -0
- package/src/astro/routes/api/admin/themes/marketplace/index.ts +45 -0
- package/src/astro/routes/api/admin/users/[id]/disable.ts +69 -0
- package/src/astro/routes/api/admin/users/[id]/enable.ts +48 -0
- package/src/astro/routes/api/admin/users/[id]/index.ts +146 -0
- package/src/astro/routes/api/admin/users/[id]/send-recovery.ts +72 -0
- package/src/astro/routes/api/admin/users/index.ts +66 -0
- package/src/astro/routes/api/auth/dev-bypass.ts +139 -0
- package/src/astro/routes/api/auth/invite/accept.ts +52 -0
- package/src/astro/routes/api/auth/invite/complete.ts +86 -0
- package/src/astro/routes/api/auth/invite/index.ts +99 -0
- package/src/astro/routes/api/auth/logout.ts +40 -0
- package/src/astro/routes/api/auth/magic-link/send.ts +89 -0
- package/src/astro/routes/api/auth/magic-link/verify.ts +71 -0
- package/src/astro/routes/api/auth/me.ts +60 -0
- package/src/astro/routes/api/auth/oauth/[provider]/callback.ts +221 -0
- package/src/astro/routes/api/auth/oauth/[provider].ts +120 -0
- package/src/astro/routes/api/auth/passkey/[id].ts +124 -0
- package/src/astro/routes/api/auth/passkey/index.ts +54 -0
- package/src/astro/routes/api/auth/passkey/options.ts +84 -0
- package/src/astro/routes/api/auth/passkey/register/options.ts +88 -0
- package/src/astro/routes/api/auth/passkey/register/verify.ts +119 -0
- package/src/astro/routes/api/auth/passkey/verify.ts +68 -0
- package/src/astro/routes/api/auth/signup/complete.ts +87 -0
- package/src/astro/routes/api/auth/signup/request.ts +77 -0
- package/src/astro/routes/api/auth/signup/verify.ts +53 -0
- package/src/astro/routes/api/comments/[collection]/[contentId]/index.ts +311 -0
- package/src/astro/routes/api/content/[collection]/[id]/compare.ts +28 -0
- package/src/astro/routes/api/content/[collection]/[id]/discard-draft.ts +54 -0
- package/src/astro/routes/api/content/[collection]/[id]/duplicate.ts +61 -0
- package/src/astro/routes/api/content/[collection]/[id]/permanent.ts +33 -0
- package/src/astro/routes/api/content/[collection]/[id]/preview-url.ts +107 -0
- package/src/astro/routes/api/content/[collection]/[id]/publish.ts +56 -0
- package/src/astro/routes/api/content/[collection]/[id]/restore.ts +54 -0
- package/src/astro/routes/api/content/[collection]/[id]/revisions.ts +31 -0
- package/src/astro/routes/api/content/[collection]/[id]/schedule.ts +105 -0
- package/src/astro/routes/api/content/[collection]/[id]/terms/[taxonomy].ts +140 -0
- package/src/astro/routes/api/content/[collection]/[id]/translations.ts +30 -0
- package/src/astro/routes/api/content/[collection]/[id]/unpublish.ts +56 -0
- package/src/astro/routes/api/content/[collection]/[id].ts +137 -0
- package/src/astro/routes/api/content/[collection]/index.ts +59 -0
- package/src/astro/routes/api/content/[collection]/trash.ts +33 -0
- package/src/astro/routes/api/dashboard.ts +32 -0
- package/src/astro/routes/api/dev/emails.ts +36 -0
- package/src/astro/routes/api/import/probe.ts +47 -0
- package/src/astro/routes/api/import/wordpress/analyze.ts +531 -0
- package/src/astro/routes/api/import/wordpress/execute.ts +296 -0
- package/src/astro/routes/api/import/wordpress/media.ts +338 -0
- package/src/astro/routes/api/import/wordpress/prepare.ts +181 -0
- package/src/astro/routes/api/import/wordpress/rewrite-urls.ts +393 -0
- package/src/astro/routes/api/import/wordpress-plugin/analyze.ts +111 -0
- package/src/astro/routes/api/import/wordpress-plugin/callback.ts +58 -0
- package/src/astro/routes/api/import/wordpress-plugin/execute.ts +357 -0
- package/src/astro/routes/api/manifest.ts +63 -0
- package/src/astro/routes/api/mcp.ts +124 -0
- package/src/astro/routes/api/media/[id]/confirm.ts +93 -0
- package/src/astro/routes/api/media/[id].ts +145 -0
- package/src/astro/routes/api/media/file/[...key].ts +79 -0
- package/src/astro/routes/api/media/providers/[providerId]/[itemId].ts +86 -0
- package/src/astro/routes/api/media/providers/[providerId]/index.ts +111 -0
- package/src/astro/routes/api/media/providers/index.ts +30 -0
- package/src/astro/routes/api/media/upload-url.ts +137 -0
- package/src/astro/routes/api/media.ts +202 -0
- package/src/astro/routes/api/menus/[name]/items.ts +87 -0
- package/src/astro/routes/api/menus/[name]/reorder.ts +33 -0
- package/src/astro/routes/api/menus/[name].ts +65 -0
- package/src/astro/routes/api/menus/index.ts +47 -0
- package/src/astro/routes/api/oauth/authorize.ts +417 -0
- package/src/astro/routes/api/oauth/device/authorize.ts +45 -0
- package/src/astro/routes/api/oauth/device/code.ts +55 -0
- package/src/astro/routes/api/oauth/device/token.ts +69 -0
- package/src/astro/routes/api/oauth/token/refresh.ts +38 -0
- package/src/astro/routes/api/oauth/token/revoke.ts +38 -0
- package/src/astro/routes/api/oauth/token.ts +184 -0
- package/src/astro/routes/api/openapi.json.ts +32 -0
- package/src/astro/routes/api/plugins/[pluginId]/[...path].ts +92 -0
- package/src/astro/routes/api/redirects/404s/index.ts +72 -0
- package/src/astro/routes/api/redirects/404s/summary.ts +33 -0
- package/src/astro/routes/api/redirects/[id].ts +84 -0
- package/src/astro/routes/api/redirects/index.ts +52 -0
- package/src/astro/routes/api/revisions/[revisionId]/index.ts +29 -0
- package/src/astro/routes/api/revisions/[revisionId]/restore.ts +62 -0
- package/src/astro/routes/api/schema/collections/[slug]/fields/[fieldSlug].ts +76 -0
- package/src/astro/routes/api/schema/collections/[slug]/fields/index.ts +52 -0
- package/src/astro/routes/api/schema/collections/[slug]/fields/reorder.ts +32 -0
- package/src/astro/routes/api/schema/collections/[slug]/index.ts +80 -0
- package/src/astro/routes/api/schema/collections/index.ts +47 -0
- package/src/astro/routes/api/schema/index.ts +109 -0
- package/src/astro/routes/api/schema/orphans/[slug].ts +36 -0
- package/src/astro/routes/api/schema/orphans/index.ts +26 -0
- package/src/astro/routes/api/search/enable.ts +64 -0
- package/src/astro/routes/api/search/index.ts +51 -0
- package/src/astro/routes/api/search/rebuild.ts +72 -0
- package/src/astro/routes/api/search/stats.ts +35 -0
- package/src/astro/routes/api/search/suggest.ts +49 -0
- package/src/astro/routes/api/sections/[slug].ts +84 -0
- package/src/astro/routes/api/sections/index.ts +52 -0
- package/src/astro/routes/api/settings/email.ts +150 -0
- package/src/astro/routes/api/settings.ts +67 -0
- package/src/astro/routes/api/setup/admin-verify.ts +102 -0
- package/src/astro/routes/api/setup/admin.ts +96 -0
- package/src/astro/routes/api/setup/dev-bypass.ts +200 -0
- package/src/astro/routes/api/setup/dev-reset.ts +40 -0
- package/src/astro/routes/api/setup/index.ts +127 -0
- package/src/astro/routes/api/setup/status.ts +122 -0
- package/src/astro/routes/api/snapshot.ts +76 -0
- package/src/astro/routes/api/taxonomies/[name]/terms/[slug].ts +95 -0
- package/src/astro/routes/api/taxonomies/[name]/terms/index.ts +69 -0
- package/src/astro/routes/api/taxonomies/index.ts +59 -0
- package/src/astro/routes/api/themes/preview.ts +78 -0
- package/src/astro/routes/api/typegen.ts +114 -0
- package/src/astro/routes/api/well-known/auth.ts +69 -0
- package/src/astro/routes/api/well-known/oauth-authorization-server.ts +45 -0
- package/src/astro/routes/api/well-known/oauth-protected-resource.ts +38 -0
- package/src/astro/routes/api/widget-areas/[name]/reorder.ts +72 -0
- package/src/astro/routes/api/widget-areas/[name]/widgets/[id].ts +127 -0
- package/src/astro/routes/api/widget-areas/[name]/widgets.ts +80 -0
- package/src/astro/routes/api/widget-areas/[name].ts +87 -0
- package/src/astro/routes/api/widget-areas/index.ts +99 -0
- package/src/astro/routes/api/widget-components.ts +22 -0
- package/src/astro/routes/robots.txt.ts +81 -0
- package/src/astro/routes/sitemap-[collection].xml.ts +104 -0
- package/src/astro/routes/sitemap.xml.ts +92 -0
- package/src/components/Break.astro +45 -0
- package/src/components/Button.astro +71 -0
- package/src/components/Buttons.astro +49 -0
- package/src/components/Code.astro +59 -0
- package/src/components/Columns.astro +59 -0
- package/src/components/CommentForm.astro +315 -0
- package/src/components/Comments.astro +232 -0
- package/src/components/Cover.astro +128 -0
- package/src/components/DinewayBodyEnd.astro +32 -0
- package/src/components/DinewayBodyStart.astro +32 -0
- package/src/components/DinewayHead.astro +53 -0
- package/src/components/DinewayImage.astro +178 -0
- package/src/components/DinewayMedia.astro +167 -0
- package/src/components/Embed.astro +128 -0
- package/src/components/File.astro +122 -0
- package/src/components/Gallery.astro +93 -0
- package/src/components/HtmlBlock.astro +33 -0
- package/src/components/Image.astro +178 -0
- package/src/components/InlineEditor.astro +27 -0
- package/src/components/InlinePortableTextEditor.tsx +1937 -0
- package/src/components/LiveSearch.astro +614 -0
- package/src/components/PortableText.astro +51 -0
- package/src/components/Pullquote.astro +51 -0
- package/src/components/Table.astro +108 -0
- package/src/components/WidgetArea.astro +22 -0
- package/src/components/WidgetRenderer.astro +72 -0
- package/src/components/index.ts +116 -0
- package/src/components/marks/Link.astro +31 -0
- package/src/components/marks/StrikeThrough.astro +7 -0
- package/src/components/marks/Subscript.astro +7 -0
- package/src/components/marks/Superscript.astro +7 -0
- package/src/components/marks/Underline.astro +7 -0
- package/src/components/widgets/Archives.astro +65 -0
- package/src/components/widgets/Categories.astro +35 -0
- package/src/components/widgets/RecentPosts.astro +51 -0
- package/src/components/widgets/Search.astro +18 -0
- package/src/components/widgets/Tags.astro +38 -0
- 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 = /<mark>/g;
|
|
117
|
+
const SNIPPET_MARK_CLOSE_RE = /<\/mark>/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, "&")
|
|
406
|
+
.replace(SNIPPET_LT_RE, "<")
|
|
407
|
+
.replace(SNIPPET_GT_RE, ">")
|
|
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">— {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>
|