@ternkit/tern 1.5.0 → 1.7.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.
Files changed (73) hide show
  1. package/dist/index.d.ts +2 -1
  2. package/dist/index.js +2 -1
  3. package/dist/runtime/builtin.d.ts +1 -0
  4. package/dist/runtime/builtin.js +182 -0
  5. package/dist/runtime/deferred.d.ts +1 -0
  6. package/dist/runtime/deferred.js +4 -2
  7. package/dist/runtime/i18n.svelte.d.ts +6 -0
  8. package/dist/runtime/i18n.svelte.js +10 -0
  9. package/dist/runtime/passkeys.d.ts +21 -0
  10. package/dist/runtime/passkeys.js +107 -0
  11. package/dist/runtime/router.svelte.js +2 -1
  12. package/dist/runtime/types.d.ts +2 -0
  13. package/dist/runtime/validate.d.ts +2 -1
  14. package/dist/runtime/validate.js +20 -18
  15. package/dist/server/index.d.ts +3 -1
  16. package/dist/server/index.js +8 -4
  17. package/dist/ui/AccountSection.svelte +75 -0
  18. package/dist/ui/AccountSection.svelte.d.ts +12 -0
  19. package/dist/ui/AlertDialog.svelte +5 -2
  20. package/dist/ui/AuthCard.svelte +64 -0
  21. package/dist/ui/AuthCard.svelte.d.ts +12 -0
  22. package/dist/ui/BarChart.svelte +2 -1
  23. package/dist/ui/Breadcrumb.svelte +2 -1
  24. package/dist/ui/Calendar.svelte +3 -2
  25. package/dist/ui/Carousel.svelte +4 -3
  26. package/dist/ui/ChartFrame.svelte +2 -1
  27. package/dist/ui/Combobox.svelte +6 -3
  28. package/dist/ui/Command.svelte +5 -1
  29. package/dist/ui/ConsentScreen.svelte +7 -6
  30. package/dist/ui/DataTable.svelte +7 -5
  31. package/dist/ui/DatePicker.svelte +8 -5
  32. package/dist/ui/Devices.svelte +50 -0
  33. package/dist/ui/Devices.svelte.d.ts +23 -0
  34. package/dist/ui/Dialog.svelte +2 -1
  35. package/dist/ui/DonutChart.svelte +3 -1
  36. package/dist/ui/Field.svelte +1 -1
  37. package/dist/ui/ForgotPassword.svelte +43 -0
  38. package/dist/ui/ForgotPassword.svelte.d.ts +16 -0
  39. package/dist/ui/Form.svelte +27 -4
  40. package/dist/ui/LinkedAccounts.svelte +54 -0
  41. package/dist/ui/LinkedAccounts.svelte.d.ts +27 -0
  42. package/dist/ui/NavigationMenu.svelte +3 -1
  43. package/dist/ui/Pagination.svelte +5 -3
  44. package/dist/ui/Passkeys.svelte +73 -0
  45. package/dist/ui/Passkeys.svelte.d.ts +21 -0
  46. package/dist/ui/PasswordInput.svelte +2 -1
  47. package/dist/ui/ResetPassword.svelte +54 -0
  48. package/dist/ui/ResetPassword.svelte.d.ts +23 -0
  49. package/dist/ui/Resizable.svelte +3 -1
  50. package/dist/ui/SelectMenu.svelte +5 -2
  51. package/dist/ui/Sidebar.svelte +5 -3
  52. package/dist/ui/SignIn.svelte +61 -13
  53. package/dist/ui/SignIn.svelte.d.ts +5 -1
  54. package/dist/ui/SignUp.svelte +10 -8
  55. package/dist/ui/Slider.svelte +2 -1
  56. package/dist/ui/Spinner.svelte +3 -1
  57. package/dist/ui/Switch.svelte +2 -1
  58. package/dist/ui/Table.svelte +3 -1
  59. package/dist/ui/Toaster.svelte +3 -2
  60. package/dist/ui/Toggle.svelte +2 -0
  61. package/dist/ui/ToggleGroup.svelte +2 -0
  62. package/dist/ui/TwoStep.svelte +155 -0
  63. package/dist/ui/TwoStep.svelte.d.ts +31 -0
  64. package/dist/ui/TwoStepCode.svelte +70 -0
  65. package/dist/ui/TwoStepCode.svelte.d.ts +16 -0
  66. package/dist/ui/accountDates.d.ts +1 -0
  67. package/dist/ui/accountDates.js +15 -0
  68. package/dist/ui/announce.d.ts +4 -0
  69. package/dist/ui/announce.js +14 -0
  70. package/dist/ui/index.d.ts +7 -0
  71. package/dist/ui/index.js +7 -0
  72. package/dist/vite/native.js +21 -4
  73. package/package.json +1 -1
@@ -0,0 +1,75 @@
1
+ <script lang="ts">
2
+ // The section the account settings lists share. Internal: not exported.
3
+ import type { Snippet } from 'svelte';
4
+
5
+ type Props = { id: string; title: string; description?: string; level?: 2 | 3 | 4; class?: string; children: Snippet };
6
+ let { id, title, description, level = 2, class: klass = '', children }: Props = $props();
7
+ </script>
8
+
9
+ <section class="tern-account {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-account {
19
+ display: grid;
20
+ gap: 1rem;
21
+ color: var(--tern-fg);
22
+ }
23
+ header {
24
+ display: grid;
25
+ gap: 0.25rem;
26
+ }
27
+ :is(h2, h3, h4) {
28
+ margin: 0;
29
+ font-size: 1.0625rem;
30
+ font-weight: 650;
31
+ }
32
+ header p,
33
+ .tern-account :global(.tern-account-empty) {
34
+ margin: 0;
35
+ color: var(--tern-muted);
36
+ font-size: 0.875rem;
37
+ }
38
+ .tern-account :global(ul) {
39
+ margin: 0;
40
+ padding: 0;
41
+ list-style: none;
42
+ border: 1px solid var(--tern-border);
43
+ border-radius: var(--tern-radius);
44
+ background: var(--tern-surface);
45
+ }
46
+ .tern-account :global(li) {
47
+ display: flex;
48
+ align-items: center;
49
+ justify-content: space-between;
50
+ gap: 1rem;
51
+ padding: 0.75rem 1rem;
52
+ }
53
+ .tern-account :global(li + li) {
54
+ border-top: 1px solid var(--tern-border);
55
+ }
56
+ .tern-account :global(.tern-account-text) {
57
+ display: grid;
58
+ gap: 0.15rem;
59
+ min-width: 0;
60
+ }
61
+ .tern-account :global(.tern-account-name) {
62
+ display: flex;
63
+ align-items: center;
64
+ gap: 0.5rem;
65
+ font-weight: 550;
66
+ overflow-wrap: anywhere;
67
+ }
68
+ .tern-account :global(.tern-account-meta) {
69
+ color: var(--tern-muted);
70
+ font-size: 0.8125rem;
71
+ }
72
+ .tern-account :global(.tern-form) {
73
+ display: contents;
74
+ }
75
+ </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?: 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 = 'Continue',
31
- cancelLabel = 'Cancel',
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;
@@ -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>Total</span></div>{/if}
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="Breadcrumb">
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}
@@ -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="Previous month" disabled={!canPrev} onclick={() => (focused = addMonths(focused, -1))}>
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="Next month" disabled={!canNext} onclick={() => (focused = addMonths(focused, 1))}>
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>
@@ -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="Previous slide" aria-controls="tern-carousel-{uid}" onclick={() => go(current - 1)}>
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="Next slide" aria-controls="tern-carousel-{uid}" onclick={() => go(current + 1)}>
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="Slides"
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="Legend">
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}
@@ -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 = 'No results',
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 ? 'Searching…' : empty}</div>
247
+ <div class="empty" role="status">{searching ? tb('tern.ui.combobox.searching') : empty}</div>
245
248
  {/each}
246
249
  {/if}
247
250
  </div>
@@ -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 = 'Commands', placeholder = 'Type a command or search…', empty = 'No results', dialog = false, open = $bindable(false), hotkey, class: klass = '' }: Props = $props();
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.client} wants to access your account</svelte:element>
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}>Not you?</a>{/if}
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">It will be able to:</p>
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">You can take this back at any time in your account settings.</p>
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">Deny</Button>
46
- <Button type="submit" name="decision" value="allow">Allow</Button>
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
 
@@ -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 = 'No rows match',
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="Search…"
119
- aria-label="Search {caption}"
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="Select all rows on this page" />
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">Actions</span></th>{/if}
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 = mode === 'range' ? 'Pick dates' : 'Pick a date',
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 ?? 'Choose a date'} align="start" {triggerId}>
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 ?? 'Choose a date'}
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;
@@ -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="Close" onclick={() => (open = false)}>
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 = 'total', size = 180, format = (v) => formatNumber(v), class: klass = '' }: Props = $props();
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
 
@@ -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>