@ternkit/tern 1.2.1 → 1.4.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 +2 -1
- package/dist/index.js +2 -1
- package/dist/runtime/validate.d.ts +31 -0
- package/dist/runtime/validate.js +168 -0
- package/dist/server/intl.js +55 -12
- 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/AlertDialog.svelte +79 -0
- package/dist/ui/AlertDialog.svelte.d.ts +24 -0
- package/dist/ui/AreaChart.svelte +9 -0
- package/dist/ui/AreaChart.svelte.d.ts +26 -0
- package/dist/ui/AspectRatio.svelte +33 -0
- package/dist/ui/AspectRatio.svelte.d.ts +10 -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/BarChart.svelte +170 -0
- package/dist/ui/BarChart.svelte.d.ts +40 -0
- package/dist/ui/Breadcrumb.svelte +92 -0
- package/dist/ui/Breadcrumb.svelte.d.ts +14 -0
- package/dist/ui/Button.svelte +136 -0
- package/dist/ui/Button.svelte.d.ts +18 -0
- package/dist/ui/ButtonGroup.svelte +61 -0
- package/dist/ui/ButtonGroup.svelte.d.ts +12 -0
- package/dist/ui/Calendar.svelte +265 -0
- package/dist/ui/Calendar.svelte.d.ts +28 -0
- package/dist/ui/Card.svelte +74 -0
- package/dist/ui/Card.svelte.d.ts +18 -0
- package/dist/ui/Carousel.svelte +169 -0
- package/dist/ui/Carousel.svelte.d.ts +36 -0
- package/dist/ui/ChartFrame.svelte +174 -0
- package/dist/ui/ChartFrame.svelte.d.ts +24 -0
- package/dist/ui/Checkbox.svelte +112 -0
- package/dist/ui/Checkbox.svelte.d.ts +14 -0
- package/dist/ui/Collapsible.svelte +59 -0
- package/dist/ui/Collapsible.svelte.d.ts +12 -0
- package/dist/ui/Combobox.svelte +350 -0
- package/dist/ui/Combobox.svelte.d.ts +24 -0
- package/dist/ui/Command.svelte +268 -0
- package/dist/ui/Command.svelte.d.ts +28 -0
- package/dist/ui/ConsentScreen.svelte +125 -0
- package/dist/ui/ConsentScreen.svelte.d.ts +21 -0
- package/dist/ui/ContextMenu.svelte +78 -0
- package/dist/ui/ContextMenu.svelte.d.ts +13 -0
- package/dist/ui/DataTable.svelte +277 -0
- package/dist/ui/DataTable.svelte.d.ts +53 -0
- package/dist/ui/DatePicker.svelte +138 -0
- package/dist/ui/DatePicker.svelte.d.ts +25 -0
- package/dist/ui/Dialog.svelte +301 -0
- package/dist/ui/Dialog.svelte.d.ts +30 -0
- package/dist/ui/DonutChart.svelte +115 -0
- package/dist/ui/DonutChart.svelte.d.ts +40 -0
- package/dist/ui/Drawer.svelte +19 -0
- package/dist/ui/Drawer.svelte.d.ts +14 -0
- package/dist/ui/Empty.svelte +62 -0
- package/dist/ui/Empty.svelte.d.ts +15 -0
- package/dist/ui/Field.svelte +97 -0
- package/dist/ui/Field.svelte.d.ts +19 -0
- package/dist/ui/Form.svelte +159 -0
- package/dist/ui/Form.svelte.d.ts +21 -0
- package/dist/ui/HoverCard.svelte +106 -0
- package/dist/ui/HoverCard.svelte.d.ts +26 -0
- package/dist/ui/Input.svelte +62 -0
- package/dist/ui/Input.svelte.d.ts +7 -0
- package/dist/ui/InputGroup.svelte +80 -0
- package/dist/ui/InputGroup.svelte.d.ts +13 -0
- package/dist/ui/InputOTP.svelte +140 -0
- package/dist/ui/InputOTP.svelte.d.ts +18 -0
- package/dist/ui/Item.svelte +86 -0
- package/dist/ui/Item.svelte.d.ts +17 -0
- package/dist/ui/Kbd.svelte +24 -0
- package/dist/ui/Kbd.svelte.d.ts +7 -0
- package/dist/ui/Label.svelte +29 -0
- package/dist/ui/Label.svelte.d.ts +10 -0
- package/dist/ui/LineChart.svelte +171 -0
- package/dist/ui/LineChart.svelte.d.ts +42 -0
- package/dist/ui/Menu.svelte +103 -0
- package/dist/ui/Menu.svelte.d.ts +15 -0
- package/dist/ui/MenuList.svelte +317 -0
- package/dist/ui/MenuList.svelte.d.ts +29 -0
- package/dist/ui/Menubar.svelte +133 -0
- package/dist/ui/Menubar.svelte.d.ts +14 -0
- package/dist/ui/NavigationMenu.svelte +208 -0
- package/dist/ui/NavigationMenu.svelte.d.ts +18 -0
- package/dist/ui/Pagination.svelte +114 -0
- package/dist/ui/Pagination.svelte.d.ts +16 -0
- package/dist/ui/PasswordInput.svelte +134 -0
- package/dist/ui/PasswordInput.svelte.d.ts +11 -0
- package/dist/ui/Popover.svelte +127 -0
- package/dist/ui/Popover.svelte.d.ts +29 -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/Resizable.svelte +122 -0
- package/dist/ui/Resizable.svelte.d.ts +16 -0
- package/dist/ui/ScrollArea.svelte +43 -0
- package/dist/ui/ScrollArea.svelte.d.ts +13 -0
- package/dist/ui/Select.svelte +77 -0
- package/dist/ui/Select.svelte.d.ts +18 -0
- package/dist/ui/SelectMenu.svelte +301 -0
- package/dist/ui/SelectMenu.svelte.d.ts +18 -0
- package/dist/ui/Separator.svelte +19 -0
- package/dist/ui/Separator.svelte.d.ts +7 -0
- package/dist/ui/Sheet.svelte +21 -0
- package/dist/ui/Sheet.svelte.d.ts +16 -0
- package/dist/ui/Sidebar.svelte +263 -0
- package/dist/ui/Sidebar.svelte.d.ts +35 -0
- package/dist/ui/SignIn.svelte +182 -0
- package/dist/ui/SignIn.svelte.d.ts +38 -0
- package/dist/ui/SignUp.svelte +106 -0
- package/dist/ui/SignUp.svelte.d.ts +23 -0
- package/dist/ui/Skeleton.svelte +29 -0
- package/dist/ui/Skeleton.svelte.d.ts +9 -0
- package/dist/ui/Slider.svelte +196 -0
- package/dist/ui/Slider.svelte.d.ts +19 -0
- package/dist/ui/Sparkline.svelte +23 -0
- package/dist/ui/Sparkline.svelte.d.ts +11 -0
- package/dist/ui/Spinner.svelte +42 -0
- package/dist/ui/Spinner.svelte.d.ts +8 -0
- package/dist/ui/Stat.svelte +91 -0
- package/dist/ui/Stat.svelte.d.ts +18 -0
- package/dist/ui/Switch.svelte +90 -0
- package/dist/ui/Switch.svelte.d.ts +12 -0
- package/dist/ui/Table.svelte +70 -0
- package/dist/ui/Table.svelte.d.ts +36 -0
- package/dist/ui/Tabs.svelte +112 -0
- package/dist/ui/Tabs.svelte.d.ts +19 -0
- package/dist/ui/Textarea.svelte +52 -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/Toggle.svelte +82 -0
- package/dist/ui/Toggle.svelte.d.ts +17 -0
- package/dist/ui/ToggleGroup.svelte +143 -0
- package/dist/ui/ToggleGroup.svelte.d.ts +27 -0
- package/dist/ui/Tooltip.svelte +85 -0
- package/dist/ui/Tooltip.svelte.d.ts +16 -0
- package/dist/ui/chart.d.ts +26 -0
- package/dist/ui/chart.js +89 -0
- package/dist/ui/dates.d.ts +13 -0
- package/dist/ui/dates.js +64 -0
- package/dist/ui/field.d.ts +18 -0
- package/dist/ui/field.js +8 -0
- package/dist/ui/floating.d.ts +44 -0
- package/dist/ui/floating.js +97 -0
- package/dist/ui/form.d.ts +12 -0
- package/dist/ui/form.js +8 -0
- package/dist/ui/index.d.ts +78 -0
- package/dist/ui/index.js +70 -0
- package/dist/ui/menu.d.ts +25 -0
- package/dist/ui/menu.js +1 -0
- package/dist/ui/options.d.ts +17 -0
- package/dist/ui/options.js +22 -0
- package/dist/ui/table.d.ts +26 -0
- package/dist/ui/table.js +1 -0
- package/dist/ui/theme.css +176 -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 +15 -4
- package/package.json +7 -1
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Snippet } from 'svelte';
|
|
3
|
+
import { page } from '../runtime/state.svelte.js';
|
|
4
|
+
import Dialog from './Dialog.svelte';
|
|
5
|
+
|
|
6
|
+
export type SidebarLink = {
|
|
7
|
+
label: string;
|
|
8
|
+
href: string;
|
|
9
|
+
/** An icon, shown alone when the sidebar is collapsed. */
|
|
10
|
+
icon?: Snippet;
|
|
11
|
+
/** A count or label after the link, such as "3". */
|
|
12
|
+
badge?: string;
|
|
13
|
+
/** Links under this one, shown while it or one of them is current. */
|
|
14
|
+
items?: { label: string; href: string }[];
|
|
15
|
+
};
|
|
16
|
+
export type SidebarGroup = { label?: string; items: SidebarLink[] };
|
|
17
|
+
|
|
18
|
+
type Props = {
|
|
19
|
+
groups: SidebarGroup[];
|
|
20
|
+
/** The app's name or logo, at the top. */
|
|
21
|
+
header?: Snippet;
|
|
22
|
+
/** Things at the bottom, such as the signed-in user. */
|
|
23
|
+
footer?: Snippet;
|
|
24
|
+
/** Show icons only on wide screens; bind it. */
|
|
25
|
+
collapsed?: boolean;
|
|
26
|
+
/** Names the navigation. */
|
|
27
|
+
label?: string;
|
|
28
|
+
/** The page's content, next to the sidebar. */
|
|
29
|
+
children: Snippet;
|
|
30
|
+
class?: string;
|
|
31
|
+
};
|
|
32
|
+
let { groups, header, footer, collapsed = $bindable(false), label = 'Main', children, class: klass = '' }: Props = $props();
|
|
33
|
+
|
|
34
|
+
let mobileOpen = $state(false);
|
|
35
|
+
const path = $derived(page.url?.pathname ?? '');
|
|
36
|
+
const isCurrent = (href: string) => path === href;
|
|
37
|
+
const inSection = (l: SidebarLink) => isCurrent(l.href) || !!l.items?.some((s) => path === s.href || path.startsWith(s.href + '/'));
|
|
38
|
+
|
|
39
|
+
// Close the mobile sheet after navigating.
|
|
40
|
+
$effect(() => {
|
|
41
|
+
void path;
|
|
42
|
+
mobileOpen = false;
|
|
43
|
+
});
|
|
44
|
+
</script>
|
|
45
|
+
|
|
46
|
+
{#snippet nav(compact: boolean)}
|
|
47
|
+
<nav aria-label={label} class="nav" class:compact>
|
|
48
|
+
{#each groups as group, gi (gi)}
|
|
49
|
+
<div class="group">
|
|
50
|
+
{#if group.label && !compact}<div class="group-label" id="tern-sidebar-group-{gi}">{group.label}</div>{/if}
|
|
51
|
+
<ul aria-labelledby={group.label && !compact ? `tern-sidebar-group-${gi}` : undefined}>
|
|
52
|
+
{#each group.items as link (link.href)}
|
|
53
|
+
<li>
|
|
54
|
+
<a href={link.href} class="link" aria-current={isCurrent(link.href) ? 'page' : undefined} aria-label={compact ? link.label : undefined} title={compact ? link.label : undefined}>
|
|
55
|
+
{#if link.icon}<span class="icon" aria-hidden="true">{@render link.icon()}</span>{/if}
|
|
56
|
+
{#if !compact}
|
|
57
|
+
<span class="label">{link.label}</span>
|
|
58
|
+
{#if link.badge}<span class="badge">{link.badge}</span>{/if}
|
|
59
|
+
{/if}
|
|
60
|
+
</a>
|
|
61
|
+
{#if link.items && !compact && inSection(link)}
|
|
62
|
+
<ul class="sub">
|
|
63
|
+
{#each link.items as sub (sub.href)}
|
|
64
|
+
<li><a href={sub.href} class="link" aria-current={isCurrent(sub.href) ? 'page' : undefined}>{sub.label}</a></li>
|
|
65
|
+
{/each}
|
|
66
|
+
</ul>
|
|
67
|
+
{/if}
|
|
68
|
+
</li>
|
|
69
|
+
{/each}
|
|
70
|
+
</ul>
|
|
71
|
+
</div>
|
|
72
|
+
{/each}
|
|
73
|
+
</nav>
|
|
74
|
+
{/snippet}
|
|
75
|
+
|
|
76
|
+
<div class="tern-sidebar-layout {klass}" class:collapsed>
|
|
77
|
+
<aside class="sidebar">
|
|
78
|
+
<div class="top">
|
|
79
|
+
{#if header && !collapsed}<div class="header">{@render header()}</div>{/if}
|
|
80
|
+
<button type="button" class="collapse" aria-label={collapsed ? 'Expand sidebar' : 'Collapse sidebar'} aria-pressed={collapsed} onclick={() => (collapsed = !collapsed)}>
|
|
81
|
+
<svg aria-hidden="true" viewBox="0 0 16 16" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.4"><rect x="2" y="2.5" width="12" height="11" rx="2" /><path d="M6 2.5v11" /></svg>
|
|
82
|
+
</button>
|
|
83
|
+
</div>
|
|
84
|
+
{@render nav(collapsed)}
|
|
85
|
+
{#if footer && !collapsed}<div class="footer">{@render footer()}</div>{/if}
|
|
86
|
+
</aside>
|
|
87
|
+
<div class="mobilebar">
|
|
88
|
+
<button type="button" class="collapse" aria-label="Open navigation" aria-expanded={mobileOpen} onclick={() => (mobileOpen = true)}>
|
|
89
|
+
<svg aria-hidden="true" viewBox="0 0 16 16" width="18" height="18" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"><path d="M2.5 4h11M2.5 8h11M2.5 12h11" /></svg>
|
|
90
|
+
</button>
|
|
91
|
+
{#if header}<div class="header">{@render header()}</div>{/if}
|
|
92
|
+
</div>
|
|
93
|
+
<div class="main">{@render children()}</div>
|
|
94
|
+
</div>
|
|
95
|
+
|
|
96
|
+
<Dialog bind:open={mobileOpen} title="Navigation" side="left" class="tern-sidebar-sheet">
|
|
97
|
+
{@render nav(false)}
|
|
98
|
+
{#if footer}<div class="footer">{@render footer()}</div>{/if}
|
|
99
|
+
</Dialog>
|
|
100
|
+
|
|
101
|
+
<style>
|
|
102
|
+
.tern-sidebar-layout {
|
|
103
|
+
display: grid;
|
|
104
|
+
grid-template-columns: 15rem 1fr;
|
|
105
|
+
min-height: 100vh;
|
|
106
|
+
}
|
|
107
|
+
.collapsed {
|
|
108
|
+
grid-template-columns: 3.75rem 1fr;
|
|
109
|
+
}
|
|
110
|
+
.sidebar {
|
|
111
|
+
position: sticky;
|
|
112
|
+
top: 0;
|
|
113
|
+
display: flex;
|
|
114
|
+
flex-direction: column;
|
|
115
|
+
gap: 0.75rem;
|
|
116
|
+
height: 100vh;
|
|
117
|
+
box-sizing: border-box;
|
|
118
|
+
padding: 0.75rem;
|
|
119
|
+
overflow-y: auto;
|
|
120
|
+
background: var(--tern-surface);
|
|
121
|
+
border-right: 1px solid var(--tern-border);
|
|
122
|
+
}
|
|
123
|
+
.top {
|
|
124
|
+
display: flex;
|
|
125
|
+
align-items: center;
|
|
126
|
+
justify-content: space-between;
|
|
127
|
+
gap: 0.5rem;
|
|
128
|
+
min-height: 2.25rem;
|
|
129
|
+
}
|
|
130
|
+
.collapsed .top {
|
|
131
|
+
justify-content: center;
|
|
132
|
+
}
|
|
133
|
+
.header {
|
|
134
|
+
font-weight: 700;
|
|
135
|
+
letter-spacing: -0.01em;
|
|
136
|
+
min-width: 0;
|
|
137
|
+
}
|
|
138
|
+
.collapse {
|
|
139
|
+
display: grid;
|
|
140
|
+
place-items: center;
|
|
141
|
+
width: 2rem;
|
|
142
|
+
height: 2rem;
|
|
143
|
+
flex: none;
|
|
144
|
+
color: var(--tern-muted);
|
|
145
|
+
background: transparent;
|
|
146
|
+
border: 0;
|
|
147
|
+
border-radius: var(--tern-radius-sm);
|
|
148
|
+
cursor: pointer;
|
|
149
|
+
}
|
|
150
|
+
.collapse:hover {
|
|
151
|
+
color: var(--tern-fg);
|
|
152
|
+
background: var(--tern-surface-2);
|
|
153
|
+
}
|
|
154
|
+
.collapse:focus-visible,
|
|
155
|
+
.link:focus-visible {
|
|
156
|
+
outline: 2px solid var(--tern-ring);
|
|
157
|
+
outline-offset: -2px;
|
|
158
|
+
}
|
|
159
|
+
.nav {
|
|
160
|
+
display: grid;
|
|
161
|
+
gap: 1rem;
|
|
162
|
+
align-content: start;
|
|
163
|
+
flex: 1;
|
|
164
|
+
}
|
|
165
|
+
.group-label {
|
|
166
|
+
padding: 0 0.6rem 0.3rem;
|
|
167
|
+
font-size: 0.75rem;
|
|
168
|
+
font-weight: 600;
|
|
169
|
+
color: var(--tern-muted);
|
|
170
|
+
}
|
|
171
|
+
ul {
|
|
172
|
+
display: grid;
|
|
173
|
+
gap: 0.125rem;
|
|
174
|
+
margin: 0;
|
|
175
|
+
padding: 0;
|
|
176
|
+
list-style: none;
|
|
177
|
+
}
|
|
178
|
+
.link {
|
|
179
|
+
display: flex;
|
|
180
|
+
align-items: center;
|
|
181
|
+
gap: 0.6rem;
|
|
182
|
+
min-height: 2.125rem;
|
|
183
|
+
padding: 0 0.6rem;
|
|
184
|
+
font-size: 0.875rem;
|
|
185
|
+
font-weight: 500;
|
|
186
|
+
color: var(--tern-muted);
|
|
187
|
+
text-decoration: none;
|
|
188
|
+
border-radius: var(--tern-radius-sm);
|
|
189
|
+
}
|
|
190
|
+
.compact .link {
|
|
191
|
+
justify-content: center;
|
|
192
|
+
padding: 0;
|
|
193
|
+
}
|
|
194
|
+
.link:hover {
|
|
195
|
+
color: var(--tern-fg);
|
|
196
|
+
background: var(--tern-surface-2);
|
|
197
|
+
}
|
|
198
|
+
.link[aria-current='page'] {
|
|
199
|
+
color: var(--tern-fg);
|
|
200
|
+
background: var(--tern-accent-soft);
|
|
201
|
+
font-weight: 600;
|
|
202
|
+
}
|
|
203
|
+
.icon {
|
|
204
|
+
display: inline-flex;
|
|
205
|
+
width: 1.125rem;
|
|
206
|
+
justify-content: center;
|
|
207
|
+
flex: none;
|
|
208
|
+
}
|
|
209
|
+
.label {
|
|
210
|
+
flex: 1;
|
|
211
|
+
}
|
|
212
|
+
.badge {
|
|
213
|
+
min-width: 1.25rem;
|
|
214
|
+
padding: 0 0.35rem;
|
|
215
|
+
font-size: 0.75rem;
|
|
216
|
+
font-weight: 600;
|
|
217
|
+
text-align: center;
|
|
218
|
+
color: var(--tern-fg);
|
|
219
|
+
background: var(--tern-surface-2);
|
|
220
|
+
border-radius: 1rem;
|
|
221
|
+
}
|
|
222
|
+
.sub {
|
|
223
|
+
margin: 0.125rem 0 0.25rem 1.45rem;
|
|
224
|
+
padding-left: 0.6rem;
|
|
225
|
+
border-left: 1px solid var(--tern-border);
|
|
226
|
+
}
|
|
227
|
+
.sub .link {
|
|
228
|
+
min-height: 1.875rem;
|
|
229
|
+
}
|
|
230
|
+
.footer {
|
|
231
|
+
padding-top: 0.75rem;
|
|
232
|
+
border-top: 1px solid var(--tern-border);
|
|
233
|
+
}
|
|
234
|
+
.main {
|
|
235
|
+
min-width: 0;
|
|
236
|
+
}
|
|
237
|
+
.mobilebar {
|
|
238
|
+
display: none;
|
|
239
|
+
}
|
|
240
|
+
@media (max-width: 48rem) {
|
|
241
|
+
.tern-sidebar-layout,
|
|
242
|
+
.collapsed {
|
|
243
|
+
grid-template-columns: 1fr;
|
|
244
|
+
}
|
|
245
|
+
.sidebar {
|
|
246
|
+
display: none;
|
|
247
|
+
}
|
|
248
|
+
.mobilebar {
|
|
249
|
+
position: sticky;
|
|
250
|
+
top: 0;
|
|
251
|
+
z-index: 10;
|
|
252
|
+
display: flex;
|
|
253
|
+
align-items: center;
|
|
254
|
+
gap: 0.5rem;
|
|
255
|
+
padding: 0.5rem 0.75rem;
|
|
256
|
+
background: var(--tern-surface);
|
|
257
|
+
border-bottom: 1px solid var(--tern-border);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
:global(.tern-sidebar-sheet.left) {
|
|
261
|
+
width: min(18rem, 100vw - 3rem);
|
|
262
|
+
}
|
|
263
|
+
</style>
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
export type SidebarLink = {
|
|
3
|
+
label: string;
|
|
4
|
+
href: string;
|
|
5
|
+
/** An icon, shown alone when the sidebar is collapsed. */
|
|
6
|
+
icon?: Snippet;
|
|
7
|
+
/** A count or label after the link, such as "3". */
|
|
8
|
+
badge?: string;
|
|
9
|
+
/** Links under this one, shown while it or one of them is current. */
|
|
10
|
+
items?: {
|
|
11
|
+
label: string;
|
|
12
|
+
href: string;
|
|
13
|
+
}[];
|
|
14
|
+
};
|
|
15
|
+
export type SidebarGroup = {
|
|
16
|
+
label?: string;
|
|
17
|
+
items: SidebarLink[];
|
|
18
|
+
};
|
|
19
|
+
type Props = {
|
|
20
|
+
groups: SidebarGroup[];
|
|
21
|
+
/** The app's name or logo, at the top. */
|
|
22
|
+
header?: Snippet;
|
|
23
|
+
/** Things at the bottom, such as the signed-in user. */
|
|
24
|
+
footer?: Snippet;
|
|
25
|
+
/** Show icons only on wide screens; bind it. */
|
|
26
|
+
collapsed?: boolean;
|
|
27
|
+
/** Names the navigation. */
|
|
28
|
+
label?: string;
|
|
29
|
+
/** The page's content, next to the sidebar. */
|
|
30
|
+
children: Snippet;
|
|
31
|
+
class?: string;
|
|
32
|
+
};
|
|
33
|
+
declare const Sidebar: import("svelte").Component<Props, {}, "collapsed">;
|
|
34
|
+
type Sidebar = ReturnType<typeof Sidebar>;
|
|
35
|
+
export default Sidebar;
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Snippet } from 'svelte';
|
|
3
|
+
import type { FormField } from '../runtime/validate.js';
|
|
4
|
+
import Alert from './Alert.svelte';
|
|
5
|
+
import Button from './Button.svelte';
|
|
6
|
+
import Checkbox from './Checkbox.svelte';
|
|
7
|
+
import Field from './Field.svelte';
|
|
8
|
+
import Form from './Form.svelte';
|
|
9
|
+
import Input from './Input.svelte';
|
|
10
|
+
import PasswordInput from './PasswordInput.svelte';
|
|
11
|
+
import Separator from './Separator.svelte';
|
|
12
|
+
|
|
13
|
+
type Props = {
|
|
14
|
+
title?: string;
|
|
15
|
+
description?: string;
|
|
16
|
+
/** The heading level; 1 when this is the page's main content. */
|
|
17
|
+
level?: 1 | 2 | 3;
|
|
18
|
+
/** Providers from plugins/oauth, such as plugin.Providers(): each links to /auth/<name>/login. */
|
|
19
|
+
providers?: { name: string; label: string }[];
|
|
20
|
+
/** Where plugins/oauth lives. */
|
|
21
|
+
authPath?: string;
|
|
22
|
+
/** Where to go after signing in, a path on this site. */
|
|
23
|
+
next?: string;
|
|
24
|
+
/** Show the email and password form, which posts to action. */
|
|
25
|
+
password?: boolean;
|
|
26
|
+
action?: string;
|
|
27
|
+
/** The sign-in action's rules, from ./$forms. */
|
|
28
|
+
rules?: FormField[];
|
|
29
|
+
/** The page's form prop, for the server's errors. */
|
|
30
|
+
form?: unknown;
|
|
31
|
+
/** The ?error= code plugins/oauth sends back after a failed sign-in. */
|
|
32
|
+
error?: string | null;
|
|
33
|
+
/** A "Keep me signed in" checkbox, posted as remember. */
|
|
34
|
+
remember?: boolean;
|
|
35
|
+
/** Where to reset a forgotten password. */
|
|
36
|
+
forgotHref?: string;
|
|
37
|
+
/** Where to create an account. */
|
|
38
|
+
signUpHref?: string;
|
|
39
|
+
/** More content under the form, such as legal text. */
|
|
40
|
+
footer?: Snippet;
|
|
41
|
+
class?: string;
|
|
42
|
+
};
|
|
43
|
+
let {
|
|
44
|
+
title = 'Sign in',
|
|
45
|
+
description,
|
|
46
|
+
level = 1,
|
|
47
|
+
providers = [],
|
|
48
|
+
authPath = '/auth',
|
|
49
|
+
next,
|
|
50
|
+
password = true,
|
|
51
|
+
action,
|
|
52
|
+
rules,
|
|
53
|
+
form,
|
|
54
|
+
error,
|
|
55
|
+
remember = false,
|
|
56
|
+
forgotHref,
|
|
57
|
+
signUpHref,
|
|
58
|
+
footer,
|
|
59
|
+
class: klass = ''
|
|
60
|
+
}: Props = $props();
|
|
61
|
+
|
|
62
|
+
let submitting = $state(false);
|
|
63
|
+
const errorText = $derived(
|
|
64
|
+
!error ? null : error === 'access_denied' ? 'You cancelled the sign-in. Try again when you’re ready.' : 'Signing in didn’t work. Try again, or choose another way.'
|
|
65
|
+
);
|
|
66
|
+
const login = (name: string) => `${authPath}/${encodeURIComponent(name)}/login${next ? `?next=${encodeURIComponent(next)}` : ''}`;
|
|
67
|
+
</script>
|
|
68
|
+
|
|
69
|
+
<section class="tern-auth {klass}" aria-labelledby="tern-signin-title">
|
|
70
|
+
<header>
|
|
71
|
+
<svelte:element this={`h${level}`} id="tern-signin-title">{title}</svelte:element>
|
|
72
|
+
{#if description}<p>{description}</p>{/if}
|
|
73
|
+
</header>
|
|
74
|
+
{#if errorText}<Alert tone="danger" live>{errorText}</Alert>{/if}
|
|
75
|
+
{#if providers.length}
|
|
76
|
+
<div class="providers">
|
|
77
|
+
{#each providers as p (p.name)}
|
|
78
|
+
<Button variant="secondary" href={login(p.name)}>Continue with {p.label}</Button>
|
|
79
|
+
{/each}
|
|
80
|
+
</div>
|
|
81
|
+
{/if}
|
|
82
|
+
{#if providers.length && password}<div class="or"><Separator /><span>or</span><Separator /></div>{/if}
|
|
83
|
+
{#if password}
|
|
84
|
+
<Form {rules} {form} {action} bind:submitting reset={false}>
|
|
85
|
+
{#if next}<input type="hidden" name="next" value={next} />{/if}
|
|
86
|
+
<Field label="Email" name="email">
|
|
87
|
+
<Input type="email" autocomplete="username" spellcheck="false" />
|
|
88
|
+
</Field>
|
|
89
|
+
<Field label="Password" name="password">
|
|
90
|
+
<PasswordInput autocomplete="current-password" />
|
|
91
|
+
</Field>
|
|
92
|
+
<div class="row">
|
|
93
|
+
{#if remember}<Checkbox name="remember">Keep me signed in</Checkbox>{/if}
|
|
94
|
+
{#if forgotHref}<a href={forgotHref}>Forgot your password?</a>{/if}
|
|
95
|
+
</div>
|
|
96
|
+
<Button type="submit" loading={submitting}>Sign in</Button>
|
|
97
|
+
</Form>
|
|
98
|
+
{/if}
|
|
99
|
+
{#if signUpHref}<p class="switch">New here? <a href={signUpHref}>Create an account</a></p>{/if}
|
|
100
|
+
{#if footer}<div class="footer">{@render footer()}</div>{/if}
|
|
101
|
+
</section>
|
|
102
|
+
|
|
103
|
+
<style>
|
|
104
|
+
.tern-auth {
|
|
105
|
+
display: grid;
|
|
106
|
+
gap: 1.25rem;
|
|
107
|
+
width: 100%;
|
|
108
|
+
max-width: 24rem;
|
|
109
|
+
box-sizing: border-box;
|
|
110
|
+
padding: 2rem;
|
|
111
|
+
color: var(--tern-fg);
|
|
112
|
+
background: var(--tern-surface);
|
|
113
|
+
border: 1px solid var(--tern-border);
|
|
114
|
+
border-radius: calc(var(--tern-radius) + 4px);
|
|
115
|
+
box-shadow: var(--tern-shadow);
|
|
116
|
+
}
|
|
117
|
+
header {
|
|
118
|
+
display: grid;
|
|
119
|
+
gap: 0.35rem;
|
|
120
|
+
}
|
|
121
|
+
:is(h1, h2, h3) {
|
|
122
|
+
margin: 0;
|
|
123
|
+
font-size: 1.375rem;
|
|
124
|
+
font-weight: 650;
|
|
125
|
+
letter-spacing: -0.02em;
|
|
126
|
+
}
|
|
127
|
+
header p {
|
|
128
|
+
margin: 0;
|
|
129
|
+
color: var(--tern-muted);
|
|
130
|
+
font-size: 0.875rem;
|
|
131
|
+
}
|
|
132
|
+
.providers {
|
|
133
|
+
display: grid;
|
|
134
|
+
gap: 0.5rem;
|
|
135
|
+
}
|
|
136
|
+
.providers :global(.tern-btn) {
|
|
137
|
+
width: 100%;
|
|
138
|
+
}
|
|
139
|
+
.or {
|
|
140
|
+
display: flex;
|
|
141
|
+
align-items: center;
|
|
142
|
+
gap: 0.75rem;
|
|
143
|
+
color: var(--tern-muted);
|
|
144
|
+
font-size: 0.8125rem;
|
|
145
|
+
}
|
|
146
|
+
.or :global(.tern-separator) {
|
|
147
|
+
flex: 1;
|
|
148
|
+
}
|
|
149
|
+
.row {
|
|
150
|
+
display: flex;
|
|
151
|
+
flex-wrap: wrap;
|
|
152
|
+
align-items: center;
|
|
153
|
+
justify-content: space-between;
|
|
154
|
+
gap: 0.5rem;
|
|
155
|
+
font-size: 0.875rem;
|
|
156
|
+
}
|
|
157
|
+
.row:empty {
|
|
158
|
+
display: none;
|
|
159
|
+
}
|
|
160
|
+
a {
|
|
161
|
+
color: var(--tern-accent);
|
|
162
|
+
font-weight: 550;
|
|
163
|
+
}
|
|
164
|
+
a:focus-visible {
|
|
165
|
+
outline: 2px solid var(--tern-ring);
|
|
166
|
+
outline-offset: 2px;
|
|
167
|
+
border-radius: 2px;
|
|
168
|
+
}
|
|
169
|
+
.switch {
|
|
170
|
+
margin: 0;
|
|
171
|
+
text-align: center;
|
|
172
|
+
font-size: 0.875rem;
|
|
173
|
+
color: var(--tern-muted);
|
|
174
|
+
}
|
|
175
|
+
.footer {
|
|
176
|
+
font-size: 0.8125rem;
|
|
177
|
+
color: var(--tern-muted);
|
|
178
|
+
}
|
|
179
|
+
.tern-auth :global(.tern-form > .tern-btn) {
|
|
180
|
+
width: 100%;
|
|
181
|
+
}
|
|
182
|
+
</style>
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
import type { FormField } from '../runtime/validate.js';
|
|
3
|
+
type Props = {
|
|
4
|
+
title?: string;
|
|
5
|
+
description?: string;
|
|
6
|
+
/** The heading level; 1 when this is the page's main content. */
|
|
7
|
+
level?: 1 | 2 | 3;
|
|
8
|
+
/** Providers from plugins/oauth, such as plugin.Providers(): each links to /auth/<name>/login. */
|
|
9
|
+
providers?: {
|
|
10
|
+
name: string;
|
|
11
|
+
label: string;
|
|
12
|
+
}[];
|
|
13
|
+
/** Where plugins/oauth lives. */
|
|
14
|
+
authPath?: string;
|
|
15
|
+
/** Where to go after signing in, a path on this site. */
|
|
16
|
+
next?: string;
|
|
17
|
+
/** Show the email and password form, which posts to action. */
|
|
18
|
+
password?: boolean;
|
|
19
|
+
action?: string;
|
|
20
|
+
/** The sign-in action's rules, from ./$forms. */
|
|
21
|
+
rules?: FormField[];
|
|
22
|
+
/** The page's form prop, for the server's errors. */
|
|
23
|
+
form?: unknown;
|
|
24
|
+
/** The ?error= code plugins/oauth sends back after a failed sign-in. */
|
|
25
|
+
error?: string | null;
|
|
26
|
+
/** A "Keep me signed in" checkbox, posted as remember. */
|
|
27
|
+
remember?: boolean;
|
|
28
|
+
/** Where to reset a forgotten password. */
|
|
29
|
+
forgotHref?: string;
|
|
30
|
+
/** Where to create an account. */
|
|
31
|
+
signUpHref?: string;
|
|
32
|
+
/** More content under the form, such as legal text. */
|
|
33
|
+
footer?: Snippet;
|
|
34
|
+
class?: string;
|
|
35
|
+
};
|
|
36
|
+
declare const SignIn: import("svelte").Component<Props, {}, "">;
|
|
37
|
+
type SignIn = ReturnType<typeof SignIn>;
|
|
38
|
+
export default SignIn;
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Snippet } from 'svelte';
|
|
3
|
+
import type { FormField } from '../runtime/validate.js';
|
|
4
|
+
import Button from './Button.svelte';
|
|
5
|
+
import Checkbox from './Checkbox.svelte';
|
|
6
|
+
import Field from './Field.svelte';
|
|
7
|
+
import Form from './Form.svelte';
|
|
8
|
+
import Input from './Input.svelte';
|
|
9
|
+
import PasswordInput from './PasswordInput.svelte';
|
|
10
|
+
|
|
11
|
+
type Props = {
|
|
12
|
+
title?: string;
|
|
13
|
+
description?: string;
|
|
14
|
+
level?: 1 | 2 | 3;
|
|
15
|
+
action?: string;
|
|
16
|
+
/** The sign-up action's rules, from ./$forms; they also set the password hint. */
|
|
17
|
+
rules?: FormField[];
|
|
18
|
+
form?: unknown;
|
|
19
|
+
/** Ask for a name, posted as name. */
|
|
20
|
+
name?: boolean;
|
|
21
|
+
/** Ask to type the password twice, posted as confirm. */
|
|
22
|
+
confirm?: boolean;
|
|
23
|
+
/** Links to the terms; then a checkbox, posted as terms, must be checked. */
|
|
24
|
+
termsHref?: string;
|
|
25
|
+
signInHref?: string;
|
|
26
|
+
footer?: Snippet;
|
|
27
|
+
class?: string;
|
|
28
|
+
};
|
|
29
|
+
let { title = 'Create an account', description, level = 1, action, rules, form, name = true, confirm = false, termsHref, signInHref, footer, class: klass = '' }: Props = $props();
|
|
30
|
+
|
|
31
|
+
let submitting = $state(false);
|
|
32
|
+
const minPassword = $derived(rules?.find((f) => f.name === 'password')?.rules?.find((r) => r.name === 'min')?.arg);
|
|
33
|
+
</script>
|
|
34
|
+
|
|
35
|
+
<section class="tern-auth {klass}" aria-labelledby="tern-signup-title">
|
|
36
|
+
<header>
|
|
37
|
+
<svelte:element this={`h${level}`} id="tern-signup-title">{title}</svelte:element>
|
|
38
|
+
{#if description}<p>{description}</p>{/if}
|
|
39
|
+
</header>
|
|
40
|
+
<Form {rules} {form} {action} bind:submitting reset={false}>
|
|
41
|
+
{#if name}
|
|
42
|
+
<Field label="Name" name="name"><Input autocomplete="name" /></Field>
|
|
43
|
+
{/if}
|
|
44
|
+
<Field label="Email" name="email"><Input type="email" autocomplete="email" spellcheck="false" /></Field>
|
|
45
|
+
<Field label="Password" name="password" description={minPassword ? `At least ${minPassword} characters.` : undefined}>
|
|
46
|
+
<PasswordInput autocomplete="new-password" />
|
|
47
|
+
</Field>
|
|
48
|
+
{#if confirm}
|
|
49
|
+
<Field label="Confirm password" name="confirm"><PasswordInput autocomplete="new-password" /></Field>
|
|
50
|
+
{/if}
|
|
51
|
+
{#if termsHref}
|
|
52
|
+
<Checkbox name="terms">I agree to the <a href={termsHref}>terms</a></Checkbox>
|
|
53
|
+
{/if}
|
|
54
|
+
<Button type="submit" loading={submitting}>Create account</Button>
|
|
55
|
+
</Form>
|
|
56
|
+
{#if signInHref}<p class="switch">Already have an account? <a href={signInHref}>Sign in</a></p>{/if}
|
|
57
|
+
{#if footer}<div class="footer">{@render footer()}</div>{/if}
|
|
58
|
+
</section>
|
|
59
|
+
|
|
60
|
+
<style>
|
|
61
|
+
.tern-auth {
|
|
62
|
+
display: grid;
|
|
63
|
+
gap: 1.25rem;
|
|
64
|
+
width: 100%;
|
|
65
|
+
max-width: 24rem;
|
|
66
|
+
box-sizing: border-box;
|
|
67
|
+
padding: 2rem;
|
|
68
|
+
color: var(--tern-fg);
|
|
69
|
+
background: var(--tern-surface);
|
|
70
|
+
border: 1px solid var(--tern-border);
|
|
71
|
+
border-radius: calc(var(--tern-radius) + 4px);
|
|
72
|
+
box-shadow: var(--tern-shadow);
|
|
73
|
+
}
|
|
74
|
+
header {
|
|
75
|
+
display: grid;
|
|
76
|
+
gap: 0.35rem;
|
|
77
|
+
}
|
|
78
|
+
:is(h1, h2, h3) {
|
|
79
|
+
margin: 0;
|
|
80
|
+
font-size: 1.375rem;
|
|
81
|
+
font-weight: 650;
|
|
82
|
+
letter-spacing: -0.02em;
|
|
83
|
+
}
|
|
84
|
+
header p {
|
|
85
|
+
margin: 0;
|
|
86
|
+
color: var(--tern-muted);
|
|
87
|
+
font-size: 0.875rem;
|
|
88
|
+
}
|
|
89
|
+
a {
|
|
90
|
+
color: var(--tern-accent);
|
|
91
|
+
font-weight: 550;
|
|
92
|
+
}
|
|
93
|
+
.switch {
|
|
94
|
+
margin: 0;
|
|
95
|
+
text-align: center;
|
|
96
|
+
font-size: 0.875rem;
|
|
97
|
+
color: var(--tern-muted);
|
|
98
|
+
}
|
|
99
|
+
.footer {
|
|
100
|
+
font-size: 0.8125rem;
|
|
101
|
+
color: var(--tern-muted);
|
|
102
|
+
}
|
|
103
|
+
.tern-auth :global(.tern-form > .tern-btn) {
|
|
104
|
+
width: 100%;
|
|
105
|
+
}
|
|
106
|
+
</style>
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
import type { FormField } from '../runtime/validate.js';
|
|
3
|
+
type Props = {
|
|
4
|
+
title?: string;
|
|
5
|
+
description?: string;
|
|
6
|
+
level?: 1 | 2 | 3;
|
|
7
|
+
action?: string;
|
|
8
|
+
/** The sign-up action's rules, from ./$forms; they also set the password hint. */
|
|
9
|
+
rules?: FormField[];
|
|
10
|
+
form?: unknown;
|
|
11
|
+
/** Ask for a name, posted as name. */
|
|
12
|
+
name?: boolean;
|
|
13
|
+
/** Ask to type the password twice, posted as confirm. */
|
|
14
|
+
confirm?: boolean;
|
|
15
|
+
/** Links to the terms; then a checkbox, posted as terms, must be checked. */
|
|
16
|
+
termsHref?: string;
|
|
17
|
+
signInHref?: string;
|
|
18
|
+
footer?: Snippet;
|
|
19
|
+
class?: string;
|
|
20
|
+
};
|
|
21
|
+
declare const SignUp: import("svelte").Component<Props, {}, "">;
|
|
22
|
+
type SignUp = ReturnType<typeof SignUp>;
|
|
23
|
+
export default SignUp;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
type Props = { width?: string; height?: string; radius?: string; class?: string };
|
|
3
|
+
let { width = '100%', height = '1rem', radius = 'var(--tern-radius-sm)', class: klass = '' }: Props = $props();
|
|
4
|
+
</script>
|
|
5
|
+
|
|
6
|
+
<!-- Decorative: pair it with a status message, such as aria-busy on the region it stands in for. -->
|
|
7
|
+
<span class="tern-skeleton {klass}" aria-hidden="true" style:width style:height style:border-radius={radius}></span>
|
|
8
|
+
|
|
9
|
+
<style>
|
|
10
|
+
.tern-skeleton {
|
|
11
|
+
display: block;
|
|
12
|
+
background: linear-gradient(90deg, var(--tern-surface-2) 25%, color-mix(in oklch, var(--tern-surface-2) 60%, var(--tern-border)) 50%, var(--tern-surface-2) 75%);
|
|
13
|
+
background-size: 200% 100%;
|
|
14
|
+
animation: shimmer 1.4s ease-in-out infinite;
|
|
15
|
+
}
|
|
16
|
+
@keyframes shimmer {
|
|
17
|
+
from {
|
|
18
|
+
background-position: 100% 0;
|
|
19
|
+
}
|
|
20
|
+
to {
|
|
21
|
+
background-position: -100% 0;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
@media (prefers-reduced-motion: reduce) {
|
|
25
|
+
.tern-skeleton {
|
|
26
|
+
animation: none;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
</style>
|