@wells-nelson/displays 0.0.0-stage → 0.0.1
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/README.md +69 -2
- package/dist/AppHeader.svelte +158 -0
- package/dist/AppHeader.svelte.d.ts +26 -0
- package/dist/Badge.svelte +43 -0
- package/dist/Badge.svelte.d.ts +8 -0
- package/dist/Button.svelte +81 -0
- package/dist/Button.svelte.d.ts +12 -0
- package/dist/ConfirmDialog.svelte +48 -0
- package/dist/ConfirmDialog.svelte.d.ts +11 -0
- package/dist/FatalError.svelte +20 -0
- package/dist/FatalError.svelte.d.ts +9 -0
- package/dist/Icon.svelte +29 -0
- package/dist/Icon.svelte.d.ts +9 -0
- package/dist/IconButton.svelte +59 -0
- package/dist/IconButton.svelte.d.ts +13 -0
- package/dist/Leaderboards.svelte +74 -0
- package/dist/Leaderboards.svelte.d.ts +31 -0
- package/dist/Modal.svelte +106 -0
- package/dist/Modal.svelte.d.ts +12 -0
- package/dist/SortableTable.svelte +161 -0
- package/dist/SortableTable.svelte.d.ts +37 -0
- package/dist/Splash.svelte +17 -0
- package/dist/Splash.svelte.d.ts +6 -0
- package/dist/icons.d.ts +36 -0
- package/dist/icons.js +41 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +13 -0
- package/dist/palettes/blue.css +32 -0
- package/dist/palettes/green.css +32 -0
- package/dist/theme.css +233 -0
- package/dist/types.d.ts +24 -0
- package/dist/types.js +15 -0
- package/package.json +44 -4
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { Leaderboard } from './types.js';
|
|
2
|
+
declare function $$render<T>(): {
|
|
3
|
+
props: {
|
|
4
|
+
boards: Leaderboard<T>[];
|
|
5
|
+
rowKey: (row: T) => string;
|
|
6
|
+
rowLabel: (row: T) => string;
|
|
7
|
+
/** Accessible name of the region. */
|
|
8
|
+
label?: string;
|
|
9
|
+
};
|
|
10
|
+
exports: {};
|
|
11
|
+
bindings: "";
|
|
12
|
+
slots: {};
|
|
13
|
+
events: {};
|
|
14
|
+
};
|
|
15
|
+
declare class __sveltets_Render<T> {
|
|
16
|
+
props(): ReturnType<typeof $$render<T>>['props'];
|
|
17
|
+
events(): ReturnType<typeof $$render<T>>['events'];
|
|
18
|
+
slots(): ReturnType<typeof $$render<T>>['slots'];
|
|
19
|
+
bindings(): "";
|
|
20
|
+
exports(): {};
|
|
21
|
+
}
|
|
22
|
+
interface $$IsomorphicComponent {
|
|
23
|
+
new <T>(options: import('svelte').ComponentConstructorOptions<ReturnType<__sveltets_Render<T>['props']>>): import('svelte').SvelteComponent<ReturnType<__sveltets_Render<T>['props']>, ReturnType<__sveltets_Render<T>['events']>, ReturnType<__sveltets_Render<T>['slots']>> & {
|
|
24
|
+
$$bindings?: ReturnType<__sveltets_Render<T>['bindings']>;
|
|
25
|
+
} & ReturnType<__sveltets_Render<T>['exports']>;
|
|
26
|
+
<T>(internal: unknown, props: ReturnType<__sveltets_Render<T>['props']> & {}): ReturnType<__sveltets_Render<T>['exports']>;
|
|
27
|
+
z_$$bindings?: ReturnType<__sveltets_Render<any>['bindings']>;
|
|
28
|
+
}
|
|
29
|
+
declare const Leaderboards: $$IsomorphicComponent;
|
|
30
|
+
type Leaderboards<T> = InstanceType<typeof Leaderboards<T>>;
|
|
31
|
+
export default Leaderboards;
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Snippet } from 'svelte';
|
|
3
|
+
import IconButton from './IconButton.svelte';
|
|
4
|
+
|
|
5
|
+
let {
|
|
6
|
+
open = $bindable(false),
|
|
7
|
+
title,
|
|
8
|
+
children,
|
|
9
|
+
footer,
|
|
10
|
+
onclose,
|
|
11
|
+
size = 'md'
|
|
12
|
+
}: {
|
|
13
|
+
open?: boolean;
|
|
14
|
+
title: string;
|
|
15
|
+
children: Snippet;
|
|
16
|
+
footer?: Snippet;
|
|
17
|
+
onclose?: () => void;
|
|
18
|
+
size?: 'sm' | 'md';
|
|
19
|
+
} = $props();
|
|
20
|
+
|
|
21
|
+
let dialog: HTMLDialogElement | undefined = $state();
|
|
22
|
+
|
|
23
|
+
$effect(() => {
|
|
24
|
+
if (!dialog) return;
|
|
25
|
+
if (open && !dialog.open) dialog.showModal();
|
|
26
|
+
else if (!open && dialog.open) dialog.close();
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
function handleClose() {
|
|
30
|
+
if (open) {
|
|
31
|
+
open = false;
|
|
32
|
+
onclose?.();
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function onBackdrop(e: MouseEvent) {
|
|
37
|
+
if (e.target === dialog) dialog?.close();
|
|
38
|
+
}
|
|
39
|
+
</script>
|
|
40
|
+
|
|
41
|
+
<dialog bind:this={dialog} class="modal {size}" onclose={handleClose} onclick={onBackdrop} aria-label={title}>
|
|
42
|
+
{#if open}
|
|
43
|
+
<div class="panel">
|
|
44
|
+
<header>
|
|
45
|
+
<h2>{title}</h2>
|
|
46
|
+
<IconButton icon="x" label="Close" onclick={() => dialog?.close()} />
|
|
47
|
+
</header>
|
|
48
|
+
<div class="body">
|
|
49
|
+
{@render children()}
|
|
50
|
+
</div>
|
|
51
|
+
{#if footer}
|
|
52
|
+
<footer>{@render footer()}</footer>
|
|
53
|
+
{/if}
|
|
54
|
+
</div>
|
|
55
|
+
{/if}
|
|
56
|
+
</dialog>
|
|
57
|
+
|
|
58
|
+
<style>
|
|
59
|
+
.modal {
|
|
60
|
+
padding: 0;
|
|
61
|
+
border: none;
|
|
62
|
+
border-radius: var(--radius-lg);
|
|
63
|
+
background: var(--surface);
|
|
64
|
+
color: var(--text);
|
|
65
|
+
width: min(34rem, calc(100vw - 2rem));
|
|
66
|
+
max-height: min(46rem, calc(100dvh - 2rem));
|
|
67
|
+
box-shadow: var(--shadow-lg);
|
|
68
|
+
}
|
|
69
|
+
.modal.sm {
|
|
70
|
+
width: min(24rem, calc(100vw - 2rem));
|
|
71
|
+
}
|
|
72
|
+
.modal::backdrop {
|
|
73
|
+
background: rgb(0 0 0 / 0.45);
|
|
74
|
+
}
|
|
75
|
+
.panel {
|
|
76
|
+
display: flex;
|
|
77
|
+
flex-direction: column;
|
|
78
|
+
max-height: inherit;
|
|
79
|
+
}
|
|
80
|
+
header {
|
|
81
|
+
display: flex;
|
|
82
|
+
align-items: center;
|
|
83
|
+
justify-content: space-between;
|
|
84
|
+
gap: 0.5rem;
|
|
85
|
+
padding: 0.75rem 0.75rem 0.25rem 1.25rem;
|
|
86
|
+
}
|
|
87
|
+
h2 {
|
|
88
|
+
margin: 0;
|
|
89
|
+
font-size: 1.15rem;
|
|
90
|
+
}
|
|
91
|
+
.body {
|
|
92
|
+
padding: 0.5rem 1.25rem 1rem;
|
|
93
|
+
overflow-y: auto;
|
|
94
|
+
display: flex;
|
|
95
|
+
flex-direction: column;
|
|
96
|
+
gap: 1rem;
|
|
97
|
+
}
|
|
98
|
+
footer {
|
|
99
|
+
display: flex;
|
|
100
|
+
flex-wrap: wrap;
|
|
101
|
+
gap: 0.5rem;
|
|
102
|
+
justify-content: flex-end;
|
|
103
|
+
padding: 0.75rem 1.25rem 1rem;
|
|
104
|
+
border-top: 1px solid var(--border);
|
|
105
|
+
}
|
|
106
|
+
</style>
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
type $$ComponentProps = {
|
|
3
|
+
open?: boolean;
|
|
4
|
+
title: string;
|
|
5
|
+
children: Snippet;
|
|
6
|
+
footer?: Snippet;
|
|
7
|
+
onclose?: () => void;
|
|
8
|
+
size?: 'sm' | 'md';
|
|
9
|
+
};
|
|
10
|
+
declare const Modal: import("svelte").Component<$$ComponentProps, {}, "open">;
|
|
11
|
+
type Modal = ReturnType<typeof Modal>;
|
|
12
|
+
export default Modal;
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
<script lang="ts" generics="T">
|
|
2
|
+
import { sortRows, type SortableColumn } from './types.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Table with a pinned first column that scrolls sideways on small screens. Tap a header to
|
|
6
|
+
* sort by it, tap again to flip. Rows without a value sort last.
|
|
7
|
+
*/
|
|
8
|
+
let {
|
|
9
|
+
title,
|
|
10
|
+
rows,
|
|
11
|
+
columns,
|
|
12
|
+
rowKey,
|
|
13
|
+
rowLabel,
|
|
14
|
+
labelHeader = 'Name',
|
|
15
|
+
sortKey = $bindable(),
|
|
16
|
+
descending = $bindable(true),
|
|
17
|
+
emptyText = 'Nothing recorded yet.'
|
|
18
|
+
}: {
|
|
19
|
+
title?: string;
|
|
20
|
+
rows: T[];
|
|
21
|
+
columns: SortableColumn<T>[];
|
|
22
|
+
rowKey: (row: T) => string;
|
|
23
|
+
/** The pinned first column. */
|
|
24
|
+
rowLabel: (row: T) => string;
|
|
25
|
+
labelHeader?: string;
|
|
26
|
+
/** Column currently sorted by; defaults to the first column. */
|
|
27
|
+
sortKey?: string;
|
|
28
|
+
descending?: boolean;
|
|
29
|
+
emptyText?: string;
|
|
30
|
+
} = $props();
|
|
31
|
+
|
|
32
|
+
const activeKey = $derived(sortKey ?? columns[0]?.key);
|
|
33
|
+
const activeColumn = $derived(columns.find((c) => c.key === activeKey));
|
|
34
|
+
const sorted = $derived(activeColumn ? sortRows(rows, activeColumn.value, rowLabel, descending) : rows);
|
|
35
|
+
|
|
36
|
+
function sortBy(key: string) {
|
|
37
|
+
if (key === activeKey) descending = !descending;
|
|
38
|
+
else {
|
|
39
|
+
sortKey = key;
|
|
40
|
+
descending = true;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function cell(col: SortableColumn<T>, row: T) {
|
|
45
|
+
if (col.display) return col.display(row);
|
|
46
|
+
return col.value(row) ?? '';
|
|
47
|
+
}
|
|
48
|
+
</script>
|
|
49
|
+
|
|
50
|
+
<section class="table-card card">
|
|
51
|
+
{#if title}<h2>{title}</h2>{/if}
|
|
52
|
+
{#if rows.length === 0}
|
|
53
|
+
<p class="muted empty-row">{emptyText}</p>
|
|
54
|
+
{:else}
|
|
55
|
+
<div class="scroll">
|
|
56
|
+
<table>
|
|
57
|
+
<thead>
|
|
58
|
+
<tr>
|
|
59
|
+
<th scope="col" class="name">{labelHeader}</th>
|
|
60
|
+
{#each columns as col (col.key)}
|
|
61
|
+
<th
|
|
62
|
+
scope="col"
|
|
63
|
+
aria-sort={col.key === activeKey ? (descending ? 'descending' : 'ascending') : undefined}
|
|
64
|
+
>
|
|
65
|
+
<button type="button" onclick={() => sortBy(col.key)} title={`Sort by ${col.full ?? col.title}`}>
|
|
66
|
+
{col.title}
|
|
67
|
+
{#if col.key === activeKey}<span aria-hidden="true">{descending ? '▼' : '▲'}</span>{/if}
|
|
68
|
+
</button>
|
|
69
|
+
</th>
|
|
70
|
+
{/each}
|
|
71
|
+
</tr>
|
|
72
|
+
</thead>
|
|
73
|
+
<tbody>
|
|
74
|
+
{#each sorted as row (rowKey(row))}
|
|
75
|
+
<tr>
|
|
76
|
+
<th scope="row" class="name">{rowLabel(row)}</th>
|
|
77
|
+
{#each columns as col (col.key)}
|
|
78
|
+
<td>{cell(col, row)}</td>
|
|
79
|
+
{/each}
|
|
80
|
+
</tr>
|
|
81
|
+
{/each}
|
|
82
|
+
</tbody>
|
|
83
|
+
</table>
|
|
84
|
+
</div>
|
|
85
|
+
{/if}
|
|
86
|
+
</section>
|
|
87
|
+
|
|
88
|
+
<style>
|
|
89
|
+
.table-card {
|
|
90
|
+
margin-bottom: 1rem;
|
|
91
|
+
overflow: hidden;
|
|
92
|
+
}
|
|
93
|
+
h2 {
|
|
94
|
+
margin: 0;
|
|
95
|
+
padding: 0.75rem 1rem;
|
|
96
|
+
font-size: 1rem;
|
|
97
|
+
}
|
|
98
|
+
.scroll {
|
|
99
|
+
overflow-x: auto;
|
|
100
|
+
-webkit-overflow-scrolling: touch;
|
|
101
|
+
}
|
|
102
|
+
table {
|
|
103
|
+
border-collapse: separate;
|
|
104
|
+
border-spacing: 0;
|
|
105
|
+
width: 100%;
|
|
106
|
+
font-size: 0.9rem;
|
|
107
|
+
font-variant-numeric: tabular-nums;
|
|
108
|
+
}
|
|
109
|
+
th,
|
|
110
|
+
td {
|
|
111
|
+
padding: 0.45rem 0.6rem;
|
|
112
|
+
border-top: 1px solid var(--border);
|
|
113
|
+
text-align: right;
|
|
114
|
+
white-space: nowrap;
|
|
115
|
+
}
|
|
116
|
+
thead th {
|
|
117
|
+
background: var(--surface-2);
|
|
118
|
+
font-size: 0.8rem;
|
|
119
|
+
padding: 0;
|
|
120
|
+
}
|
|
121
|
+
thead th.name {
|
|
122
|
+
padding: 0.45rem 0.75rem;
|
|
123
|
+
}
|
|
124
|
+
thead button {
|
|
125
|
+
width: 100%;
|
|
126
|
+
padding: 0.5rem 0.6rem;
|
|
127
|
+
border: none;
|
|
128
|
+
background: none;
|
|
129
|
+
font-weight: 700;
|
|
130
|
+
text-align: right;
|
|
131
|
+
cursor: pointer;
|
|
132
|
+
white-space: nowrap;
|
|
133
|
+
}
|
|
134
|
+
thead button span {
|
|
135
|
+
font-size: 0.65rem;
|
|
136
|
+
color: var(--accent-text);
|
|
137
|
+
}
|
|
138
|
+
.name {
|
|
139
|
+
position: sticky;
|
|
140
|
+
left: 0;
|
|
141
|
+
z-index: 1;
|
|
142
|
+
text-align: left;
|
|
143
|
+
background: var(--surface);
|
|
144
|
+
font-weight: 600;
|
|
145
|
+
max-width: 11rem;
|
|
146
|
+
overflow: hidden;
|
|
147
|
+
text-overflow: ellipsis;
|
|
148
|
+
box-shadow: 1px 0 0 var(--border);
|
|
149
|
+
}
|
|
150
|
+
thead .name {
|
|
151
|
+
background: var(--surface-2);
|
|
152
|
+
}
|
|
153
|
+
tbody tr:hover td,
|
|
154
|
+
tbody tr:hover .name {
|
|
155
|
+
background: var(--surface-hover);
|
|
156
|
+
}
|
|
157
|
+
.empty-row {
|
|
158
|
+
padding: 0 1rem 1rem;
|
|
159
|
+
margin: 0;
|
|
160
|
+
}
|
|
161
|
+
</style>
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { type SortableColumn } from './types.js';
|
|
2
|
+
declare function $$render<T>(): {
|
|
3
|
+
props: {
|
|
4
|
+
title?: string;
|
|
5
|
+
rows: T[];
|
|
6
|
+
columns: SortableColumn<T>[];
|
|
7
|
+
rowKey: (row: T) => string;
|
|
8
|
+
/** The pinned first column. */
|
|
9
|
+
rowLabel: (row: T) => string;
|
|
10
|
+
labelHeader?: string;
|
|
11
|
+
/** Column currently sorted by; defaults to the first column. */
|
|
12
|
+
sortKey?: string;
|
|
13
|
+
descending?: boolean;
|
|
14
|
+
emptyText?: string;
|
|
15
|
+
};
|
|
16
|
+
exports: {};
|
|
17
|
+
bindings: "sortKey" | "descending";
|
|
18
|
+
slots: {};
|
|
19
|
+
events: {};
|
|
20
|
+
};
|
|
21
|
+
declare class __sveltets_Render<T> {
|
|
22
|
+
props(): ReturnType<typeof $$render<T>>['props'];
|
|
23
|
+
events(): ReturnType<typeof $$render<T>>['events'];
|
|
24
|
+
slots(): ReturnType<typeof $$render<T>>['slots'];
|
|
25
|
+
bindings(): "sortKey" | "descending";
|
|
26
|
+
exports(): {};
|
|
27
|
+
}
|
|
28
|
+
interface $$IsomorphicComponent {
|
|
29
|
+
new <T>(options: import('svelte').ComponentConstructorOptions<ReturnType<__sveltets_Render<T>['props']>>): import('svelte').SvelteComponent<ReturnType<__sveltets_Render<T>['props']>, ReturnType<__sveltets_Render<T>['events']>, ReturnType<__sveltets_Render<T>['slots']>> & {
|
|
30
|
+
$$bindings?: ReturnType<__sveltets_Render<T>['bindings']>;
|
|
31
|
+
} & ReturnType<__sveltets_Render<T>['exports']>;
|
|
32
|
+
<T>(internal: unknown, props: ReturnType<__sveltets_Render<T>['props']> & {}): ReturnType<__sveltets_Render<T>['exports']>;
|
|
33
|
+
z_$$bindings?: ReturnType<__sveltets_Render<any>['bindings']>;
|
|
34
|
+
}
|
|
35
|
+
declare const SortableTable: $$IsomorphicComponent;
|
|
36
|
+
type SortableTable<T> = InstanceType<typeof SortableTable<T>>;
|
|
37
|
+
export default SortableTable;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
/** Startup placeholder shown while the local database opens. */
|
|
3
|
+
let { text }: { text: string } = $props();
|
|
4
|
+
</script>
|
|
5
|
+
|
|
6
|
+
<div class="splash" aria-busy="true">{text}</div>
|
|
7
|
+
|
|
8
|
+
<style>
|
|
9
|
+
.splash {
|
|
10
|
+
display: grid;
|
|
11
|
+
place-items: center;
|
|
12
|
+
min-height: 100dvh;
|
|
13
|
+
font-size: 2.5rem;
|
|
14
|
+
font-weight: 800;
|
|
15
|
+
color: var(--accent-text);
|
|
16
|
+
}
|
|
17
|
+
</style>
|
package/dist/icons.d.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 24x24 stroke icons (inner SVG markup, drawn with `stroke="currentColor"`).
|
|
3
|
+
* The base set covers what the shared components and typical apps need; apps add their own
|
|
4
|
+
* with `registerIcons` before the first render.
|
|
5
|
+
*/
|
|
6
|
+
export declare const baseIcons: {
|
|
7
|
+
readonly alert: "<path d=\"M12 3.5l9.5 16.5h-19z\"/><path d=\"M12 10v4.5M12 17.3v.2\"/>";
|
|
8
|
+
readonly check: "<path d=\"M5 12.5l4.5 4.5L19 7.5\"/>";
|
|
9
|
+
readonly chevron: "<path d=\"M6 9l6 6 6-6\"/>";
|
|
10
|
+
readonly cloud: "<path d=\"M7 18.5h10.5a4 4 0 0 0 .6-8 6 6 0 0 0-11.6 1.6A3.3 3.3 0 0 0 7 18.5z\"/>";
|
|
11
|
+
readonly 'cloud-pending': "<path d=\"M7 18.5h10.5a4 4 0 0 0 .6-8 6 6 0 0 0-11.6 1.6A3.3 3.3 0 0 0 7 18.5z\"/><path d=\"M12 10.5v5M9.8 12.7l2.2-2.2 2.2 2.2\"/>";
|
|
12
|
+
readonly gear: "<circle cx=\"12\" cy=\"12\" r=\"3\"/><path d=\"M19.4 15a1.7 1.7 0 0 0 .3 1.8l.1.1a2 2 0 1 1-2.8 2.8l-.1-.1a1.7 1.7 0 0 0-1.8-.3 1.7 1.7 0 0 0-1 1.5V21a2 2 0 1 1-4 0v-.1a1.7 1.7 0 0 0-1.1-1.5 1.7 1.7 0 0 0-1.8.3l-.1.1a2 2 0 1 1-2.8-2.8l.1-.1a1.7 1.7 0 0 0 .3-1.8 1.7 1.7 0 0 0-1.5-1H3a2 2 0 1 1 0-4h.1a1.7 1.7 0 0 0 1.5-1.1 1.7 1.7 0 0 0-.3-1.8l-.1-.1a2 2 0 1 1 2.8-2.8l.1.1a1.7 1.7 0 0 0 1.8.3H9a1.7 1.7 0 0 0 1-1.5V3a2 2 0 1 1 4 0v.1a1.7 1.7 0 0 0 1 1.5 1.7 1.7 0 0 0 1.8-.3l.1-.1a2 2 0 1 1 2.8 2.8l-.1.1a1.7 1.7 0 0 0-.3 1.8V9a1.7 1.7 0 0 0 1.5 1H21a2 2 0 1 1 0 4h-.1a1.7 1.7 0 0 0-1.5 1z\"/>";
|
|
13
|
+
readonly grip: "<circle cx=\"9\" cy=\"6\" r=\"1\"/><circle cx=\"15\" cy=\"6\" r=\"1\"/><circle cx=\"9\" cy=\"12\" r=\"1\"/><circle cx=\"15\" cy=\"12\" r=\"1\"/><circle cx=\"9\" cy=\"18\" r=\"1\"/><circle cx=\"15\" cy=\"18\" r=\"1\"/>";
|
|
14
|
+
readonly mail: "<rect x=\"3\" y=\"5.5\" width=\"18\" height=\"13\" rx=\"2\"/><path d=\"M3.5 7l8.5 6 8.5-6\"/>";
|
|
15
|
+
readonly pencil: "<path d=\"M4 20h4L19 9a2.8 2.8 0 0 0-4-4L4 16z\"/><path d=\"M13.5 6.5l4 4\"/>";
|
|
16
|
+
readonly plus: "<path d=\"M12 5v14M5 12h14\"/>";
|
|
17
|
+
readonly refresh: "<path d=\"M20 11a8 8 0 0 0-14.7-4.3L4 8.5M4 13a8 8 0 0 0 14.7 4.3l1.3-1.8\"/><path d=\"M4 4v4.5h4.5M20 20v-4.5h-4.5\"/>";
|
|
18
|
+
readonly search: "<circle cx=\"11\" cy=\"11\" r=\"6.5\"/><path d=\"M20 20l-4.4-4.4\"/>";
|
|
19
|
+
readonly share: "<circle cx=\"18\" cy=\"5.5\" r=\"2.5\"/><circle cx=\"6\" cy=\"12\" r=\"2.5\"/><circle cx=\"18\" cy=\"18.5\" r=\"2.5\"/><path d=\"M8.2 10.8l7.6-4.1M8.2 13.2l7.6 4.1\"/>";
|
|
20
|
+
readonly trash: "<path d=\"M4 7h16M10 11v6M14 11v6M5.5 7l1 12.5a1.5 1.5 0 0 0 1.5 1.5h8a1.5 1.5 0 0 0 1.5-1.5l1-12.5M9 7V4.5A1.5 1.5 0 0 1 10.5 3h3A1.5 1.5 0 0 1 15 4.5V7\"/>";
|
|
21
|
+
readonly undo: "<path d=\"M9 14L4 9l5-5\"/><path d=\"M4 9h10.5a5.5 5.5 0 0 1 0 11H11\"/>";
|
|
22
|
+
readonly user: "<circle cx=\"12\" cy=\"8\" r=\"4\"/><path d=\"M4 21a8 8 0 0 1 16 0\"/>";
|
|
23
|
+
readonly 'wifi-off': "<path d=\"M3 3l18 18M8.5 16.5a5 5 0 0 1 7 0M5 12.9a10 10 0 0 1 4.2-2.4M14.8 10.5A10 10 0 0 1 19 12.9M2 9a15 15 0 0 1 4.1-2.6M10.7 5.1A15 15 0 0 1 22 9\"/><circle cx=\"12\" cy=\"20\" r=\"0.6\"/>";
|
|
24
|
+
readonly x: "<path d=\"M6 6l12 12M18 6L6 18\"/>";
|
|
25
|
+
};
|
|
26
|
+
export type BaseIconName = keyof typeof baseIcons;
|
|
27
|
+
/** A base icon or any name an app registered. `string & {}` keeps editor completion for the base set. */
|
|
28
|
+
export type IconName = BaseIconName | (string & {});
|
|
29
|
+
/**
|
|
30
|
+
* Add (or replace) icons. Values are the inner SVG markup for a 24x24 viewBox, stroke-based.
|
|
31
|
+
* Call once at startup, before anything renders them.
|
|
32
|
+
*/
|
|
33
|
+
export declare function registerIcons(icons: Record<string, string>): void;
|
|
34
|
+
/** The SVG markup for an icon, or '' when the name isn't registered. */
|
|
35
|
+
export declare function iconMarkup(name: IconName): string;
|
|
36
|
+
export declare function hasIcon(name: string): boolean;
|
package/dist/icons.js
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 24x24 stroke icons (inner SVG markup, drawn with `stroke="currentColor"`).
|
|
3
|
+
* The base set covers what the shared components and typical apps need; apps add their own
|
|
4
|
+
* with `registerIcons` before the first render.
|
|
5
|
+
*/
|
|
6
|
+
export const baseIcons = {
|
|
7
|
+
alert: '<path d="M12 3.5l9.5 16.5h-19z"/><path d="M12 10v4.5M12 17.3v.2"/>',
|
|
8
|
+
check: '<path d="M5 12.5l4.5 4.5L19 7.5"/>',
|
|
9
|
+
chevron: '<path d="M6 9l6 6 6-6"/>',
|
|
10
|
+
cloud: '<path d="M7 18.5h10.5a4 4 0 0 0 .6-8 6 6 0 0 0-11.6 1.6A3.3 3.3 0 0 0 7 18.5z"/>',
|
|
11
|
+
'cloud-pending': '<path d="M7 18.5h10.5a4 4 0 0 0 .6-8 6 6 0 0 0-11.6 1.6A3.3 3.3 0 0 0 7 18.5z"/><path d="M12 10.5v5M9.8 12.7l2.2-2.2 2.2 2.2"/>',
|
|
12
|
+
gear: '<circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.7 1.7 0 0 0 .3 1.8l.1.1a2 2 0 1 1-2.8 2.8l-.1-.1a1.7 1.7 0 0 0-1.8-.3 1.7 1.7 0 0 0-1 1.5V21a2 2 0 1 1-4 0v-.1a1.7 1.7 0 0 0-1.1-1.5 1.7 1.7 0 0 0-1.8.3l-.1.1a2 2 0 1 1-2.8-2.8l.1-.1a1.7 1.7 0 0 0 .3-1.8 1.7 1.7 0 0 0-1.5-1H3a2 2 0 1 1 0-4h.1a1.7 1.7 0 0 0 1.5-1.1 1.7 1.7 0 0 0-.3-1.8l-.1-.1a2 2 0 1 1 2.8-2.8l.1.1a1.7 1.7 0 0 0 1.8.3H9a1.7 1.7 0 0 0 1-1.5V3a2 2 0 1 1 4 0v.1a1.7 1.7 0 0 0 1 1.5 1.7 1.7 0 0 0 1.8-.3l.1-.1a2 2 0 1 1 2.8 2.8l-.1.1a1.7 1.7 0 0 0-.3 1.8V9a1.7 1.7 0 0 0 1.5 1H21a2 2 0 1 1 0 4h-.1a1.7 1.7 0 0 0-1.5 1z"/>',
|
|
13
|
+
grip: '<circle cx="9" cy="6" r="1"/><circle cx="15" cy="6" r="1"/><circle cx="9" cy="12" r="1"/><circle cx="15" cy="12" r="1"/><circle cx="9" cy="18" r="1"/><circle cx="15" cy="18" r="1"/>',
|
|
14
|
+
mail: '<rect x="3" y="5.5" width="18" height="13" rx="2"/><path d="M3.5 7l8.5 6 8.5-6"/>',
|
|
15
|
+
pencil: '<path d="M4 20h4L19 9a2.8 2.8 0 0 0-4-4L4 16z"/><path d="M13.5 6.5l4 4"/>',
|
|
16
|
+
plus: '<path d="M12 5v14M5 12h14"/>',
|
|
17
|
+
refresh: '<path d="M20 11a8 8 0 0 0-14.7-4.3L4 8.5M4 13a8 8 0 0 0 14.7 4.3l1.3-1.8"/><path d="M4 4v4.5h4.5M20 20v-4.5h-4.5"/>',
|
|
18
|
+
search: '<circle cx="11" cy="11" r="6.5"/><path d="M20 20l-4.4-4.4"/>',
|
|
19
|
+
share: '<circle cx="18" cy="5.5" r="2.5"/><circle cx="6" cy="12" r="2.5"/><circle cx="18" cy="18.5" r="2.5"/><path d="M8.2 10.8l7.6-4.1M8.2 13.2l7.6 4.1"/>',
|
|
20
|
+
trash: '<path d="M4 7h16M10 11v6M14 11v6M5.5 7l1 12.5a1.5 1.5 0 0 0 1.5 1.5h8a1.5 1.5 0 0 0 1.5-1.5l1-12.5M9 7V4.5A1.5 1.5 0 0 1 10.5 3h3A1.5 1.5 0 0 1 15 4.5V7"/>',
|
|
21
|
+
undo: '<path d="M9 14L4 9l5-5"/><path d="M4 9h10.5a5.5 5.5 0 0 1 0 11H11"/>',
|
|
22
|
+
user: '<circle cx="12" cy="8" r="4"/><path d="M4 21a8 8 0 0 1 16 0"/>',
|
|
23
|
+
'wifi-off': '<path d="M3 3l18 18M8.5 16.5a5 5 0 0 1 7 0M5 12.9a10 10 0 0 1 4.2-2.4M14.8 10.5A10 10 0 0 1 19 12.9M2 9a15 15 0 0 1 4.1-2.6M10.7 5.1A15 15 0 0 1 22 9"/><circle cx="12" cy="20" r="0.6"/>',
|
|
24
|
+
x: '<path d="M6 6l12 12M18 6L6 18"/>'
|
|
25
|
+
};
|
|
26
|
+
const registry = new Map(Object.entries(baseIcons));
|
|
27
|
+
/**
|
|
28
|
+
* Add (or replace) icons. Values are the inner SVG markup for a 24x24 viewBox, stroke-based.
|
|
29
|
+
* Call once at startup, before anything renders them.
|
|
30
|
+
*/
|
|
31
|
+
export function registerIcons(icons) {
|
|
32
|
+
for (const [name, markup] of Object.entries(icons))
|
|
33
|
+
registry.set(name, markup);
|
|
34
|
+
}
|
|
35
|
+
/** The SVG markup for an icon, or '' when the name isn't registered. */
|
|
36
|
+
export function iconMarkup(name) {
|
|
37
|
+
return registry.get(name) ?? '';
|
|
38
|
+
}
|
|
39
|
+
export function hasIcon(name) {
|
|
40
|
+
return registry.has(name);
|
|
41
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export { default as AppHeader } from './AppHeader.svelte';
|
|
2
|
+
export { default as Badge } from './Badge.svelte';
|
|
3
|
+
export { default as Button } from './Button.svelte';
|
|
4
|
+
export { default as ConfirmDialog } from './ConfirmDialog.svelte';
|
|
5
|
+
export { default as FatalError } from './FatalError.svelte';
|
|
6
|
+
export { default as Icon } from './Icon.svelte';
|
|
7
|
+
export { default as IconButton } from './IconButton.svelte';
|
|
8
|
+
export { default as Leaderboards } from './Leaderboards.svelte';
|
|
9
|
+
export { default as Modal } from './Modal.svelte';
|
|
10
|
+
export { default as SortableTable } from './SortableTable.svelte';
|
|
11
|
+
export { default as Splash } from './Splash.svelte';
|
|
12
|
+
export { baseIcons, hasIcon, iconMarkup, registerIcons, type BaseIconName, type IconName } from './icons.js';
|
|
13
|
+
export { sortRows, type Leaderboard, type SortableColumn } from './types.js';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export { default as AppHeader } from './AppHeader.svelte';
|
|
2
|
+
export { default as Badge } from './Badge.svelte';
|
|
3
|
+
export { default as Button } from './Button.svelte';
|
|
4
|
+
export { default as ConfirmDialog } from './ConfirmDialog.svelte';
|
|
5
|
+
export { default as FatalError } from './FatalError.svelte';
|
|
6
|
+
export { default as Icon } from './Icon.svelte';
|
|
7
|
+
export { default as IconButton } from './IconButton.svelte';
|
|
8
|
+
export { default as Leaderboards } from './Leaderboards.svelte';
|
|
9
|
+
export { default as Modal } from './Modal.svelte';
|
|
10
|
+
export { default as SortableTable } from './SortableTable.svelte';
|
|
11
|
+
export { default as Splash } from './Splash.svelte';
|
|
12
|
+
export { baseIcons, hasIcon, iconMarkup, registerIcons } from './icons.js';
|
|
13
|
+
export { sortRows } from './types.js';
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/* Blue palette. theme.css already defaults to it; import this to be explicit. */
|
|
2
|
+
:root {
|
|
3
|
+
--bg: #f3f5f8;
|
|
4
|
+
--surface-2: #edf0f4;
|
|
5
|
+
--surface-hover: #e6eaf0;
|
|
6
|
+
--text: #1b1f26;
|
|
7
|
+
--muted: #646c78;
|
|
8
|
+
--border: #dfe3ea;
|
|
9
|
+
--border-strong: #c3c9d3;
|
|
10
|
+
--accent: #1f5fa8;
|
|
11
|
+
--accent-strong: #184c88;
|
|
12
|
+
--accent-text: #1d58a0;
|
|
13
|
+
--accent-soft: #dbe7f6;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
@media (prefers-color-scheme: dark) {
|
|
17
|
+
:root {
|
|
18
|
+
--bg: #111418;
|
|
19
|
+
--surface: #1a1e24;
|
|
20
|
+
--surface-2: #21262d;
|
|
21
|
+
--surface-hover: #282e36;
|
|
22
|
+
--text: #e6e9ee;
|
|
23
|
+
--muted: #98a1ad;
|
|
24
|
+
--border: #2a3038;
|
|
25
|
+
--border-strong: #434b56;
|
|
26
|
+
--accent: #4b8ad6;
|
|
27
|
+
--accent-strong: #62a0e8;
|
|
28
|
+
--accent-text: #7fb2ef;
|
|
29
|
+
--accent-soft: #1d2c40;
|
|
30
|
+
--on-accent: #0b1422;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/* Green palette. Import after theme.css. */
|
|
2
|
+
:root {
|
|
3
|
+
--bg: #f4f5f2;
|
|
4
|
+
--surface-2: #eef0ec;
|
|
5
|
+
--surface-hover: #e9ece6;
|
|
6
|
+
--text: #1d211e;
|
|
7
|
+
--muted: #69706a;
|
|
8
|
+
--border: #e1e4de;
|
|
9
|
+
--border-strong: #c5cac3;
|
|
10
|
+
--accent: #2f6f4f;
|
|
11
|
+
--accent-strong: #255a40;
|
|
12
|
+
--accent-text: #2a6447;
|
|
13
|
+
--accent-soft: #dcebe2;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
@media (prefers-color-scheme: dark) {
|
|
17
|
+
:root {
|
|
18
|
+
--bg: #121513;
|
|
19
|
+
--surface: #1c201d;
|
|
20
|
+
--surface-2: #232825;
|
|
21
|
+
--surface-hover: #2a302c;
|
|
22
|
+
--text: #e7ebe7;
|
|
23
|
+
--muted: #9aa39c;
|
|
24
|
+
--border: #2c322e;
|
|
25
|
+
--border-strong: #444c46;
|
|
26
|
+
--accent: #4f9a73;
|
|
27
|
+
--accent-strong: #5fae85;
|
|
28
|
+
--accent-text: #7cc59e;
|
|
29
|
+
--accent-soft: #23382c;
|
|
30
|
+
--on-accent: #0d1a12;
|
|
31
|
+
}
|
|
32
|
+
}
|