@ternkit/tern 1.2.1 → 1.3.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/goscript/index.svelte.d.ts +32 -2
- package/dist/goscript/index.svelte.js +257 -31
- package/dist/goscript/patch.d.ts +6 -0
- package/dist/goscript/patch.js +25 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/ui/Accordion.svelte +84 -0
- package/dist/ui/Accordion.svelte.d.ts +20 -0
- package/dist/ui/Alert.svelte +76 -0
- package/dist/ui/Alert.svelte.d.ts +12 -0
- package/dist/ui/Avatar.svelte +48 -0
- package/dist/ui/Avatar.svelte.d.ts +10 -0
- package/dist/ui/Badge.svelte +54 -0
- package/dist/ui/Badge.svelte.d.ts +10 -0
- package/dist/ui/Button.svelte +136 -0
- package/dist/ui/Button.svelte.d.ts +18 -0
- package/dist/ui/Card.svelte +74 -0
- package/dist/ui/Card.svelte.d.ts +18 -0
- package/dist/ui/Checkbox.svelte +100 -0
- package/dist/ui/Checkbox.svelte.d.ts +14 -0
- package/dist/ui/Dialog.svelte +160 -0
- package/dist/ui/Dialog.svelte.d.ts +20 -0
- package/dist/ui/Field.svelte +77 -0
- package/dist/ui/Field.svelte.d.ts +17 -0
- package/dist/ui/Input.svelte +61 -0
- package/dist/ui/Input.svelte.d.ts +7 -0
- package/dist/ui/Kbd.svelte +24 -0
- package/dist/ui/Kbd.svelte.d.ts +7 -0
- package/dist/ui/Menu.svelte +232 -0
- package/dist/ui/Menu.svelte.d.ts +22 -0
- package/dist/ui/Progress.svelte +73 -0
- package/dist/ui/Progress.svelte.d.ts +12 -0
- package/dist/ui/RadioGroup.svelte +105 -0
- package/dist/ui/RadioGroup.svelte.d.ts +21 -0
- package/dist/ui/Select.svelte +76 -0
- package/dist/ui/Select.svelte.d.ts +18 -0
- package/dist/ui/Separator.svelte +19 -0
- package/dist/ui/Separator.svelte.d.ts +7 -0
- package/dist/ui/Skeleton.svelte +29 -0
- package/dist/ui/Skeleton.svelte.d.ts +9 -0
- package/dist/ui/Spinner.svelte +42 -0
- package/dist/ui/Spinner.svelte.d.ts +8 -0
- package/dist/ui/Switch.svelte +90 -0
- package/dist/ui/Switch.svelte.d.ts +12 -0
- package/dist/ui/Tabs.svelte +112 -0
- package/dist/ui/Tabs.svelte.d.ts +19 -0
- package/dist/ui/Textarea.svelte +51 -0
- package/dist/ui/Textarea.svelte.d.ts +7 -0
- package/dist/ui/Toaster.svelte +146 -0
- package/dist/ui/Toaster.svelte.d.ts +6 -0
- package/dist/ui/Tooltip.svelte +77 -0
- package/dist/ui/Tooltip.svelte.d.ts +15 -0
- package/dist/ui/field.d.ts +10 -0
- package/dist/ui/field.js +8 -0
- package/dist/ui/index.d.ts +24 -0
- package/dist/ui/index.js +27 -0
- package/dist/ui/theme.css +151 -0
- package/dist/ui/toast.svelte.d.ts +19 -0
- package/dist/ui/toast.svelte.js +18 -0
- package/dist/vite/goscript.d.ts +14 -0
- package/dist/vite/goscript.js +45 -16
- package/dist/vite/index.js +5 -2
- package/package.json +7 -1
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { applyPatch, type PatchOp } from './patch.js';
|
|
2
|
+
export { applyPatch, type PatchOp };
|
|
1
3
|
type Instance = {
|
|
2
4
|
call(name: string, args: string): string | null;
|
|
3
5
|
destroy(): void;
|
|
@@ -30,9 +32,38 @@ export declare function useGo(id: string, initial: Record<string, unknown>, runt
|
|
|
30
32
|
declare global {
|
|
31
33
|
var __tern_live_initial: ((id: string, props: string) => string) | undefined;
|
|
32
34
|
}
|
|
33
|
-
|
|
35
|
+
/** A Func of a live component, as the markup sees it. */
|
|
36
|
+
export interface LiveFunc<A extends unknown[] = unknown[], R = unknown> {
|
|
37
|
+
(...args: A): Promise<R>;
|
|
38
|
+
/** Whether a call is waiting for the server. */
|
|
39
|
+
readonly pending: boolean;
|
|
40
|
+
/** The last call's error message (from goscript.Fail), or null. */
|
|
41
|
+
readonly error: string | null;
|
|
42
|
+
/** Calls the Func, showing values for these state names until it returns. */
|
|
43
|
+
optimistic(values: Record<string, unknown>, ...args: A): Promise<R>;
|
|
44
|
+
/** A version that waits until calls stop for ms (default 200), for input events. */
|
|
45
|
+
debounce(ms?: number): (...args: A) => void;
|
|
46
|
+
}
|
|
47
|
+
export interface LiveComponent<S extends Record<string, unknown> = Record<string, unknown>> extends GoComponent {
|
|
48
|
+
readonly state: S;
|
|
34
49
|
/** The server-rendered state, embedded in the page for hydration. */
|
|
35
50
|
readonly stateScript: string;
|
|
51
|
+
/** Whether the page is connected to the server. */
|
|
52
|
+
readonly connected: boolean;
|
|
53
|
+
/** Whether any call is waiting for the server. */
|
|
54
|
+
readonly pending: boolean;
|
|
55
|
+
/** Why the component failed to mount (such as goscript Reject's message), or null. */
|
|
56
|
+
readonly error: string | null;
|
|
57
|
+
/** Whether the state has arrived; the markup renders once it has. */
|
|
58
|
+
readonly loaded: boolean;
|
|
59
|
+
/** Everything the parent passed, such as children snippets and onevent handlers. */
|
|
60
|
+
readonly props: Record<string, any>;
|
|
61
|
+
/** Bindable state: bind:value={live.bind.name} for a State declared with goscript.Bind. */
|
|
62
|
+
readonly bind: S;
|
|
63
|
+
/** Sets a State declared with goscript.Bind; the page shows value at once. */
|
|
64
|
+
set<K extends keyof S & string>(name: K, value: S[K]): Promise<void>;
|
|
65
|
+
/** The Func named name. */
|
|
66
|
+
fn<A extends unknown[] = unknown[], R = unknown>(name: string): LiveFunc<A, R>;
|
|
36
67
|
}
|
|
37
68
|
/**
|
|
38
69
|
* Mounts a live Go component; code generated from <script lang="go" live>
|
|
@@ -41,4 +72,3 @@ export interface LiveComponent extends GoComponent {
|
|
|
41
72
|
* /_tern/live, where its Go instance runs.
|
|
42
73
|
*/
|
|
43
74
|
export declare function useLive(id: string, uid: string, props: () => Record<string, unknown>): LiveComponent;
|
|
44
|
-
export {};
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
// The browser side of the experimental <script lang="go">: loads the app's
|
|
2
2
|
// Go WebAssembly module once, mounts a Go instance per component, and keeps
|
|
3
3
|
// the values Go publishes in Svelte state. See docs/experimental-go-script.md.
|
|
4
|
+
import { untrack } from 'svelte';
|
|
4
5
|
import { getBase } from '../runtime/base.js';
|
|
6
|
+
import { applyPatch } from './patch.js';
|
|
7
|
+
export { applyPatch };
|
|
5
8
|
let loading = null;
|
|
6
9
|
function load(runtime, wasm) {
|
|
7
10
|
loading ??= (async () => {
|
|
@@ -65,6 +68,87 @@ export function useGo(id, initial, runtime, wasm, props) {
|
|
|
65
68
|
}
|
|
66
69
|
};
|
|
67
70
|
}
|
|
71
|
+
function clone(v) {
|
|
72
|
+
return v === undefined ? v : JSON.parse(JSON.stringify(v));
|
|
73
|
+
}
|
|
74
|
+
/** The browser side of one live component instance. */
|
|
75
|
+
class LiveInstance {
|
|
76
|
+
id;
|
|
77
|
+
props;
|
|
78
|
+
/** What the markup shows: the server's values, or optimistic ones while a call is pending. */
|
|
79
|
+
state;
|
|
80
|
+
/** The server's values, as last sent. */
|
|
81
|
+
#server;
|
|
82
|
+
#holds = new Map();
|
|
83
|
+
calls = new Map();
|
|
84
|
+
pendingCalls = $state({});
|
|
85
|
+
errors = $state({});
|
|
86
|
+
error = $state(null);
|
|
87
|
+
/** Whether the component has its state: from the server render, or the first message. */
|
|
88
|
+
loaded = $state(false);
|
|
89
|
+
mounted = false;
|
|
90
|
+
constructor(id, props, initial) {
|
|
91
|
+
this.id = id;
|
|
92
|
+
this.props = props;
|
|
93
|
+
const { __tern_error: error, ...values } = initial ?? {};
|
|
94
|
+
this.state = $state(clone(values));
|
|
95
|
+
this.#server = clone(values);
|
|
96
|
+
this.loaded = initial !== null;
|
|
97
|
+
if (typeof error === 'string')
|
|
98
|
+
this.error = error;
|
|
99
|
+
}
|
|
100
|
+
apply(m) {
|
|
101
|
+
this.loaded = true;
|
|
102
|
+
for (const [name, v] of Object.entries(m.set ?? {})) {
|
|
103
|
+
this.#server[name] = clone(v);
|
|
104
|
+
if (!this.#holds.has(name))
|
|
105
|
+
this.state[name] = v;
|
|
106
|
+
}
|
|
107
|
+
for (const [name, ops] of Object.entries(m.patch ?? {})) {
|
|
108
|
+
this.#server[name] = applyPatch(this.#server[name], clone(ops));
|
|
109
|
+
if (!this.#holds.has(name))
|
|
110
|
+
this.state[name] = applyPatch(this.state[name], ops);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
/** Shows value for name until the operation holding it settles. */
|
|
114
|
+
hold(name, value) {
|
|
115
|
+
this.#holds.set(name, (this.#holds.get(name) ?? 0) + 1);
|
|
116
|
+
this.state[name] = value;
|
|
117
|
+
}
|
|
118
|
+
release(names) {
|
|
119
|
+
for (const name of names) {
|
|
120
|
+
const n = (this.#holds.get(name) ?? 1) - 1;
|
|
121
|
+
if (n > 0) {
|
|
122
|
+
this.#holds.set(name, n);
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
this.#holds.delete(name);
|
|
126
|
+
this.state[name] = clone(this.#server[name]);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
begin(seq, call) {
|
|
130
|
+
this.calls.set(seq, call);
|
|
131
|
+
this.pendingCalls[call.name] = (this.pendingCalls[call.name] ?? 0) + 1;
|
|
132
|
+
}
|
|
133
|
+
settle(seq, value, error) {
|
|
134
|
+
const call = this.calls.get(seq);
|
|
135
|
+
if (!call)
|
|
136
|
+
return;
|
|
137
|
+
this.calls.delete(seq);
|
|
138
|
+
this.pendingCalls[call.name] = (this.pendingCalls[call.name] ?? 1) - 1;
|
|
139
|
+
this.release(call.holds);
|
|
140
|
+
this.errors[call.name] = error ?? null;
|
|
141
|
+
if (error)
|
|
142
|
+
call.reject(new Error(error));
|
|
143
|
+
else
|
|
144
|
+
call.resolve(value);
|
|
145
|
+
}
|
|
146
|
+
/** The values to restore after reconnecting to a new server process. */
|
|
147
|
+
snapshot() {
|
|
148
|
+
return $state.snapshot(this.state);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
const status = $state({ connected: false });
|
|
68
152
|
class LiveConnection {
|
|
69
153
|
#ws = null;
|
|
70
154
|
#refs = new Map();
|
|
@@ -76,6 +160,12 @@ class LiveConnection {
|
|
|
76
160
|
constructor(url) {
|
|
77
161
|
this.#url = url;
|
|
78
162
|
}
|
|
163
|
+
#mountMessage(ref, r) {
|
|
164
|
+
const props = untrack(() => r.props());
|
|
165
|
+
// After a reconnect, the server has forgotten the instance: values
|
|
166
|
+
// declared with Bind or Restore come back from what the page shows.
|
|
167
|
+
return JSON.stringify({ op: 'mount', ref, id: r.id, props, restore: r.mounted ? r.snapshot() : undefined });
|
|
168
|
+
}
|
|
79
169
|
#connect() {
|
|
80
170
|
const u = new URL(this.#url, location.href);
|
|
81
171
|
u.protocol = u.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
@@ -83,40 +173,71 @@ class LiveConnection {
|
|
|
83
173
|
this.#ws = ws;
|
|
84
174
|
ws.onopen = () => {
|
|
85
175
|
this.#attempt = 0;
|
|
86
|
-
|
|
87
|
-
for (const [ref, r] of this.#refs)
|
|
88
|
-
ws.send(
|
|
89
|
-
|
|
90
|
-
|
|
176
|
+
status.connected = true;
|
|
177
|
+
for (const [ref, r] of this.#refs) {
|
|
178
|
+
ws.send(this.#mountMessage(ref, r));
|
|
179
|
+
r.mounted = true;
|
|
180
|
+
}
|
|
181
|
+
for (const m of this.#queue.splice(0)) {
|
|
182
|
+
if (m.ref !== undefined && !this.#refs.has(m.ref))
|
|
183
|
+
continue;
|
|
184
|
+
ws.send(m.data);
|
|
185
|
+
if (m.seq !== undefined)
|
|
186
|
+
this.#markSent(m.ref, m.seq);
|
|
187
|
+
}
|
|
91
188
|
};
|
|
92
189
|
ws.onmessage = (e) => {
|
|
93
190
|
const m = JSON.parse(e.data);
|
|
94
191
|
const r = this.#refs.get(m.ref);
|
|
95
192
|
if (!r)
|
|
96
193
|
return;
|
|
97
|
-
if (m.op === 'state'
|
|
98
|
-
r.
|
|
99
|
-
else if (m.op === 'result')
|
|
100
|
-
r.
|
|
101
|
-
|
|
194
|
+
if (m.op === 'state')
|
|
195
|
+
r.apply(m);
|
|
196
|
+
else if (m.op === 'result')
|
|
197
|
+
r.settle(m.seq, m.value, m.error);
|
|
198
|
+
else if (m.op === 'event') {
|
|
199
|
+
const handler = untrack(() => r.props())['on' + m.name];
|
|
200
|
+
if (typeof handler === 'function')
|
|
201
|
+
handler(m.value);
|
|
102
202
|
}
|
|
103
|
-
else if (m.op === 'error')
|
|
203
|
+
else if (m.op === 'error') {
|
|
204
|
+
r.error = m.error ?? 'error';
|
|
205
|
+
r.loaded = true;
|
|
104
206
|
console.error(`[tern] live component ${r.id}: ${m.error}`);
|
|
207
|
+
}
|
|
105
208
|
};
|
|
106
209
|
ws.onclose = () => {
|
|
107
|
-
this.#ws
|
|
210
|
+
if (this.#ws === ws)
|
|
211
|
+
this.#ws = null;
|
|
212
|
+
status.connected = false;
|
|
213
|
+
// Calls that reached the old connection have no answer coming.
|
|
214
|
+
for (const r of this.#refs.values()) {
|
|
215
|
+
for (const [seq, call] of r.calls)
|
|
216
|
+
if (call.sent)
|
|
217
|
+
r.settle(seq, undefined, 'connection lost');
|
|
218
|
+
}
|
|
108
219
|
if (this.#refs.size === 0)
|
|
109
220
|
return;
|
|
110
|
-
const delay = Math.min(10_000,
|
|
111
|
-
setTimeout(() => this.#refs.size && this.#connect(), delay);
|
|
221
|
+
const delay = Math.min(10_000, 250 * 2 ** this.#attempt++) * (0.75 + Math.random() / 2);
|
|
222
|
+
setTimeout(() => this.#refs.size && !this.#ws && this.#connect(), delay);
|
|
112
223
|
};
|
|
113
224
|
}
|
|
225
|
+
#markSent(ref, seq) {
|
|
226
|
+
const call = this.#refs.get(ref)?.calls.get(seq);
|
|
227
|
+
if (call)
|
|
228
|
+
call.sent = true;
|
|
229
|
+
}
|
|
114
230
|
#send(m) {
|
|
115
231
|
const data = JSON.stringify(m);
|
|
116
|
-
|
|
232
|
+
const seq = m.seq;
|
|
233
|
+
const ref = m.ref;
|
|
234
|
+
if (this.#ws?.readyState === WebSocket.OPEN) {
|
|
117
235
|
this.#ws.send(data);
|
|
236
|
+
if (seq !== undefined)
|
|
237
|
+
this.#markSent(ref, seq);
|
|
238
|
+
}
|
|
118
239
|
else {
|
|
119
|
-
this.#queue.push(data);
|
|
240
|
+
this.#queue.push({ data, seq, ref });
|
|
120
241
|
if (!this.#ws)
|
|
121
242
|
this.#connect();
|
|
122
243
|
}
|
|
@@ -124,7 +245,12 @@ class LiveConnection {
|
|
|
124
245
|
mount(r) {
|
|
125
246
|
const ref = this.#nextRef++;
|
|
126
247
|
this.#refs.set(ref, r);
|
|
127
|
-
this.#
|
|
248
|
+
if (this.#ws?.readyState === WebSocket.OPEN) {
|
|
249
|
+
this.#ws.send(this.#mountMessage(ref, r));
|
|
250
|
+
r.mounted = true;
|
|
251
|
+
}
|
|
252
|
+
else if (!this.#ws)
|
|
253
|
+
this.#connect();
|
|
128
254
|
return ref;
|
|
129
255
|
}
|
|
130
256
|
unmount(ref) {
|
|
@@ -132,19 +258,44 @@ class LiveConnection {
|
|
|
132
258
|
if (this.#ws?.readyState === WebSocket.OPEN)
|
|
133
259
|
this.#ws.send(JSON.stringify({ op: 'unmount', ref }));
|
|
134
260
|
if (this.#refs.size === 0) {
|
|
135
|
-
this.#ws
|
|
261
|
+
const ws = this.#ws;
|
|
136
262
|
this.#ws = null;
|
|
263
|
+
this.#queue = [];
|
|
264
|
+
ws?.close(1000);
|
|
137
265
|
}
|
|
138
266
|
}
|
|
139
|
-
|
|
267
|
+
props(ref, props) {
|
|
268
|
+
this.#send({ op: 'props', ref, props });
|
|
269
|
+
}
|
|
270
|
+
call(ref, name, args, optimistic) {
|
|
140
271
|
const r = this.#refs.get(ref);
|
|
141
272
|
if (!r)
|
|
142
|
-
return Promise.
|
|
273
|
+
return Promise.reject(new Error('not mounted'));
|
|
143
274
|
const seq = this.#seq++;
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
275
|
+
const holds = Object.keys(optimistic ?? {});
|
|
276
|
+
for (const k of holds)
|
|
277
|
+
r.hold(k, optimistic[k]);
|
|
278
|
+
const p = new Promise((resolve, reject) => {
|
|
279
|
+
r.begin(seq, { resolve, reject, name, holds, sent: false });
|
|
147
280
|
});
|
|
281
|
+
this.#send({ op: 'call', ref, seq, name, args });
|
|
282
|
+
// The error is also in fn.error; don't report unawaited calls as
|
|
283
|
+
// unhandled rejections.
|
|
284
|
+
p.catch(() => { });
|
|
285
|
+
return p;
|
|
286
|
+
}
|
|
287
|
+
set(ref, name, value) {
|
|
288
|
+
const r = this.#refs.get(ref);
|
|
289
|
+
if (!r)
|
|
290
|
+
return Promise.reject(new Error('not mounted'));
|
|
291
|
+
const seq = this.#seq++;
|
|
292
|
+
r.hold(name, value);
|
|
293
|
+
const p = new Promise((resolve, reject) => {
|
|
294
|
+
r.begin(seq, { resolve, reject, name: '$set:' + name, holds: [name], sent: false });
|
|
295
|
+
});
|
|
296
|
+
this.#send({ op: 'set', ref, seq, name, value: $state.snapshot(value) });
|
|
297
|
+
p.catch(() => { });
|
|
298
|
+
return p;
|
|
148
299
|
}
|
|
149
300
|
}
|
|
150
301
|
let connection = null;
|
|
@@ -159,7 +310,9 @@ function escapeJSON(s) {
|
|
|
159
310
|
*/
|
|
160
311
|
export function useLive(id, uid, props) {
|
|
161
312
|
const elementID = `tern-live-${uid}`;
|
|
162
|
-
|
|
313
|
+
// After a client-side navigation there is no server-rendered state: the
|
|
314
|
+
// markup waits for the first message (see the preprocessor).
|
|
315
|
+
let initial = null;
|
|
163
316
|
let stateScript = '';
|
|
164
317
|
if (typeof window === 'undefined') {
|
|
165
318
|
const raw = globalThis.__tern_live_initial?.(id, JSON.stringify(props())) ?? '{}';
|
|
@@ -173,14 +326,13 @@ export function useLive(id, uid, props) {
|
|
|
173
326
|
stateScript = el.outerHTML;
|
|
174
327
|
}
|
|
175
328
|
}
|
|
176
|
-
const
|
|
329
|
+
const inst = new LiveInstance(id, props, initial);
|
|
177
330
|
let ref = 0;
|
|
178
331
|
let ready = $state(false);
|
|
179
332
|
if (typeof window !== 'undefined') {
|
|
180
333
|
$effect(() => {
|
|
181
334
|
connection ??= new LiveConnection(`${getBase()}/_tern/live`);
|
|
182
|
-
|
|
183
|
-
ref = connection.mount(live);
|
|
335
|
+
ref = connection.mount(inst);
|
|
184
336
|
ready = true;
|
|
185
337
|
return () => {
|
|
186
338
|
connection?.unmount(ref);
|
|
@@ -188,17 +340,91 @@ export function useLive(id, uid, props) {
|
|
|
188
340
|
ready = false;
|
|
189
341
|
};
|
|
190
342
|
});
|
|
343
|
+
// New props from the parent go to the server without remounting.
|
|
344
|
+
let sent = JSON.stringify(untrack(props));
|
|
345
|
+
$effect(() => {
|
|
346
|
+
const next = JSON.stringify(props());
|
|
347
|
+
if (next !== sent && ref) {
|
|
348
|
+
sent = next;
|
|
349
|
+
connection.props(ref, JSON.parse(next));
|
|
350
|
+
}
|
|
351
|
+
});
|
|
191
352
|
}
|
|
353
|
+
const call = (name, args, optimistic) => {
|
|
354
|
+
if (!ref || !connection)
|
|
355
|
+
return Promise.reject(new Error('not mounted'));
|
|
356
|
+
return connection.call(ref, name, args.map(toArg), optimistic);
|
|
357
|
+
};
|
|
358
|
+
const funcs = new Map();
|
|
359
|
+
const fn = (name) => {
|
|
360
|
+
let f = funcs.get(name);
|
|
361
|
+
if (f)
|
|
362
|
+
return f;
|
|
363
|
+
const debounced = new Map();
|
|
364
|
+
f = Object.assign((...args) => call(name, args), {
|
|
365
|
+
optimistic: (values, ...args) => call(name, args, values),
|
|
366
|
+
debounce: (ms = 200) => {
|
|
367
|
+
let d = debounced.get(ms);
|
|
368
|
+
if (!d) {
|
|
369
|
+
let timer;
|
|
370
|
+
d = (...args) => {
|
|
371
|
+
const converted = args.map(toArg); // read input values now
|
|
372
|
+
clearTimeout(timer);
|
|
373
|
+
timer = setTimeout(() => void call(name, converted), ms);
|
|
374
|
+
};
|
|
375
|
+
debounced.set(ms, d);
|
|
376
|
+
}
|
|
377
|
+
return d;
|
|
378
|
+
}
|
|
379
|
+
});
|
|
380
|
+
Object.defineProperties(f, {
|
|
381
|
+
pending: { get: () => (inst.pendingCalls[name] ?? 0) > 0 },
|
|
382
|
+
error: { get: () => inst.errors[name] ?? null }
|
|
383
|
+
});
|
|
384
|
+
funcs.set(name, f);
|
|
385
|
+
return f;
|
|
386
|
+
};
|
|
387
|
+
const set = (name, value) => {
|
|
388
|
+
if (!ref || !connection) {
|
|
389
|
+
inst.state[name] = value;
|
|
390
|
+
return Promise.resolve();
|
|
391
|
+
}
|
|
392
|
+
return connection.set(ref, name, value).then(() => undefined);
|
|
393
|
+
};
|
|
394
|
+
const bind = new Proxy({}, {
|
|
395
|
+
get: (_, name) => (typeof name === 'string' ? inst.state[name] : undefined),
|
|
396
|
+
set: (_, name, value) => {
|
|
397
|
+
if (typeof name === 'string')
|
|
398
|
+
void set(name, value);
|
|
399
|
+
return true;
|
|
400
|
+
}
|
|
401
|
+
});
|
|
192
402
|
return {
|
|
193
|
-
state,
|
|
403
|
+
state: inst.state,
|
|
194
404
|
stateScript,
|
|
405
|
+
bind,
|
|
406
|
+
fn: fn,
|
|
407
|
+
set: set,
|
|
195
408
|
get ready() {
|
|
196
409
|
return ready;
|
|
197
410
|
},
|
|
411
|
+
get props() {
|
|
412
|
+
return props();
|
|
413
|
+
},
|
|
414
|
+
get connected() {
|
|
415
|
+
return status.connected;
|
|
416
|
+
},
|
|
417
|
+
get pending() {
|
|
418
|
+
return Object.values(inst.pendingCalls).some((n) => n > 0);
|
|
419
|
+
},
|
|
420
|
+
get error() {
|
|
421
|
+
return inst.error;
|
|
422
|
+
},
|
|
423
|
+
get loaded() {
|
|
424
|
+
return inst.loaded;
|
|
425
|
+
},
|
|
198
426
|
call(name, ...args) {
|
|
199
|
-
|
|
200
|
-
return undefined;
|
|
201
|
-
return connection.call(ref, name, args.map(toArg));
|
|
427
|
+
return call(name, args);
|
|
202
428
|
}
|
|
203
429
|
};
|
|
204
430
|
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
type Key = string | number;
|
|
2
|
+
/** One patch operation from the server. */
|
|
3
|
+
export type PatchOp = ['s', Key[], unknown] | ['d', Key[]] | ['a', Key[], number, number, unknown[]];
|
|
4
|
+
/** Applies a patch to root and returns the new root. */
|
|
5
|
+
export declare function applyPatch(root: unknown, ops: PatchOp[]): unknown;
|
|
6
|
+
export {};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// Applying the patches live components send; see internal/jsondiff.
|
|
2
|
+
/** Applies a patch to root and returns the new root. */
|
|
3
|
+
export function applyPatch(root, ops) {
|
|
4
|
+
for (const op of ops) {
|
|
5
|
+
const path = op[1];
|
|
6
|
+
if (path.length === 0) {
|
|
7
|
+
if (op[0] === 's')
|
|
8
|
+
root = op[2];
|
|
9
|
+
else if (op[0] === 'a')
|
|
10
|
+
root.splice(op[2], op[3], ...op[4]);
|
|
11
|
+
continue;
|
|
12
|
+
}
|
|
13
|
+
let parent = root;
|
|
14
|
+
for (const k of path.slice(0, -1))
|
|
15
|
+
parent = parent[k];
|
|
16
|
+
const last = path[path.length - 1];
|
|
17
|
+
if (op[0] === 's')
|
|
18
|
+
parent[last] = op[2];
|
|
19
|
+
else if (op[0] === 'd')
|
|
20
|
+
delete parent[last];
|
|
21
|
+
else
|
|
22
|
+
parent[last].splice(op[2], op[3], ...op[4]);
|
|
23
|
+
}
|
|
24
|
+
return root;
|
|
25
|
+
}
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -16,4 +16,4 @@ export { stream, socket, EventStreamState, SocketState } from './runtime/realtim
|
|
|
16
16
|
export { default as Image } from './image/Image.svelte';
|
|
17
17
|
export { t, i18n, setLocale } from './runtime/i18n.svelte.js';
|
|
18
18
|
/** The version of the Tern client runtime. */
|
|
19
|
-
export const VERSION = '1.
|
|
19
|
+
export const VERSION = '1.3.0';
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Snippet } from 'svelte';
|
|
3
|
+
|
|
4
|
+
type Item = { id: string; title: string };
|
|
5
|
+
type Props = {
|
|
6
|
+
items: Item[];
|
|
7
|
+
/** The open items' ids; bind it. */
|
|
8
|
+
open?: string[];
|
|
9
|
+
/** Let several items be open at once. */
|
|
10
|
+
multiple?: boolean;
|
|
11
|
+
/** The heading level of the item titles, to fit the page's outline. */
|
|
12
|
+
level?: 2 | 3 | 4 | 5 | 6;
|
|
13
|
+
class?: string;
|
|
14
|
+
/** Renders an item's content. */
|
|
15
|
+
content: Snippet<[string]>;
|
|
16
|
+
};
|
|
17
|
+
let { items, open = $bindable([]), multiple = false, level = 3, class: klass = '', content }: Props = $props();
|
|
18
|
+
const uid = $props.id();
|
|
19
|
+
|
|
20
|
+
function toggle(id: string) {
|
|
21
|
+
if (open.includes(id)) open = open.filter((x) => x !== id);
|
|
22
|
+
else open = multiple ? [...open, id] : [id];
|
|
23
|
+
}
|
|
24
|
+
</script>
|
|
25
|
+
|
|
26
|
+
<div class="tern-accordion {klass}">
|
|
27
|
+
{#each items as item (item.id)}
|
|
28
|
+
{@const expanded = open.includes(item.id)}
|
|
29
|
+
<div class="item">
|
|
30
|
+
<div role="heading" aria-level={level}>
|
|
31
|
+
<button type="button" id="tern-acc-{uid}-{item.id}" aria-expanded={expanded} aria-controls="tern-acc-{uid}-{item.id}-panel" onclick={() => toggle(item.id)}>
|
|
32
|
+
{item.title}
|
|
33
|
+
<svg aria-hidden="true" viewBox="0 0 16 16" width="16" height="16"><path d="M4 6l4 4 4-4" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" /></svg>
|
|
34
|
+
</button>
|
|
35
|
+
</div>
|
|
36
|
+
<div class="panel" role="region" id="tern-acc-{uid}-{item.id}-panel" aria-labelledby="tern-acc-{uid}-{item.id}" hidden={!expanded}>
|
|
37
|
+
{#if expanded}{@render content(item.id)}{/if}
|
|
38
|
+
</div>
|
|
39
|
+
</div>
|
|
40
|
+
{/each}
|
|
41
|
+
</div>
|
|
42
|
+
|
|
43
|
+
<style>
|
|
44
|
+
.tern-accordion {
|
|
45
|
+
border-top: 1px solid var(--tern-border);
|
|
46
|
+
}
|
|
47
|
+
.item {
|
|
48
|
+
border-bottom: 1px solid var(--tern-border);
|
|
49
|
+
}
|
|
50
|
+
button {
|
|
51
|
+
display: flex;
|
|
52
|
+
width: 100%;
|
|
53
|
+
align-items: center;
|
|
54
|
+
justify-content: space-between;
|
|
55
|
+
gap: 1rem;
|
|
56
|
+
padding: 0.85rem 0.25rem;
|
|
57
|
+
border: 0;
|
|
58
|
+
background: transparent;
|
|
59
|
+
font: inherit;
|
|
60
|
+
font-size: 0.9375rem;
|
|
61
|
+
font-weight: 550;
|
|
62
|
+
color: var(--tern-fg);
|
|
63
|
+
text-align: start;
|
|
64
|
+
cursor: pointer;
|
|
65
|
+
}
|
|
66
|
+
button:focus-visible {
|
|
67
|
+
outline: 2px solid var(--tern-ring);
|
|
68
|
+
outline-offset: -2px;
|
|
69
|
+
border-radius: var(--tern-radius-sm);
|
|
70
|
+
}
|
|
71
|
+
svg {
|
|
72
|
+
flex: none;
|
|
73
|
+
color: var(--tern-muted);
|
|
74
|
+
transition: rotate var(--tern-duration);
|
|
75
|
+
}
|
|
76
|
+
[aria-expanded='true'] svg {
|
|
77
|
+
rotate: 180deg;
|
|
78
|
+
}
|
|
79
|
+
.panel {
|
|
80
|
+
padding: 0 0.25rem 1rem;
|
|
81
|
+
color: var(--tern-muted);
|
|
82
|
+
font-size: 0.875rem;
|
|
83
|
+
}
|
|
84
|
+
</style>
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
type Item = {
|
|
3
|
+
id: string;
|
|
4
|
+
title: string;
|
|
5
|
+
};
|
|
6
|
+
type Props = {
|
|
7
|
+
items: Item[];
|
|
8
|
+
/** The open items' ids; bind it. */
|
|
9
|
+
open?: string[];
|
|
10
|
+
/** Let several items be open at once. */
|
|
11
|
+
multiple?: boolean;
|
|
12
|
+
/** The heading level of the item titles, to fit the page's outline. */
|
|
13
|
+
level?: 2 | 3 | 4 | 5 | 6;
|
|
14
|
+
class?: string;
|
|
15
|
+
/** Renders an item's content. */
|
|
16
|
+
content: Snippet<[string]>;
|
|
17
|
+
};
|
|
18
|
+
declare const Accordion: import("svelte").Component<Props, {}, "open">;
|
|
19
|
+
type Accordion = ReturnType<typeof Accordion>;
|
|
20
|
+
export default Accordion;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Snippet } from 'svelte';
|
|
3
|
+
|
|
4
|
+
type Props = {
|
|
5
|
+
tone?: 'info' | 'success' | 'warning' | 'danger';
|
|
6
|
+
title?: string;
|
|
7
|
+
/** Announce it when it appears: true for messages shown in response to something, false for ones on the page from the start. */
|
|
8
|
+
live?: boolean;
|
|
9
|
+
class?: string;
|
|
10
|
+
children?: Snippet;
|
|
11
|
+
};
|
|
12
|
+
let { tone = 'info', title, live = false, class: klass = '', children }: Props = $props();
|
|
13
|
+
const role = $derived(live ? (tone === 'danger' || tone === 'warning' ? 'alert' : 'status') : undefined);
|
|
14
|
+
const icons = {
|
|
15
|
+
info: 'M8 7v4M8 5h.01',
|
|
16
|
+
success: 'M5 8.5l2 2 4-4.5',
|
|
17
|
+
warning: 'M8 5v3.5M8 11h.01',
|
|
18
|
+
danger: 'M6 6l4 4M10 6l-4 4'
|
|
19
|
+
};
|
|
20
|
+
</script>
|
|
21
|
+
|
|
22
|
+
<div class="tern-alert {tone} {klass}" {role}>
|
|
23
|
+
<svg class="icon" viewBox="0 0 16 16" width="18" height="18" aria-hidden="true">
|
|
24
|
+
<circle cx="8" cy="8" r="6.5" fill="none" stroke="currentColor" stroke-width="1.4" />
|
|
25
|
+
<path d={icons[tone]} fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" />
|
|
26
|
+
</svg>
|
|
27
|
+
<div class="text">
|
|
28
|
+
{#if title}<p class="title">{title}</p>{/if}
|
|
29
|
+
{#if children}<div class="body">{@render children()}</div>{/if}
|
|
30
|
+
</div>
|
|
31
|
+
</div>
|
|
32
|
+
|
|
33
|
+
<style>
|
|
34
|
+
.tern-alert {
|
|
35
|
+
display: flex;
|
|
36
|
+
gap: 0.7rem;
|
|
37
|
+
padding: 0.85rem 1rem;
|
|
38
|
+
border-radius: var(--tern-radius);
|
|
39
|
+
border: 1px solid;
|
|
40
|
+
font-size: 0.875rem;
|
|
41
|
+
}
|
|
42
|
+
.icon {
|
|
43
|
+
flex: none;
|
|
44
|
+
margin-top: 0.05rem;
|
|
45
|
+
}
|
|
46
|
+
.title {
|
|
47
|
+
margin: 0;
|
|
48
|
+
font-weight: 650;
|
|
49
|
+
}
|
|
50
|
+
.body {
|
|
51
|
+
color: var(--tern-fg);
|
|
52
|
+
}
|
|
53
|
+
.title + .body {
|
|
54
|
+
margin-top: 0.2rem;
|
|
55
|
+
}
|
|
56
|
+
.info {
|
|
57
|
+
background: var(--tern-accent-soft);
|
|
58
|
+
border-color: color-mix(in oklch, var(--tern-accent) 30%, transparent);
|
|
59
|
+
color: var(--tern-accent);
|
|
60
|
+
}
|
|
61
|
+
.success {
|
|
62
|
+
background: var(--tern-success-soft);
|
|
63
|
+
border-color: color-mix(in oklch, var(--tern-success) 30%, transparent);
|
|
64
|
+
color: color-mix(in oklch, var(--tern-success) 80%, var(--tern-fg));
|
|
65
|
+
}
|
|
66
|
+
.warning {
|
|
67
|
+
background: var(--tern-warning-soft);
|
|
68
|
+
border-color: color-mix(in oklch, var(--tern-warning) 35%, transparent);
|
|
69
|
+
color: color-mix(in oklch, var(--tern-warning) 55%, var(--tern-fg));
|
|
70
|
+
}
|
|
71
|
+
.danger {
|
|
72
|
+
background: var(--tern-danger-soft);
|
|
73
|
+
border-color: color-mix(in oklch, var(--tern-danger) 30%, transparent);
|
|
74
|
+
color: color-mix(in oklch, var(--tern-danger) 85%, var(--tern-fg));
|
|
75
|
+
}
|
|
76
|
+
</style>
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
type Props = {
|
|
3
|
+
tone?: 'info' | 'success' | 'warning' | 'danger';
|
|
4
|
+
title?: string;
|
|
5
|
+
/** Announce it when it appears: true for messages shown in response to something, false for ones on the page from the start. */
|
|
6
|
+
live?: boolean;
|
|
7
|
+
class?: string;
|
|
8
|
+
children?: Snippet;
|
|
9
|
+
};
|
|
10
|
+
declare const Alert: import("svelte").Component<Props, {}, "">;
|
|
11
|
+
type Alert = ReturnType<typeof Alert>;
|
|
12
|
+
export default Alert;
|