@ternkit/tern 1.13.0 → 1.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +3 -2
- package/dist/index.js +2 -2
- package/dist/runtime/api.d.ts +12 -1
- package/dist/runtime/api.js +130 -31
- package/dist/runtime/builtin.js +34 -0
- package/dist/runtime/dev.d.ts +35 -0
- package/dist/runtime/dev.js +32 -0
- package/dist/runtime/forms.js +3 -0
- package/dist/runtime/router.svelte.js +3 -0
- package/dist/runtime/types.d.ts +6 -0
- package/dist/ui/Admin.svelte +485 -0
- package/dist/ui/Admin.svelte.d.ts +11 -0
- package/dist/ui/Highlight.svelte +23 -0
- package/dist/ui/Highlight.svelte.d.ts +9 -0
- package/dist/ui/SearchBox.svelte +344 -0
- package/dist/ui/SearchBox.svelte.d.ts +25 -0
- package/dist/ui/admin.d.ts +67 -0
- package/dist/ui/admin.js +32 -0
- package/dist/ui/index.d.ts +5 -0
- package/dist/ui/index.js +3 -0
- package/dist/ui/search.d.ts +16 -0
- package/dist/ui/search.js +1 -0
- package/dist/vite/goscript.js +3 -1
- package/package.json +1 -1
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { tb } from '../runtime/i18n.svelte.js';
|
|
3
|
+
import { goto } from '../runtime/router.svelte.js';
|
|
4
|
+
import Highlight from './Highlight.svelte';
|
|
5
|
+
import Spinner from './Spinner.svelte';
|
|
6
|
+
import type { SearchItem } from './search.js';
|
|
7
|
+
|
|
8
|
+
type Props = {
|
|
9
|
+
/** The results page the form goes to on Enter, such as "/search"; without JavaScript, the only way. */
|
|
10
|
+
action?: string;
|
|
11
|
+
/** The query's name in the results page's address. */
|
|
12
|
+
name?: string;
|
|
13
|
+
/** The query; bind it. */
|
|
14
|
+
value?: string;
|
|
15
|
+
/** Results as the user types, such as from a typed endpoint. Without it, the box is a plain search form. */
|
|
16
|
+
search?: (query: string, signal: AbortSignal) => Promise<SearchItem[]>;
|
|
17
|
+
/** Milliseconds to wait after typing before searching. */
|
|
18
|
+
debounce?: number;
|
|
19
|
+
/** The fewest characters to search for. */
|
|
20
|
+
minLength?: number;
|
|
21
|
+
/** A key that focuses the box from anywhere on the page, such as "/". */
|
|
22
|
+
hotkey?: string;
|
|
23
|
+
label?: string;
|
|
24
|
+
placeholder?: string;
|
|
25
|
+
/** Runs when a result is chosen; return false to stay on the page. */
|
|
26
|
+
onselect?: (item: SearchItem) => void | boolean;
|
|
27
|
+
class?: string;
|
|
28
|
+
};
|
|
29
|
+
let {
|
|
30
|
+
action,
|
|
31
|
+
name = 'q',
|
|
32
|
+
value = $bindable(''),
|
|
33
|
+
search,
|
|
34
|
+
debounce = 150,
|
|
35
|
+
minLength = 2,
|
|
36
|
+
hotkey,
|
|
37
|
+
label: labelProp,
|
|
38
|
+
placeholder: placeholderProp,
|
|
39
|
+
onselect,
|
|
40
|
+
class: klass = ''
|
|
41
|
+
}: Props = $props();
|
|
42
|
+
const label = $derived(labelProp ?? tb('tern.ui.search.label'));
|
|
43
|
+
const placeholder = $derived(placeholderProp ?? tb('tern.ui.search.placeholder'));
|
|
44
|
+
|
|
45
|
+
const uid = $props.id();
|
|
46
|
+
const id = `tern-search-${uid}`;
|
|
47
|
+
let input = $state<HTMLInputElement>();
|
|
48
|
+
let form = $state<HTMLFormElement>();
|
|
49
|
+
let items = $state<SearchItem[]>([]);
|
|
50
|
+
let searched = $state('');
|
|
51
|
+
let active = $state(-1);
|
|
52
|
+
let open = $state(false);
|
|
53
|
+
let searching = $state(false);
|
|
54
|
+
let failed = $state(false);
|
|
55
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
56
|
+
let controller: AbortController | undefined;
|
|
57
|
+
|
|
58
|
+
const query = $derived(value.trim());
|
|
59
|
+
const showAll = $derived(!!action && items.length > 0);
|
|
60
|
+
const count = $derived(items.length + (showAll ? 1 : 0));
|
|
61
|
+
|
|
62
|
+
function run() {
|
|
63
|
+
clearTimeout(timer);
|
|
64
|
+
controller?.abort();
|
|
65
|
+
if (!search || query.length < minLength) {
|
|
66
|
+
items = [];
|
|
67
|
+
searched = '';
|
|
68
|
+
searching = false;
|
|
69
|
+
open = false;
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
const q = query;
|
|
73
|
+
timer = setTimeout(async () => {
|
|
74
|
+
const c = (controller = new AbortController());
|
|
75
|
+
searching = true;
|
|
76
|
+
failed = false;
|
|
77
|
+
try {
|
|
78
|
+
const got = await search!(q, c.signal);
|
|
79
|
+
if (c.signal.aborted) return;
|
|
80
|
+
items = got;
|
|
81
|
+
searched = q;
|
|
82
|
+
active = -1;
|
|
83
|
+
open = true;
|
|
84
|
+
} catch (e) {
|
|
85
|
+
if (c.signal.aborted) return;
|
|
86
|
+
items = [];
|
|
87
|
+
searched = q;
|
|
88
|
+
failed = true;
|
|
89
|
+
open = true;
|
|
90
|
+
} finally {
|
|
91
|
+
if (!c.signal.aborted) searching = false;
|
|
92
|
+
}
|
|
93
|
+
}, debounce);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function choose(i: number) {
|
|
97
|
+
if (i === items.length && showAll) {
|
|
98
|
+
form?.requestSubmit();
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
const item = items[i];
|
|
102
|
+
if (!item) return;
|
|
103
|
+
open = false;
|
|
104
|
+
if (onselect?.(item) === false) return;
|
|
105
|
+
void goto(item.href);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function onkeydown(e: KeyboardEvent) {
|
|
109
|
+
if (e.key === 'ArrowDown' || e.key === 'ArrowUp') {
|
|
110
|
+
if (!open && items.length) open = true;
|
|
111
|
+
if (!count) return;
|
|
112
|
+
e.preventDefault();
|
|
113
|
+
const step = e.key === 'ArrowDown' ? 1 : -1;
|
|
114
|
+
active = active < 0 && step < 0 ? count - 1 : (active + step + count) % count;
|
|
115
|
+
} else if (e.key === 'Enter' && open && active >= 0) {
|
|
116
|
+
e.preventDefault();
|
|
117
|
+
choose(active);
|
|
118
|
+
} else if (e.key === 'Escape') {
|
|
119
|
+
if (open) {
|
|
120
|
+
e.preventDefault();
|
|
121
|
+
open = false;
|
|
122
|
+
} else if (value) {
|
|
123
|
+
e.preventDefault();
|
|
124
|
+
value = '';
|
|
125
|
+
run();
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
$effect(() => {
|
|
131
|
+
if (!hotkey) return;
|
|
132
|
+
const onKey = (e: KeyboardEvent) => {
|
|
133
|
+
const t = e.target instanceof Element ? e.target : null;
|
|
134
|
+
if (e.key !== hotkey || e.metaKey || e.ctrlKey || e.altKey || t?.closest('input, textarea, select, [contenteditable]')) return;
|
|
135
|
+
e.preventDefault();
|
|
136
|
+
input?.focus();
|
|
137
|
+
};
|
|
138
|
+
addEventListener('keydown', onKey);
|
|
139
|
+
return () => removeEventListener('keydown', onKey);
|
|
140
|
+
});
|
|
141
|
+
$effect(() => () => {
|
|
142
|
+
clearTimeout(timer);
|
|
143
|
+
controller?.abort();
|
|
144
|
+
});
|
|
145
|
+
</script>
|
|
146
|
+
|
|
147
|
+
<form bind:this={form} class="tern-search {klass}" role="search" method="GET" {action} onsubmit={(e) => !action && e.preventDefault()}>
|
|
148
|
+
<span class="glass" aria-hidden="true">
|
|
149
|
+
{#if searching}<Spinner size="0.875rem" />{:else}<svg viewBox="0 0 16 16" width="14" height="14"><circle cx="7" cy="7" r="4.5" fill="none" stroke="currentColor" stroke-width="1.5" /><path d="M10.5 10.5L14 14" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" /></svg>{/if}
|
|
150
|
+
</span>
|
|
151
|
+
<input
|
|
152
|
+
bind:this={input}
|
|
153
|
+
bind:value
|
|
154
|
+
type="search"
|
|
155
|
+
{name}
|
|
156
|
+
{placeholder}
|
|
157
|
+
aria-label={label}
|
|
158
|
+
autocomplete="off"
|
|
159
|
+
spellcheck="false"
|
|
160
|
+
role={search ? 'combobox' : undefined}
|
|
161
|
+
aria-expanded={search ? open : undefined}
|
|
162
|
+
aria-controls={search ? `${id}-list` : undefined}
|
|
163
|
+
aria-autocomplete={search ? 'list' : undefined}
|
|
164
|
+
aria-activedescendant={open && active >= 0 ? `${id}-opt-${active}` : undefined}
|
|
165
|
+
oninput={run}
|
|
166
|
+
{onkeydown}
|
|
167
|
+
onfocus={() => items.length && (open = true)}
|
|
168
|
+
onblur={() => setTimeout(() => (open = false), 120)}
|
|
169
|
+
/>
|
|
170
|
+
{#if hotkey && !value}<kbd class="hotkey" aria-hidden="true">{hotkey}</kbd>{/if}
|
|
171
|
+
{#if search}
|
|
172
|
+
<div class="tern-sr-only" aria-live="polite">{open && !searching ? (failed ? tb('tern.ui.search.failed') : tb('tern.ui.search.results', { n: items.length })) : ''}</div>
|
|
173
|
+
{#if open}
|
|
174
|
+
<div id="{id}-list" class="list" role="listbox" aria-label={label}>
|
|
175
|
+
{#if failed}
|
|
176
|
+
<p class="note">{tb('tern.ui.search.failed')}</p>
|
|
177
|
+
{:else if items.length === 0}
|
|
178
|
+
<p class="note">{tb('tern.ui.search.empty', { query: searched })}</p>
|
|
179
|
+
{:else}
|
|
180
|
+
{#each items as item, i (item.href)}
|
|
181
|
+
<a
|
|
182
|
+
id="{id}-opt-{i}"
|
|
183
|
+
class="item"
|
|
184
|
+
class:active={i === active}
|
|
185
|
+
role="option"
|
|
186
|
+
aria-selected={i === active}
|
|
187
|
+
href={item.href}
|
|
188
|
+
tabindex="-1"
|
|
189
|
+
onpointerdown={(e) => e.preventDefault()}
|
|
190
|
+
onpointermove={() => (active = i)}
|
|
191
|
+
onclick={(e) => {
|
|
192
|
+
if (e.metaKey || e.ctrlKey || e.shiftKey || e.button !== 0) return;
|
|
193
|
+
e.preventDefault();
|
|
194
|
+
choose(i);
|
|
195
|
+
}}
|
|
196
|
+
>
|
|
197
|
+
<span class="title"><Highlight text={item.title} />{#if item.meta}<span class="meta">{item.meta}</span>{/if}</span>
|
|
198
|
+
{#if item.snippet?.length}<span class="snippet"><Highlight text={item.snippet} /></span>{/if}
|
|
199
|
+
</a>
|
|
200
|
+
{/each}
|
|
201
|
+
{#if showAll}
|
|
202
|
+
<button
|
|
203
|
+
id="{id}-opt-{items.length}"
|
|
204
|
+
type="submit"
|
|
205
|
+
class="item all"
|
|
206
|
+
class:active={active === items.length}
|
|
207
|
+
role="option"
|
|
208
|
+
aria-selected={active === items.length}
|
|
209
|
+
tabindex="-1"
|
|
210
|
+
onpointerdown={(e) => e.preventDefault()}
|
|
211
|
+
onpointermove={() => (active = items.length)}>{tb('tern.ui.search.all', { query: searched })}</button
|
|
212
|
+
>
|
|
213
|
+
{/if}
|
|
214
|
+
{/if}
|
|
215
|
+
</div>
|
|
216
|
+
{/if}
|
|
217
|
+
{/if}
|
|
218
|
+
</form>
|
|
219
|
+
|
|
220
|
+
<style>
|
|
221
|
+
.tern-search {
|
|
222
|
+
position: relative;
|
|
223
|
+
display: flex;
|
|
224
|
+
align-items: center;
|
|
225
|
+
width: 100%;
|
|
226
|
+
max-width: 32rem;
|
|
227
|
+
margin: 0;
|
|
228
|
+
}
|
|
229
|
+
.glass {
|
|
230
|
+
position: absolute;
|
|
231
|
+
left: 0.75rem;
|
|
232
|
+
display: grid;
|
|
233
|
+
place-items: center;
|
|
234
|
+
color: var(--tern-muted);
|
|
235
|
+
pointer-events: none;
|
|
236
|
+
}
|
|
237
|
+
input {
|
|
238
|
+
box-sizing: border-box;
|
|
239
|
+
width: 100%;
|
|
240
|
+
height: var(--tern-control-height);
|
|
241
|
+
padding: 0 2.25rem 0 2.25rem;
|
|
242
|
+
border: 1px solid var(--tern-border);
|
|
243
|
+
border-radius: var(--tern-radius-sm);
|
|
244
|
+
background: var(--tern-bg);
|
|
245
|
+
color: var(--tern-fg);
|
|
246
|
+
font: inherit;
|
|
247
|
+
font-size: 0.875rem;
|
|
248
|
+
}
|
|
249
|
+
input::-webkit-search-cancel-button {
|
|
250
|
+
-webkit-appearance: none;
|
|
251
|
+
}
|
|
252
|
+
input:focus-visible {
|
|
253
|
+
outline: none;
|
|
254
|
+
border-color: var(--tern-accent);
|
|
255
|
+
box-shadow: 0 0 0 3px var(--tern-ring);
|
|
256
|
+
}
|
|
257
|
+
.hotkey {
|
|
258
|
+
position: absolute;
|
|
259
|
+
right: 0.625rem;
|
|
260
|
+
padding: 0 0.375rem;
|
|
261
|
+
border: 1px solid var(--tern-border);
|
|
262
|
+
border-radius: 4px;
|
|
263
|
+
color: var(--tern-muted);
|
|
264
|
+
font: 0.75rem/1.4 var(--tern-font-mono);
|
|
265
|
+
pointer-events: none;
|
|
266
|
+
}
|
|
267
|
+
@media (pointer: coarse) {
|
|
268
|
+
.hotkey {
|
|
269
|
+
display: none;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
.list {
|
|
273
|
+
box-sizing: border-box;
|
|
274
|
+
overflow-x: hidden;
|
|
275
|
+
position: absolute;
|
|
276
|
+
top: calc(100% + 0.375rem);
|
|
277
|
+
left: 0;
|
|
278
|
+
right: 0;
|
|
279
|
+
z-index: 50;
|
|
280
|
+
max-height: min(26rem, 70vh);
|
|
281
|
+
overflow-y: auto;
|
|
282
|
+
padding: 0.25rem;
|
|
283
|
+
border: 1px solid var(--tern-border);
|
|
284
|
+
border-radius: var(--tern-radius);
|
|
285
|
+
background: var(--tern-surface);
|
|
286
|
+
box-shadow: var(--tern-shadow-lg);
|
|
287
|
+
}
|
|
288
|
+
.item {
|
|
289
|
+
box-sizing: border-box;
|
|
290
|
+
display: grid;
|
|
291
|
+
gap: 0.125rem;
|
|
292
|
+
width: 100%;
|
|
293
|
+
padding: 0.5rem 0.625rem;
|
|
294
|
+
border: 0;
|
|
295
|
+
border-radius: var(--tern-radius-sm);
|
|
296
|
+
background: none;
|
|
297
|
+
color: var(--tern-fg);
|
|
298
|
+
font: inherit;
|
|
299
|
+
font-size: 0.875rem;
|
|
300
|
+
text-align: start;
|
|
301
|
+
text-decoration: none;
|
|
302
|
+
cursor: pointer;
|
|
303
|
+
}
|
|
304
|
+
.item.active {
|
|
305
|
+
background: var(--tern-accent-soft);
|
|
306
|
+
}
|
|
307
|
+
.item,
|
|
308
|
+
.item:hover {
|
|
309
|
+
text-decoration: none;
|
|
310
|
+
}
|
|
311
|
+
.title {
|
|
312
|
+
display: flex;
|
|
313
|
+
align-items: baseline;
|
|
314
|
+
gap: 0.5rem;
|
|
315
|
+
font-weight: 550;
|
|
316
|
+
overflow: hidden;
|
|
317
|
+
}
|
|
318
|
+
.meta {
|
|
319
|
+
margin-left: auto;
|
|
320
|
+
flex: none;
|
|
321
|
+
color: var(--tern-muted);
|
|
322
|
+
font-size: 0.75rem;
|
|
323
|
+
font-weight: 400;
|
|
324
|
+
}
|
|
325
|
+
.snippet {
|
|
326
|
+
color: var(--tern-muted);
|
|
327
|
+
font-size: 0.8125rem;
|
|
328
|
+
overflow: hidden;
|
|
329
|
+
display: -webkit-box;
|
|
330
|
+
-webkit-line-clamp: 2;
|
|
331
|
+
line-clamp: 2;
|
|
332
|
+
-webkit-box-orient: vertical;
|
|
333
|
+
}
|
|
334
|
+
.all {
|
|
335
|
+
color: var(--tern-accent);
|
|
336
|
+
font-weight: 550;
|
|
337
|
+
}
|
|
338
|
+
.note {
|
|
339
|
+
margin: 0;
|
|
340
|
+
padding: 0.75rem 0.625rem;
|
|
341
|
+
color: var(--tern-muted);
|
|
342
|
+
font-size: 0.875rem;
|
|
343
|
+
}
|
|
344
|
+
</style>
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { SearchItem } from './search.js';
|
|
2
|
+
type Props = {
|
|
3
|
+
/** The results page the form goes to on Enter, such as "/search"; without JavaScript, the only way. */
|
|
4
|
+
action?: string;
|
|
5
|
+
/** The query's name in the results page's address. */
|
|
6
|
+
name?: string;
|
|
7
|
+
/** The query; bind it. */
|
|
8
|
+
value?: string;
|
|
9
|
+
/** Results as the user types, such as from a typed endpoint. Without it, the box is a plain search form. */
|
|
10
|
+
search?: (query: string, signal: AbortSignal) => Promise<SearchItem[]>;
|
|
11
|
+
/** Milliseconds to wait after typing before searching. */
|
|
12
|
+
debounce?: number;
|
|
13
|
+
/** The fewest characters to search for. */
|
|
14
|
+
minLength?: number;
|
|
15
|
+
/** A key that focuses the box from anywhere on the page, such as "/". */
|
|
16
|
+
hotkey?: string;
|
|
17
|
+
label?: string;
|
|
18
|
+
placeholder?: string;
|
|
19
|
+
/** Runs when a result is chosen; return false to stay on the page. */
|
|
20
|
+
onselect?: (item: SearchItem) => void | boolean;
|
|
21
|
+
class?: string;
|
|
22
|
+
};
|
|
23
|
+
declare const SearchBox: import("svelte").Component<Props, {}, "value">;
|
|
24
|
+
type SearchBox = ReturnType<typeof SearchBox>;
|
|
25
|
+
export default SearchBox;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import type { FormField } from '../runtime/validate.js';
|
|
2
|
+
/** A field on an admin screen, from the admin package's Field. */
|
|
3
|
+
export type AdminField = {
|
|
4
|
+
name: string;
|
|
5
|
+
label: string;
|
|
6
|
+
description: string;
|
|
7
|
+
/** text, textarea, number, checkbox, date, datetime-local, email, url, password or select. */
|
|
8
|
+
input: string;
|
|
9
|
+
options: string[] | null;
|
|
10
|
+
list: boolean;
|
|
11
|
+
filter: boolean;
|
|
12
|
+
readonly: boolean;
|
|
13
|
+
/** The resource this field holds a key of. */
|
|
14
|
+
ref: string;
|
|
15
|
+
/** The field's rules, for the browser to check: a FormField. */
|
|
16
|
+
rules: Omit<FormField, 'kind'> & {
|
|
17
|
+
kind: string;
|
|
18
|
+
};
|
|
19
|
+
};
|
|
20
|
+
/** What the admin package's Load returns: admin.Page. */
|
|
21
|
+
export type AdminPage = {
|
|
22
|
+
title: string;
|
|
23
|
+
base: string;
|
|
24
|
+
resources: {
|
|
25
|
+
name: string;
|
|
26
|
+
label: string;
|
|
27
|
+
href: string;
|
|
28
|
+
}[];
|
|
29
|
+
/** home, list or form. */
|
|
30
|
+
view: string;
|
|
31
|
+
resource: {
|
|
32
|
+
name: string;
|
|
33
|
+
label: string;
|
|
34
|
+
singular: string;
|
|
35
|
+
href: string;
|
|
36
|
+
key: string;
|
|
37
|
+
fields: AdminField[];
|
|
38
|
+
search: boolean;
|
|
39
|
+
create: boolean;
|
|
40
|
+
edit: boolean;
|
|
41
|
+
delete: boolean;
|
|
42
|
+
} | null;
|
|
43
|
+
list: {
|
|
44
|
+
rows: {
|
|
45
|
+
key: string;
|
|
46
|
+
values: Record<string, string>;
|
|
47
|
+
}[];
|
|
48
|
+
total: number;
|
|
49
|
+
page: number;
|
|
50
|
+
pages: number;
|
|
51
|
+
perPage: number;
|
|
52
|
+
search: string;
|
|
53
|
+
sort: string;
|
|
54
|
+
desc: boolean;
|
|
55
|
+
filters: Record<string, string>;
|
|
56
|
+
} | null;
|
|
57
|
+
record: {
|
|
58
|
+
key: string;
|
|
59
|
+
values: Record<string, string>;
|
|
60
|
+
} | null;
|
|
61
|
+
counts: number[];
|
|
62
|
+
notice: string;
|
|
63
|
+
};
|
|
64
|
+
/** A value as a list shows it: dates without seconds, checkboxes as words. */
|
|
65
|
+
export declare function shown(f: AdminField, v: string | undefined, yes: string, no: string): string;
|
|
66
|
+
/** The list's address with some of its query changed; page goes back to 1 unless it's set. */
|
|
67
|
+
export declare function listHref(base: string, list: NonNullable<AdminPage['list']>, change: Record<string, string | number | null>): string;
|
package/dist/ui/admin.js
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/** A value as a list shows it: dates without seconds, checkboxes as words. */
|
|
2
|
+
export function shown(f, v, yes, no) {
|
|
3
|
+
if (f.input === 'checkbox')
|
|
4
|
+
return v ? yes : no;
|
|
5
|
+
if (!v)
|
|
6
|
+
return '';
|
|
7
|
+
if (f.input === 'datetime-local')
|
|
8
|
+
return v.replace('T', ' ');
|
|
9
|
+
return v;
|
|
10
|
+
}
|
|
11
|
+
/** The list's address with some of its query changed; page goes back to 1 unless it's set. */
|
|
12
|
+
export function listHref(base, list, change) {
|
|
13
|
+
const q = new URLSearchParams();
|
|
14
|
+
if (list.search)
|
|
15
|
+
q.set('q', list.search);
|
|
16
|
+
if (list.sort)
|
|
17
|
+
q.set('sort', (list.desc ? '-' : '') + list.sort);
|
|
18
|
+
for (const [k, v] of Object.entries(list.filters ?? {}))
|
|
19
|
+
q.set(k, v);
|
|
20
|
+
for (const [k, v] of Object.entries(change)) {
|
|
21
|
+
if (v === null || v === '')
|
|
22
|
+
q.delete(k);
|
|
23
|
+
else
|
|
24
|
+
q.set(k, String(v));
|
|
25
|
+
}
|
|
26
|
+
if (!('page' in change))
|
|
27
|
+
q.delete('page');
|
|
28
|
+
if (q.get('page') === '1')
|
|
29
|
+
q.delete('page');
|
|
30
|
+
const s = q.toString();
|
|
31
|
+
return s ? `${base}?${s}` : base;
|
|
32
|
+
}
|
package/dist/ui/index.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export { default as Accordion } from './Accordion.svelte';
|
|
2
|
+
export { default as Admin } from './Admin.svelte';
|
|
2
3
|
export { default as Alert } from './Alert.svelte';
|
|
3
4
|
export { default as AlertDialog } from './AlertDialog.svelte';
|
|
4
5
|
export { default as AreaChart } from './AreaChart.svelte';
|
|
@@ -26,6 +27,7 @@ export { default as Drawer } from './Drawer.svelte';
|
|
|
26
27
|
export { default as Empty } from './Empty.svelte';
|
|
27
28
|
export { default as Field } from './Field.svelte';
|
|
28
29
|
export { default as Form } from './Form.svelte';
|
|
30
|
+
export { default as Highlight } from './Highlight.svelte';
|
|
29
31
|
export { default as HoverCard } from './HoverCard.svelte';
|
|
30
32
|
export { default as Input } from './Input.svelte';
|
|
31
33
|
export { default as InputGroup } from './InputGroup.svelte';
|
|
@@ -44,6 +46,7 @@ export { default as Progress } from './Progress.svelte';
|
|
|
44
46
|
export { default as RadioGroup } from './RadioGroup.svelte';
|
|
45
47
|
export { default as Resizable } from './Resizable.svelte';
|
|
46
48
|
export { default as ScrollArea } from './ScrollArea.svelte';
|
|
49
|
+
export { default as SearchBox } from './SearchBox.svelte';
|
|
47
50
|
export { default as Select } from './Select.svelte';
|
|
48
51
|
export { default as SelectMenu } from './SelectMenu.svelte';
|
|
49
52
|
export { default as Separator } from './Separator.svelte';
|
|
@@ -84,6 +87,8 @@ export { dismiss, toast, toasts, type Toast, type Tone } from './toast.svelte.js
|
|
|
84
87
|
export type { MenuItem } from './menu.js';
|
|
85
88
|
export type { Align, Side } from './floating.js';
|
|
86
89
|
export type { Option } from './options.js';
|
|
90
|
+
export type { AdminField, AdminPage } from './admin.js';
|
|
91
|
+
export type { SearchItem, Segment } from './search.js';
|
|
87
92
|
export type { ISODate } from './dates.js';
|
|
88
93
|
export type { DateRange } from './Calendar.svelte';
|
|
89
94
|
export type { CommandItem } from './Command.svelte';
|
package/dist/ui/index.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// theme once (import '@ternkit/tern/ui/theme.css') and the components where
|
|
3
3
|
// you use them. See docs/ui.md.
|
|
4
4
|
export { default as Accordion } from './Accordion.svelte';
|
|
5
|
+
export { default as Admin } from './Admin.svelte';
|
|
5
6
|
export { default as Alert } from './Alert.svelte';
|
|
6
7
|
export { default as AlertDialog } from './AlertDialog.svelte';
|
|
7
8
|
export { default as AreaChart } from './AreaChart.svelte';
|
|
@@ -29,6 +30,7 @@ export { default as Drawer } from './Drawer.svelte';
|
|
|
29
30
|
export { default as Empty } from './Empty.svelte';
|
|
30
31
|
export { default as Field } from './Field.svelte';
|
|
31
32
|
export { default as Form } from './Form.svelte';
|
|
33
|
+
export { default as Highlight } from './Highlight.svelte';
|
|
32
34
|
export { default as HoverCard } from './HoverCard.svelte';
|
|
33
35
|
export { default as Input } from './Input.svelte';
|
|
34
36
|
export { default as InputGroup } from './InputGroup.svelte';
|
|
@@ -47,6 +49,7 @@ export { default as Progress } from './Progress.svelte';
|
|
|
47
49
|
export { default as RadioGroup } from './RadioGroup.svelte';
|
|
48
50
|
export { default as Resizable } from './Resizable.svelte';
|
|
49
51
|
export { default as ScrollArea } from './ScrollArea.svelte';
|
|
52
|
+
export { default as SearchBox } from './SearchBox.svelte';
|
|
50
53
|
export { default as Select } from './Select.svelte';
|
|
51
54
|
export { default as SelectMenu } from './SelectMenu.svelte';
|
|
52
55
|
export { default as Separator } from './Separator.svelte';
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/** A piece of a search snippet: text that matched, or text around it. The search package's Segment. */
|
|
2
|
+
export type Segment = {
|
|
3
|
+
text: string;
|
|
4
|
+
match?: boolean;
|
|
5
|
+
};
|
|
6
|
+
/** A result in a SearchBox. */
|
|
7
|
+
export type SearchItem = {
|
|
8
|
+
/** Where choosing it goes. */
|
|
9
|
+
href: string;
|
|
10
|
+
/** Its title, plain or with the matches marked. */
|
|
11
|
+
title: string | Segment[];
|
|
12
|
+
/** Text around what matched, such as a snippet of its body. */
|
|
13
|
+
snippet?: Segment[];
|
|
14
|
+
/** A short line beside the title, such as its section. */
|
|
15
|
+
meta?: string;
|
|
16
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/vite/goscript.js
CHANGED
|
@@ -60,7 +60,9 @@ function scan(root) {
|
|
|
60
60
|
export function exportedNames(source) {
|
|
61
61
|
const values = [
|
|
62
62
|
...source.matchAll(/\b(?:State|Derived)(?:\[[^\]]*\])?\(\s*\w+\s*,\s*"([A-Za-z_$][\w$]*)"/g),
|
|
63
|
-
...source.matchAll(/\.Show\(\s*\w+\s*,\s*"([A-Za-z_$][\w$]*)"/g)
|
|
63
|
+
...source.matchAll(/\.Show\(\s*\w+\s*,\s*"([A-Za-z_$][\w$]*)"/g),
|
|
64
|
+
// A room's Show takes the room's key first: room.Show(c, "board:"+id, "people").
|
|
65
|
+
...source.matchAll(/\.Show\(\s*\w+\s*,\s*(?:[^,()"]|"[^"]*"|\([^()]*\))+,\s*"([A-Za-z_$][\w$]*)"/g)
|
|
64
66
|
].map((m) => m[1]);
|
|
65
67
|
const funcs = [...source.matchAll(/\.Func\(\s*"([A-Za-z_$][\w$]*)"/g)].map((m) => m[1]);
|
|
66
68
|
return { values: [...new Set(values)], funcs: [...new Set(funcs)] };
|