@ternkit/tern 1.5.0 → 1.6.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 +2 -1
- package/dist/index.js +2 -1
- package/dist/runtime/builtin.d.ts +1 -0
- package/dist/runtime/builtin.js +140 -0
- package/dist/runtime/deferred.d.ts +1 -0
- package/dist/runtime/deferred.js +4 -2
- package/dist/runtime/i18n.svelte.d.ts +6 -0
- package/dist/runtime/i18n.svelte.js +10 -0
- package/dist/runtime/passkeys.d.ts +21 -0
- package/dist/runtime/passkeys.js +107 -0
- package/dist/runtime/router.svelte.js +2 -1
- package/dist/runtime/types.d.ts +2 -0
- package/dist/runtime/validate.d.ts +2 -1
- package/dist/runtime/validate.js +20 -18
- package/dist/server/index.d.ts +3 -1
- package/dist/server/index.js +8 -4
- package/dist/ui/AccountSection.svelte +75 -0
- package/dist/ui/AccountSection.svelte.d.ts +12 -0
- package/dist/ui/AlertDialog.svelte +5 -2
- package/dist/ui/AuthCard.svelte +64 -0
- package/dist/ui/AuthCard.svelte.d.ts +12 -0
- package/dist/ui/BarChart.svelte +2 -1
- package/dist/ui/Breadcrumb.svelte +2 -1
- package/dist/ui/Calendar.svelte +3 -2
- package/dist/ui/Carousel.svelte +4 -3
- package/dist/ui/ChartFrame.svelte +2 -1
- package/dist/ui/Combobox.svelte +6 -3
- package/dist/ui/Command.svelte +5 -1
- package/dist/ui/ConsentScreen.svelte +7 -6
- package/dist/ui/DataTable.svelte +7 -5
- package/dist/ui/DatePicker.svelte +8 -5
- package/dist/ui/Devices.svelte +50 -0
- package/dist/ui/Devices.svelte.d.ts +23 -0
- package/dist/ui/Dialog.svelte +2 -1
- package/dist/ui/DonutChart.svelte +3 -1
- package/dist/ui/Field.svelte +1 -1
- package/dist/ui/ForgotPassword.svelte +43 -0
- package/dist/ui/ForgotPassword.svelte.d.ts +16 -0
- package/dist/ui/Form.svelte +8 -3
- package/dist/ui/NavigationMenu.svelte +3 -1
- package/dist/ui/Pagination.svelte +5 -3
- package/dist/ui/Passkeys.svelte +73 -0
- package/dist/ui/Passkeys.svelte.d.ts +21 -0
- package/dist/ui/PasswordInput.svelte +2 -1
- package/dist/ui/ResetPassword.svelte +54 -0
- package/dist/ui/ResetPassword.svelte.d.ts +23 -0
- package/dist/ui/Resizable.svelte +3 -1
- package/dist/ui/SelectMenu.svelte +5 -2
- package/dist/ui/Sidebar.svelte +5 -3
- package/dist/ui/SignIn.svelte +48 -12
- package/dist/ui/SignIn.svelte.d.ts +4 -0
- package/dist/ui/SignUp.svelte +10 -8
- package/dist/ui/Slider.svelte +2 -1
- package/dist/ui/Spinner.svelte +3 -1
- package/dist/ui/Switch.svelte +2 -1
- package/dist/ui/Table.svelte +3 -1
- package/dist/ui/Toaster.svelte +3 -2
- package/dist/ui/Toggle.svelte +2 -0
- package/dist/ui/ToggleGroup.svelte +2 -0
- package/dist/ui/accountDates.d.ts +1 -0
- package/dist/ui/accountDates.js +15 -0
- package/dist/ui/announce.d.ts +4 -0
- package/dist/ui/announce.js +14 -0
- package/dist/ui/index.d.ts +4 -0
- package/dist/ui/index.js +4 -0
- package/dist/vite/native.js +21 -4
- package/package.json +1 -1
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
type Props = {
|
|
3
|
+
id: string;
|
|
4
|
+
title: string;
|
|
5
|
+
description?: string;
|
|
6
|
+
level?: 2 | 3 | 4;
|
|
7
|
+
class?: string;
|
|
8
|
+
children: Snippet;
|
|
9
|
+
};
|
|
10
|
+
declare const AccountSection: import("svelte").Component<Props, {}, "">;
|
|
11
|
+
type AccountSection = ReturnType<typeof AccountSection>;
|
|
12
|
+
export default AccountSection;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
+
import { tb } from '../runtime/i18n.svelte.js';
|
|
2
3
|
import Button from './Button.svelte';
|
|
3
4
|
import Dialog from './Dialog.svelte';
|
|
4
5
|
import type { Snippet } from 'svelte';
|
|
@@ -27,8 +28,8 @@
|
|
|
27
28
|
open = $bindable(false),
|
|
28
29
|
title,
|
|
29
30
|
description,
|
|
30
|
-
confirmLabel
|
|
31
|
-
cancelLabel
|
|
31
|
+
confirmLabel: confirmLabelProp,
|
|
32
|
+
cancelLabel: cancelLabelProp,
|
|
32
33
|
danger = false,
|
|
33
34
|
onconfirm,
|
|
34
35
|
oncancel,
|
|
@@ -36,6 +37,8 @@
|
|
|
36
37
|
confirmDisabled = false,
|
|
37
38
|
class: klass = ''
|
|
38
39
|
}: Props = $props();
|
|
40
|
+
const cancelLabel = $derived(cancelLabelProp ?? tb('tern.ui.alertDialog.cancel'));
|
|
41
|
+
const confirmLabel = $derived(confirmLabelProp ?? tb('tern.ui.alertDialog.confirm'));
|
|
39
42
|
|
|
40
43
|
const uid = $props.id();
|
|
41
44
|
let busy = $state(false);
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
// The card the account screens share. Internal: not exported.
|
|
3
|
+
import type { Snippet } from 'svelte';
|
|
4
|
+
|
|
5
|
+
type Props = { id: string; title: string; description?: string; level?: 1 | 2 | 3; class?: string; children: Snippet };
|
|
6
|
+
let { id, title, description, level = 1, class: klass = '', children }: Props = $props();
|
|
7
|
+
</script>
|
|
8
|
+
|
|
9
|
+
<section class="tern-auth {klass}" aria-labelledby={id}>
|
|
10
|
+
<header>
|
|
11
|
+
<svelte:element this={`h${level}`} {id}>{title}</svelte:element>
|
|
12
|
+
{#if description}<p>{description}</p>{/if}
|
|
13
|
+
</header>
|
|
14
|
+
{@render children()}
|
|
15
|
+
</section>
|
|
16
|
+
|
|
17
|
+
<style>
|
|
18
|
+
.tern-auth {
|
|
19
|
+
display: grid;
|
|
20
|
+
gap: 1.25rem;
|
|
21
|
+
width: 100%;
|
|
22
|
+
max-width: 24rem;
|
|
23
|
+
box-sizing: border-box;
|
|
24
|
+
padding: 2rem;
|
|
25
|
+
color: var(--tern-fg);
|
|
26
|
+
background: var(--tern-surface);
|
|
27
|
+
border: 1px solid var(--tern-border);
|
|
28
|
+
border-radius: calc(var(--tern-radius) + 4px);
|
|
29
|
+
box-shadow: var(--tern-shadow);
|
|
30
|
+
}
|
|
31
|
+
header {
|
|
32
|
+
display: grid;
|
|
33
|
+
gap: 0.35rem;
|
|
34
|
+
}
|
|
35
|
+
:is(h1, h2, h3) {
|
|
36
|
+
margin: 0;
|
|
37
|
+
font-size: 1.375rem;
|
|
38
|
+
font-weight: 650;
|
|
39
|
+
letter-spacing: -0.02em;
|
|
40
|
+
}
|
|
41
|
+
header p {
|
|
42
|
+
margin: 0;
|
|
43
|
+
color: var(--tern-muted);
|
|
44
|
+
font-size: 0.875rem;
|
|
45
|
+
}
|
|
46
|
+
.tern-auth :global(a) {
|
|
47
|
+
color: var(--tern-accent);
|
|
48
|
+
font-weight: 550;
|
|
49
|
+
}
|
|
50
|
+
.tern-auth :global(a:focus-visible) {
|
|
51
|
+
outline: 2px solid var(--tern-ring);
|
|
52
|
+
outline-offset: 2px;
|
|
53
|
+
border-radius: 2px;
|
|
54
|
+
}
|
|
55
|
+
.tern-auth :global(.tern-form > .tern-btn) {
|
|
56
|
+
width: 100%;
|
|
57
|
+
}
|
|
58
|
+
.tern-auth :global(.tern-auth-note) {
|
|
59
|
+
margin: 0;
|
|
60
|
+
text-align: center;
|
|
61
|
+
font-size: 0.875rem;
|
|
62
|
+
color: var(--tern-muted);
|
|
63
|
+
}
|
|
64
|
+
</style>
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
type Props = {
|
|
3
|
+
id: string;
|
|
4
|
+
title: string;
|
|
5
|
+
description?: string;
|
|
6
|
+
level?: 1 | 2 | 3;
|
|
7
|
+
class?: string;
|
|
8
|
+
children: Snippet;
|
|
9
|
+
};
|
|
10
|
+
declare const AuthCard: import("svelte").Component<Props, {}, "">;
|
|
11
|
+
type AuthCard = ReturnType<typeof AuthCard>;
|
|
12
|
+
export default AuthCard;
|
package/dist/ui/BarChart.svelte
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
<script lang="ts" generics="Row extends object">
|
|
2
|
+
import { tb } from '../runtime/i18n.svelte.js';
|
|
2
3
|
import ChartFrame from './ChartFrame.svelte';
|
|
3
4
|
import { lastIndex, formatNumber, niceTicks, seriesColor, textWidth, type Series } from './chart.js';
|
|
4
5
|
|
|
@@ -163,7 +164,7 @@
|
|
|
163
164
|
{#each series as s, k (s.key)}
|
|
164
165
|
<div class="row"><span class="swatch" style:background={seriesColor(k)}></span><strong>{format(values[active]![k]!)}</strong><span>{s.label}</span></div>
|
|
165
166
|
{/each}
|
|
166
|
-
{#if stacked && series.length > 1}<div class="row"><strong>{format(totals[active]!)}</strong><span>
|
|
167
|
+
{#if stacked && series.length > 1}<div class="row"><strong>{format(totals[active]!)}</strong><span>{tb('tern.ui.barChart.total')}</span></div>{/if}
|
|
167
168
|
</div>
|
|
168
169
|
{/if}
|
|
169
170
|
</div>
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
+
import { tb } from '../runtime/i18n.svelte.js';
|
|
2
3
|
import Menu from './Menu.svelte';
|
|
3
4
|
|
|
4
5
|
type Crumb = { label: string; href?: string };
|
|
@@ -29,7 +30,7 @@
|
|
|
29
30
|
</li>
|
|
30
31
|
{/snippet}
|
|
31
32
|
|
|
32
|
-
<nav class="tern-breadcrumb {klass}" aria-label=
|
|
33
|
+
<nav class="tern-breadcrumb {klass}" aria-label={tb('tern.ui.breadcrumb.label')}>
|
|
33
34
|
<ol>
|
|
34
35
|
{#each head as item, i (i)}{@render crumb(item, !collapsed && i === items.length - 1)}{/each}
|
|
35
36
|
{#if collapsed}
|
package/dist/ui/Calendar.svelte
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
+
import { tb } from '../runtime/i18n.svelte.js';
|
|
2
3
|
import { tick } from 'svelte';
|
|
3
4
|
import { addDays, addMonths, format, localeWeekStart, monthGrid, startOfMonth, today, type ISODate } from './dates.js';
|
|
4
5
|
|
|
@@ -108,11 +109,11 @@
|
|
|
108
109
|
|
|
109
110
|
<div class="tern-calendar {klass}" role="group" aria-label={label}>
|
|
110
111
|
<div class="header">
|
|
111
|
-
<button type="button" class="nav" aria-label=
|
|
112
|
+
<button type="button" class="nav" aria-label={tb('tern.ui.calendar.previousMonth')} disabled={!canPrev} onclick={() => (focused = addMonths(focused, -1))}>
|
|
112
113
|
<svg aria-hidden="true" viewBox="0 0 16 16" width="16" height="16"><path d="M10 4l-4 4 4 4" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" /></svg>
|
|
113
114
|
</button>
|
|
114
115
|
<div class="title" id="{id}-title" aria-live="polite">{format(focused, locale, { month: 'long', year: 'numeric' })}</div>
|
|
115
|
-
<button type="button" class="nav" aria-label=
|
|
116
|
+
<button type="button" class="nav" aria-label={tb('tern.ui.calendar.nextMonth')} disabled={!canNext} onclick={() => (focused = addMonths(focused, 1))}>
|
|
116
117
|
<svg aria-hidden="true" viewBox="0 0 16 16" width="16" height="16"><path d="M6 4l4 4-4 4" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" /></svg>
|
|
117
118
|
</button>
|
|
118
119
|
</div>
|
package/dist/ui/Carousel.svelte
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
<script lang="ts" generics="T">
|
|
2
|
+
import { tb } from '../runtime/i18n.svelte.js';
|
|
2
3
|
import type { Snippet } from 'svelte';
|
|
3
4
|
|
|
4
5
|
type Props = {
|
|
@@ -63,10 +64,10 @@
|
|
|
63
64
|
{/if}
|
|
64
65
|
</button>
|
|
65
66
|
{/if}
|
|
66
|
-
<button type="button" class="control" aria-label=
|
|
67
|
+
<button type="button" class="control" aria-label={tb('tern.ui.carousel.previous')} aria-controls="tern-carousel-{uid}" onclick={() => go(current - 1)}>
|
|
67
68
|
<svg aria-hidden="true" viewBox="0 0 16 16" width="16" height="16"><path d="M10 4l-4 4 4 4" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" /></svg>
|
|
68
69
|
</button>
|
|
69
|
-
<button type="button" class="control" aria-label=
|
|
70
|
+
<button type="button" class="control" aria-label={tb('tern.ui.carousel.next')} aria-controls="tern-carousel-{uid}" onclick={() => go(current + 1)}>
|
|
70
71
|
<svg aria-hidden="true" viewBox="0 0 16 16" width="16" height="16"><path d="M6 4l4 4-4 4" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" /></svg>
|
|
71
72
|
</button>
|
|
72
73
|
</div>
|
|
@@ -76,7 +77,7 @@
|
|
|
76
77
|
class="track"
|
|
77
78
|
id="tern-carousel-{uid}"
|
|
78
79
|
role="group"
|
|
79
|
-
aria-label=
|
|
80
|
+
aria-label={tb('tern.ui.carousel.label')}
|
|
80
81
|
tabindex="0"
|
|
81
82
|
aria-live={playing && !paused ? 'off' : 'polite'}
|
|
82
83
|
{onscroll}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
+
import { tb } from '../runtime/i18n.svelte.js';
|
|
2
3
|
import type { Snippet } from 'svelte';
|
|
3
4
|
import Collapsible from './Collapsible.svelte';
|
|
4
5
|
import ScrollArea from './ScrollArea.svelte';
|
|
@@ -26,7 +27,7 @@
|
|
|
26
27
|
{#if description}<p class="description">{description}</p>{/if}
|
|
27
28
|
</figcaption>
|
|
28
29
|
{#if legend.length >= 2}
|
|
29
|
-
<ul class="legend" aria-label=
|
|
30
|
+
<ul class="legend" aria-label={tb('tern.ui.chart.legend')}>
|
|
30
31
|
{#each legend as item (item.label)}
|
|
31
32
|
<li><span class="key {item.kind}" style:background={item.color} aria-hidden="true"></span>{item.label}</li>
|
|
32
33
|
{/each}
|
package/dist/ui/Combobox.svelte
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
+
import { announce } from './announce.js';
|
|
3
|
+
import { tb } from '../runtime/i18n.svelte.js';
|
|
2
4
|
import { getField } from './field.js';
|
|
3
5
|
import { float } from './floating.js';
|
|
4
6
|
import { grouped, matches, type Option } from './options.js';
|
|
@@ -31,13 +33,14 @@
|
|
|
31
33
|
placeholder,
|
|
32
34
|
label,
|
|
33
35
|
name: nameProp,
|
|
34
|
-
empty
|
|
36
|
+
empty: emptyProp,
|
|
35
37
|
onsearch,
|
|
36
38
|
debounce = 200,
|
|
37
39
|
disabled = false,
|
|
38
40
|
class: klass = '',
|
|
39
41
|
onchange
|
|
40
42
|
}: Props = $props();
|
|
43
|
+
const empty = $derived(emptyProp ?? tb('tern.ui.combobox.empty'));
|
|
41
44
|
const name = $derived(nameProp ?? field?.name);
|
|
42
45
|
|
|
43
46
|
const uid = $props.id();
|
|
@@ -202,7 +205,7 @@
|
|
|
202
205
|
<span class="icon" aria-hidden="true">
|
|
203
206
|
{#if searching}<Spinner size="0.875rem" label="Searching" />{:else}<svg viewBox="0 0 16 16" width="14" height="14"><path d="M5 6.5l3-3 3 3M5 9.5l3 3 3-3" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" /></svg>{/if}
|
|
204
207
|
</span>
|
|
205
|
-
{#if name}<input type="hidden" {name} value={value ?? ''} />{/if}
|
|
208
|
+
{#if name}<input type="hidden" {name} value={value ?? ''} use:announce={value} />{/if}
|
|
206
209
|
<div bind:this={list} id="{id}-list" class="list" role="listbox" aria-label={label} aria-labelledby={field?.labelId} popover="manual" class:open>
|
|
207
210
|
{#if open}
|
|
208
211
|
{#each grouped(shown) as g, gi (gi)}
|
|
@@ -241,7 +244,7 @@
|
|
|
241
244
|
{@render items()}
|
|
242
245
|
{/if}
|
|
243
246
|
{:else}
|
|
244
|
-
<div class="empty" role="status">{searching ? '
|
|
247
|
+
<div class="empty" role="status">{searching ? tb('tern.ui.combobox.searching') : empty}</div>
|
|
245
248
|
{/each}
|
|
246
249
|
{/if}
|
|
247
250
|
</div>
|
package/dist/ui/Command.svelte
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
+
import { tb } from '../runtime/i18n.svelte.js';
|
|
2
3
|
import { matches } from './options.js';
|
|
3
4
|
import { trapTab } from './floating.js';
|
|
4
5
|
|
|
@@ -28,7 +29,10 @@
|
|
|
28
29
|
hotkey?: string;
|
|
29
30
|
class?: string;
|
|
30
31
|
};
|
|
31
|
-
let { items, label
|
|
32
|
+
let { items, label: labelProp, placeholder: placeholderProp, empty: emptyProp, dialog = false, open = $bindable(false), hotkey, class: klass = '' }: Props = $props();
|
|
33
|
+
const empty = $derived(emptyProp ?? tb('tern.ui.command.empty'));
|
|
34
|
+
const placeholder = $derived(placeholderProp ?? tb('tern.ui.command.placeholder'));
|
|
35
|
+
const label = $derived(labelProp ?? tb('tern.ui.command.label'));
|
|
32
36
|
|
|
33
37
|
const uid = $props.id();
|
|
34
38
|
const id = `tern-command-${uid}`;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
+
import { tb } from '../runtime/i18n.svelte.js';
|
|
2
3
|
import Avatar from './Avatar.svelte';
|
|
3
4
|
import Button from './Button.svelte';
|
|
4
5
|
|
|
@@ -17,7 +18,7 @@
|
|
|
17
18
|
|
|
18
19
|
<!-- Asks the signed-in user to let another app use their account. The form posts back to plugins/oidc, which checks the request again. -->
|
|
19
20
|
<section class="tern-consent {klass}" aria-labelledby="tern-consent-title">
|
|
20
|
-
<svelte:element this={`h${level}`} id="tern-consent-title">{consent.
|
|
21
|
+
<svelte:element this={`h${level}`} id="tern-consent-title">{tb('tern.ui.consent.title', { client: consent.client })}</svelte:element>
|
|
21
22
|
{#if user}
|
|
22
23
|
<div class="user">
|
|
23
24
|
<Avatar name={user.name} size="2rem" />
|
|
@@ -25,11 +26,11 @@
|
|
|
25
26
|
<div class="name">{user.name}</div>
|
|
26
27
|
{#if user.email}<div class="email">{user.email}</div>{/if}
|
|
27
28
|
</div>
|
|
28
|
-
{#if switchHref}<a href={switchHref}>
|
|
29
|
+
{#if switchHref}<a href={switchHref}>{tb('tern.ui.consent.notYou')}</a>{/if}
|
|
29
30
|
</div>
|
|
30
31
|
{/if}
|
|
31
32
|
<div>
|
|
32
|
-
<p id="tern-consent-scopes">
|
|
33
|
+
<p id="tern-consent-scopes">{tb('tern.ui.consent.scopes')}</p>
|
|
33
34
|
<ul aria-labelledby="tern-consent-scopes">
|
|
34
35
|
{#each consent.scopes as scope (scope)}
|
|
35
36
|
<li>
|
|
@@ -39,11 +40,11 @@
|
|
|
39
40
|
{/each}
|
|
40
41
|
</ul>
|
|
41
42
|
</div>
|
|
42
|
-
<p class="note">
|
|
43
|
+
<p class="note">{tb('tern.ui.consent.note')}</p>
|
|
43
44
|
<form method="POST" action={consent.action}>
|
|
44
45
|
{#each Object.entries(consent.params) as [k, v] (k)}<input type="hidden" name={k} value={v} />{/each}
|
|
45
|
-
<Button type="submit" name="decision" value="deny" variant="secondary">
|
|
46
|
-
<Button type="submit" name="decision" value="allow">
|
|
46
|
+
<Button type="submit" name="decision" value="deny" variant="secondary">{tb('tern.ui.consent.deny')}</Button>
|
|
47
|
+
<Button type="submit" name="decision" value="allow">{tb('tern.ui.consent.allow')}</Button>
|
|
47
48
|
</form>
|
|
48
49
|
</section>
|
|
49
50
|
|
package/dist/ui/DataTable.svelte
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
<script lang="ts" generics="Row">
|
|
2
|
+
import { tb } from '../runtime/i18n.svelte.js';
|
|
2
3
|
import type { Snippet } from 'svelte';
|
|
3
4
|
import Checkbox from './Checkbox.svelte';
|
|
4
5
|
import Input from './Input.svelte';
|
|
@@ -49,10 +50,11 @@
|
|
|
49
50
|
total,
|
|
50
51
|
toolbar,
|
|
51
52
|
actions,
|
|
52
|
-
empty
|
|
53
|
+
empty: emptyProp,
|
|
53
54
|
loading = false,
|
|
54
55
|
class: klass = ''
|
|
55
56
|
}: Props = $props();
|
|
57
|
+
const empty = $derived(emptyProp ?? tb('tern.ui.dataTable.empty'));
|
|
56
58
|
|
|
57
59
|
const uid = $props.id();
|
|
58
60
|
let sort = $state<DataQuery['sort']>(null);
|
|
@@ -115,8 +117,8 @@
|
|
|
115
117
|
<div class="search">
|
|
116
118
|
<Input
|
|
117
119
|
type="search"
|
|
118
|
-
placeholder=
|
|
119
|
-
aria-label=
|
|
120
|
+
placeholder={tb('tern.ui.dataTable.search')}
|
|
121
|
+
aria-label={tb('tern.ui.dataTable.searchLabel', { caption })}
|
|
120
122
|
aria-controls="tern-datatable-{uid}"
|
|
121
123
|
bind:value={search}
|
|
122
124
|
oninput={() => (page = 1)}
|
|
@@ -133,7 +135,7 @@
|
|
|
133
135
|
<tr>
|
|
134
136
|
{#if selectable}
|
|
135
137
|
<th scope="col" class="select">
|
|
136
|
-
<Checkbox checked={allShown} indeterminate={someShown} onchange={toggleAll} aria-label=
|
|
138
|
+
<Checkbox checked={allShown} indeterminate={someShown} onchange={toggleAll} aria-label={tb('tern.ui.dataTable.selectAll')} />
|
|
137
139
|
</th>
|
|
138
140
|
{/if}
|
|
139
141
|
{#each columns as col (col.key)}
|
|
@@ -150,7 +152,7 @@
|
|
|
150
152
|
{/if}
|
|
151
153
|
</th>
|
|
152
154
|
{/each}
|
|
153
|
-
{#if actions}<th scope="col"><span class="tern-sr-only">
|
|
155
|
+
{#if actions}<th scope="col"><span class="tern-sr-only">{tb('tern.ui.dataTable.actions')}</span></th>{/if}
|
|
154
156
|
</tr>
|
|
155
157
|
</thead>
|
|
156
158
|
<tbody>
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
+
import { announce } from './announce.js';
|
|
3
|
+
import { tb } from '../runtime/i18n.svelte.js';
|
|
2
4
|
import Calendar, { type DateRange } from './Calendar.svelte';
|
|
3
5
|
import Popover from './Popover.svelte';
|
|
4
6
|
import { format, type ISODate } from './dates.js';
|
|
@@ -28,7 +30,7 @@
|
|
|
28
30
|
let {
|
|
29
31
|
mode = 'single',
|
|
30
32
|
value = $bindable(mode === 'range' ? { start: null, end: null } : null),
|
|
31
|
-
placeholder
|
|
33
|
+
placeholder: placeholderProp,
|
|
32
34
|
label,
|
|
33
35
|
name: nameProp,
|
|
34
36
|
min,
|
|
@@ -41,6 +43,7 @@
|
|
|
41
43
|
class: klass = '',
|
|
42
44
|
onchange
|
|
43
45
|
}: Props = $props();
|
|
46
|
+
const placeholder = $derived(placeholderProp ?? (mode === 'range' ? tb('tern.ui.datePicker.placeholderRange') : tb('tern.ui.datePicker.placeholder')));
|
|
44
47
|
const name = $derived(nameProp ?? field?.name);
|
|
45
48
|
|
|
46
49
|
const uid = $props.id();
|
|
@@ -56,7 +59,7 @@
|
|
|
56
59
|
});
|
|
57
60
|
</script>
|
|
58
61
|
|
|
59
|
-
<Popover bind:open label={label ?? '
|
|
62
|
+
<Popover bind:open label={label ?? tb('tern.ui.datePicker.label')} align="start" {triggerId}>
|
|
60
63
|
{#snippet trigger(props)}
|
|
61
64
|
<button
|
|
62
65
|
type="button"
|
|
@@ -79,7 +82,7 @@
|
|
|
79
82
|
{isDisabled}
|
|
80
83
|
{locale}
|
|
81
84
|
{weekStart}
|
|
82
|
-
label={label ?? '
|
|
85
|
+
label={label ?? tb('tern.ui.datePicker.label')}
|
|
83
86
|
onselect={(v) => {
|
|
84
87
|
onchange?.(v);
|
|
85
88
|
open = false;
|
|
@@ -89,9 +92,9 @@
|
|
|
89
92
|
</Popover>
|
|
90
93
|
{#if name}
|
|
91
94
|
{#if mode === 'single'}
|
|
92
|
-
<input type="hidden" {name} value={(value as ISODate | null) ?? ''} />
|
|
95
|
+
<input type="hidden" {name} value={(value as ISODate | null) ?? ''} use:announce={value} />
|
|
93
96
|
{:else}
|
|
94
|
-
<input type="hidden" {name} value={(value as DateRange | null)?.start ?? ''} />
|
|
97
|
+
<input type="hidden" {name} value={(value as DateRange | null)?.start ?? ''} use:announce={value} />
|
|
95
98
|
<input type="hidden" name="{name}_end" value={(value as DateRange | null)?.end ?? ''} />
|
|
96
99
|
{/if}
|
|
97
100
|
{/if}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { tb } from '../runtime/i18n.svelte.js';
|
|
3
|
+
import Badge from './Badge.svelte';
|
|
4
|
+
import AccountSection from './AccountSection.svelte';
|
|
5
|
+
import Button from './Button.svelte';
|
|
6
|
+
import Form from './Form.svelte';
|
|
7
|
+
import { when } from './accountDates.js';
|
|
8
|
+
|
|
9
|
+
type Session = { id: string; device: string; ip: string; createdAt: string; lastSeen: string; current: boolean };
|
|
10
|
+
type Props = {
|
|
11
|
+
title?: string;
|
|
12
|
+
description?: string;
|
|
13
|
+
level?: 2 | 3 | 4;
|
|
14
|
+
/** From acc.Sessions: the current one first. */
|
|
15
|
+
sessions: Session[];
|
|
16
|
+
/** The action that calls acc.Revoke with the posted id. */
|
|
17
|
+
action?: string;
|
|
18
|
+
/** The action that calls acc.SignOutOthers; then a button signs out every other device. */
|
|
19
|
+
othersAction?: string;
|
|
20
|
+
class?: string;
|
|
21
|
+
};
|
|
22
|
+
let { title, description, level = 2, sessions, action, othersAction, class: klass = '' }: Props = $props();
|
|
23
|
+
const others = $derived(sessions.filter((s) => !s.current).length);
|
|
24
|
+
</script>
|
|
25
|
+
|
|
26
|
+
<AccountSection id="tern-devices-title" title={title ?? tb('tern.ui.devices.title')} description={description ?? tb('tern.ui.devices.description')} {level} class={klass}>
|
|
27
|
+
<ul>
|
|
28
|
+
{#each sessions as s (s.id)}
|
|
29
|
+
<li>
|
|
30
|
+
<div class="tern-account-text">
|
|
31
|
+
<span class="tern-account-name">{s.device}{#if s.current}<Badge tone="success">{tb('tern.ui.devices.current')}</Badge>{/if}</span>
|
|
32
|
+
<span class="tern-account-meta">{tb('tern.ui.devices.lastSeen', { when: when(s.lastSeen) })}{s.ip ? ` · ${s.ip}` : ''}</span>
|
|
33
|
+
</div>
|
|
34
|
+
{#if action}
|
|
35
|
+
<Form {action} reset={false}>
|
|
36
|
+
<input type="hidden" name="id" value={s.id} />
|
|
37
|
+
<Button type="submit" variant={s.current ? 'secondary' : 'ghost'} size="sm">{tb('tern.ui.devices.signOut')}</Button>
|
|
38
|
+
</Form>
|
|
39
|
+
{/if}
|
|
40
|
+
</li>
|
|
41
|
+
{/each}
|
|
42
|
+
</ul>
|
|
43
|
+
{#if othersAction && others > 0}
|
|
44
|
+
<div>
|
|
45
|
+
<Form action={othersAction} reset={false}>
|
|
46
|
+
<Button type="submit" variant="danger" size="sm">{tb('tern.ui.devices.signOutOthers')}</Button>
|
|
47
|
+
</Form>
|
|
48
|
+
</div>
|
|
49
|
+
{/if}
|
|
50
|
+
</AccountSection>
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
type Session = {
|
|
2
|
+
id: string;
|
|
3
|
+
device: string;
|
|
4
|
+
ip: string;
|
|
5
|
+
createdAt: string;
|
|
6
|
+
lastSeen: string;
|
|
7
|
+
current: boolean;
|
|
8
|
+
};
|
|
9
|
+
type Props = {
|
|
10
|
+
title?: string;
|
|
11
|
+
description?: string;
|
|
12
|
+
level?: 2 | 3 | 4;
|
|
13
|
+
/** From acc.Sessions: the current one first. */
|
|
14
|
+
sessions: Session[];
|
|
15
|
+
/** The action that calls acc.Revoke with the posted id. */
|
|
16
|
+
action?: string;
|
|
17
|
+
/** The action that calls acc.SignOutOthers; then a button signs out every other device. */
|
|
18
|
+
othersAction?: string;
|
|
19
|
+
class?: string;
|
|
20
|
+
};
|
|
21
|
+
declare const Devices: import("svelte").Component<Props, {}, "">;
|
|
22
|
+
type Devices = ReturnType<typeof Devices>;
|
|
23
|
+
export default Devices;
|
package/dist/ui/Dialog.svelte
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
+
import { tb } from '../runtime/i18n.svelte.js';
|
|
2
3
|
import type { Snippet } from 'svelte';
|
|
3
4
|
import { trapTab } from './floating.js';
|
|
4
5
|
|
|
@@ -101,7 +102,7 @@
|
|
|
101
102
|
<header>
|
|
102
103
|
<h2 id="tern-dialog-{uid}-title">{title}</h2>
|
|
103
104
|
{#if closeButton}
|
|
104
|
-
<button type="button" class="close" aria-label=
|
|
105
|
+
<button type="button" class="close" aria-label={tb('tern.ui.dialog.close')} onclick={() => (open = false)}>
|
|
105
106
|
<svg viewBox="0 0 16 16" width="16" height="16" aria-hidden="true"><path d="M4 4l8 8M12 4l-8 8" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" /></svg>
|
|
106
107
|
</button>
|
|
107
108
|
{/if}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
<script lang="ts" generics="Row extends object">
|
|
2
|
+
import { tb } from '../runtime/i18n.svelte.js';
|
|
2
3
|
import ChartFrame from './ChartFrame.svelte';
|
|
3
4
|
import { formatNumber, seriesColor } from './chart.js';
|
|
4
5
|
|
|
@@ -19,7 +20,8 @@
|
|
|
19
20
|
format?: (v: number) => string;
|
|
20
21
|
class?: string;
|
|
21
22
|
};
|
|
22
|
-
let { data, label, value, title, description, level, max = 6, totalLabel
|
|
23
|
+
let { data, label, value, title, description, level, max = 6, totalLabel: totalLabelProp, size = 180, format = (v) => formatNumber(v), class: klass = '' }: Props = $props();
|
|
24
|
+
const totalLabel = $derived(totalLabelProp ?? tb('tern.ui.donutChart.total'));
|
|
23
25
|
|
|
24
26
|
let active = $state<number | null>(null);
|
|
25
27
|
|
package/dist/ui/Field.svelte
CHANGED
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
});
|
|
55
55
|
</script>
|
|
56
56
|
|
|
57
|
-
<div class="tern-field {klass}" class:disabled>
|
|
57
|
+
<div class="tern-field {klass}" class:disabled data-tern-field={name}>
|
|
58
58
|
<!-- svelte-ignore a11y_click_events_have_key_events, a11y_no_noninteractive_element_interactions -->
|
|
59
59
|
<label for={custom ? undefined : id} id="{id}-label" onclick={custom ? () => document.getElementById(id)?.focus() : undefined}>
|
|
60
60
|
{label}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { tb } from '../runtime/i18n.svelte.js';
|
|
3
|
+
import type { FormField } from '../runtime/validate.js';
|
|
4
|
+
import Alert from './Alert.svelte';
|
|
5
|
+
import AuthCard from './AuthCard.svelte';
|
|
6
|
+
import Button from './Button.svelte';
|
|
7
|
+
import Field from './Field.svelte';
|
|
8
|
+
import Form from './Form.svelte';
|
|
9
|
+
import Input from './Input.svelte';
|
|
10
|
+
|
|
11
|
+
type Props = {
|
|
12
|
+
title?: string;
|
|
13
|
+
description?: string;
|
|
14
|
+
level?: 1 | 2 | 3;
|
|
15
|
+
/** The action that calls acc.RequestPasswordReset with the posted email. */
|
|
16
|
+
action?: string;
|
|
17
|
+
rules?: FormField[];
|
|
18
|
+
/** The page's form prop. An action returning { sent: true } shows that the email is on its way. */
|
|
19
|
+
form?: unknown;
|
|
20
|
+
signInHref?: string;
|
|
21
|
+
class?: string;
|
|
22
|
+
};
|
|
23
|
+
let { title, description, level = 1, action, rules, form, signInHref, class: klass = '' }: Props = $props();
|
|
24
|
+
|
|
25
|
+
let submitting = $state(false);
|
|
26
|
+
let sent = $state(false);
|
|
27
|
+
// Without JavaScript, the action returns { sent: true } and it shows through the form prop.
|
|
28
|
+
const done = $derived(sent || (!!form && typeof form === 'object' && (form as { sent?: unknown }).sent === true));
|
|
29
|
+
</script>
|
|
30
|
+
|
|
31
|
+
<AuthCard id="tern-forgot-title" title={title ?? tb('tern.ui.forgotPassword.title')} description={done ? undefined : (description ?? tb('tern.ui.forgotPassword.description'))} {level} class={klass}>
|
|
32
|
+
{#if done}
|
|
33
|
+
<Alert tone="success" live>{tb('tern.ui.forgotPassword.sent')}</Alert>
|
|
34
|
+
{:else}
|
|
35
|
+
<Form {rules} {form} {action} bind:submitting reset={false} onsuccess={() => (sent = true)}>
|
|
36
|
+
<Field label={tb('tern.ui.forgotPassword.email')} name="email">
|
|
37
|
+
<Input type="email" autocomplete="username" spellcheck="false" />
|
|
38
|
+
</Field>
|
|
39
|
+
<Button type="submit" loading={submitting}>{tb('tern.ui.forgotPassword.submit')}</Button>
|
|
40
|
+
</Form>
|
|
41
|
+
{/if}
|
|
42
|
+
{#if signInHref}<p class="tern-auth-note"><a href={signInHref}>{tb('tern.ui.forgotPassword.back')}</a></p>{/if}
|
|
43
|
+
</AuthCard>
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { FormField } from '../runtime/validate.js';
|
|
2
|
+
type Props = {
|
|
3
|
+
title?: string;
|
|
4
|
+
description?: string;
|
|
5
|
+
level?: 1 | 2 | 3;
|
|
6
|
+
/** The action that calls acc.RequestPasswordReset with the posted email. */
|
|
7
|
+
action?: string;
|
|
8
|
+
rules?: FormField[];
|
|
9
|
+
/** The page's form prop. An action returning { sent: true } shows that the email is on its way. */
|
|
10
|
+
form?: unknown;
|
|
11
|
+
signInHref?: string;
|
|
12
|
+
class?: string;
|
|
13
|
+
};
|
|
14
|
+
declare const ForgotPassword: import("svelte").Component<Props, {}, "">;
|
|
15
|
+
type ForgotPassword = ReturnType<typeof ForgotPassword>;
|
|
16
|
+
export default ForgotPassword;
|
package/dist/ui/Form.svelte
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
+
import { tb } from '../runtime/i18n.svelte.js';
|
|
2
3
|
import { tick, type Snippet } from 'svelte';
|
|
3
4
|
import type { HTMLFormAttributes } from 'svelte/elements';
|
|
4
5
|
import { enhance as enhanceAction } from '../runtime/forms.js';
|
|
@@ -67,14 +68,18 @@
|
|
|
67
68
|
// A field and the fields that must match it, such as a password and its confirmation.
|
|
68
69
|
const related = (name: string) => [name, ...rules.filter((f) => f.rules?.some((r) => r.name === 'same' && r.arg === name)).map((f) => f.name)];
|
|
69
70
|
|
|
71
|
+
// A control's name: its own, or for controls posting through hidden
|
|
72
|
+
// inputs, its Field's.
|
|
73
|
+
const nameOf = (t: EventTarget | null) => (t as HTMLInputElement | null)?.name || (t as HTMLElement | null)?.closest?.('[data-tern-field]')?.getAttribute('data-tern-field') || '';
|
|
74
|
+
|
|
70
75
|
function oninput(e: Event) {
|
|
71
|
-
const name = (e.target
|
|
76
|
+
const name = nameOf(e.target);
|
|
72
77
|
if (!name) return;
|
|
73
78
|
edited = { ...edited, [name]: true };
|
|
74
79
|
if (touched[name] || submitted) check(related(name).filter((n) => touched[n] || submitted || n === name));
|
|
75
80
|
}
|
|
76
81
|
function onfocusout(e: FocusEvent) {
|
|
77
|
-
const name = (e.target
|
|
82
|
+
const name = nameOf(e.target);
|
|
78
83
|
// Don't complain about a field the user only passed through.
|
|
79
84
|
if (!name || !byName.has(name) || (!edited[name] && !submitted)) return;
|
|
80
85
|
touched = { ...touched, [name]: true };
|
|
@@ -91,7 +96,7 @@
|
|
|
91
96
|
summary = '';
|
|
92
97
|
return true;
|
|
93
98
|
}
|
|
94
|
-
summary =
|
|
99
|
+
summary = tb('tern.ui.form.summary', { n: names.length });
|
|
95
100
|
void tick().then(() => el?.querySelector<HTMLElement>('[aria-invalid="true"]')?.focus());
|
|
96
101
|
return false;
|
|
97
102
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
+
import { tb } from '../runtime/i18n.svelte.js';
|
|
2
3
|
import { page } from '../runtime/state.svelte.js';
|
|
3
4
|
import { float } from './floating.js';
|
|
4
5
|
|
|
@@ -9,7 +10,8 @@
|
|
|
9
10
|
label?: string;
|
|
10
11
|
class?: string;
|
|
11
12
|
};
|
|
12
|
-
let { items, label
|
|
13
|
+
let { items, label: labelProp, class: klass = '' }: Props = $props();
|
|
14
|
+
const label = $derived(labelProp ?? tb('tern.ui.navigationMenu.label'));
|
|
13
15
|
|
|
14
16
|
const uid = $props.id();
|
|
15
17
|
let open = $state<number | null>(null);
|