@wells-nelson/displays 1.0.2 → 1.0.4
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 +29 -5
- package/dist/AppHeader.svelte +68 -14
- package/dist/AppHeader.svelte.d.ts +11 -1
- package/dist/IconButton.svelte +46 -5
- package/dist/IconButton.svelte.d.ts +5 -0
- package/dist/Leaderboards.svelte +1 -1
- package/dist/SortableTable.svelte +1 -1
- package/dist/TabBar.svelte +9 -5
- package/dist/icons.d.ts +2 -0
- package/dist/icons.js +2 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/layout.d.ts +14 -0
- package/dist/layout.js +20 -0
- package/dist/theme.css +53 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -20,10 +20,15 @@ Tokens: `--bg`, `--surface`, `--surface-2`, `--surface-hover`, `--text`, `--mute
|
|
|
20
20
|
`.stack`, `.card`, `.row-card`, `.muted`, `.notice`, `.empty`, `.visually-hidden`, plus
|
|
21
21
|
drag-and-drop classes. Keep app-only tokens in the app's own stylesheet.
|
|
22
22
|
|
|
23
|
+
Date and time inputs (`date`, `time`, `datetime-local`) match the text inputs, and on iOS keep a
|
|
24
|
+
fixed size whatever their value. On touch screens text can't be selected (a long press acts like an
|
|
25
|
+
app, not a page), except in fields and anything marked `.selectable`; `Leaderboards` and
|
|
26
|
+
`SortableTable` are.
|
|
27
|
+
|
|
23
28
|
## Icons
|
|
24
29
|
|
|
25
|
-
The base set: alert, check, chevron, cloud, cloud-pending, gear, grip, home, mail, pencil, plus,
|
|
26
|
-
refresh, search, share, trash, undo, user, wifi-off, x. Register app icons at startup:
|
|
30
|
+
The base set: alert, check, chevron, cloud, cloud-pending, download, gear, grip, home, mail, pencil, plus,
|
|
31
|
+
refresh, search, share, trash, undo, upload, user, wifi-off, x. Register app icons at startup:
|
|
27
32
|
|
|
28
33
|
```ts
|
|
29
34
|
import { registerIcons } from '@wells-nelson/displays';
|
|
@@ -42,12 +47,13 @@ registerIcons({ calendar: '<rect …/>', folder: '<path …/>' }); // 24x24, str
|
|
|
42
47
|
unsynced={cloud.hasAnyPending || cloud.sessionExpired}
|
|
43
48
|
hideOnScroll
|
|
44
49
|
width={isDashboard ? '64rem' : undefined}
|
|
50
|
+
labels="always"
|
|
45
51
|
>
|
|
46
52
|
{#snippet brand()}MA{/snippet}
|
|
47
|
-
{#snippet nav()}<IconButton icon="calendar" label="Schedule" href={…} active={…} />{/snippet}
|
|
53
|
+
{#snippet nav()}<IconButton icon="calendar" label="Schedule" href={…} active={…} showLabel />{/snippet}
|
|
48
54
|
</AppHeader>
|
|
49
55
|
|
|
50
|
-
<!-- Phones: a bottom tab bar instead of the header
|
|
56
|
+
<!-- Phones: a bottom tab bar instead of the header. -->
|
|
51
57
|
<TabBar
|
|
52
58
|
items={[
|
|
53
59
|
{ label: 'Home', icon: 'home', href: resolve('/'), active: routeId === '/' },
|
|
@@ -75,6 +81,24 @@ registerIcons({ calendar: '<rect …/>', folder: '<path …/>' }); // 24x24, str
|
|
|
75
81
|
<FatalError error={app.fatal} />
|
|
76
82
|
```
|
|
77
83
|
|
|
84
|
+
Nav buttons with `showLabel` show their label to the right of the icon. `AppHeader`'s `labels` decides
|
|
85
|
+
which: `always` (default) all of them, `active` only the current one. `profileShowLabel` does the same
|
|
86
|
+
for the avatar (off by default).
|
|
87
|
+
|
|
88
|
+
**Phones.** Show a `TabBar` instead of the `AppHeader` when `phoneLayout.current` is true: narrow
|
|
89
|
+
screens (up to 40rem) and touch screens held sideways (up to 30rem tall). It goes by the screen,
|
|
90
|
+
not the user agent, which can't tell an iPad from a Mac; the query is `PHONE_LAYOUT_QUERY`. While
|
|
91
|
+
mounted, `TabBar` sets `--header-h: 0px` so pages start at the top, and `--footer-offset` so fixed
|
|
92
|
+
bottom UI sits above it.
|
|
93
|
+
|
|
94
|
+
```svelte
|
|
95
|
+
<script>
|
|
96
|
+
import { phoneLayout } from '@wells-nelson/displays';
|
|
97
|
+
</script>
|
|
98
|
+
|
|
99
|
+
{#if phoneLayout.current}<TabBar items={…} />{:else}<AppHeader …>…</AppHeader>{/if}
|
|
100
|
+
```
|
|
101
|
+
|
|
78
102
|
`Button` (primary / secondary / danger / ghost, optional icon, `block`), `IconButton` (button or
|
|
79
|
-
link, required label, `active`), `Modal` (native `<dialog>`, sm / md / lg) and `ConfirmDialog`
|
|
103
|
+
link, required label, `active`, `showLabel`), `Modal` (native `<dialog>`, sm / md / lg) and `ConfirmDialog`
|
|
80
104
|
(danger variant, busy state) complete the set. See them all in the sandbox (`npm run sandbox`).
|
package/dist/AppHeader.svelte
CHANGED
|
@@ -19,14 +19,19 @@
|
|
|
19
19
|
unsynced = false,
|
|
20
20
|
unsyncedLabel = 'Unsynced changes',
|
|
21
21
|
hideOnScroll = false,
|
|
22
|
-
width = 'var(--content-w)'
|
|
22
|
+
width = 'var(--content-w)',
|
|
23
|
+
labels = 'always',
|
|
24
|
+
profileShowLabel = false
|
|
23
25
|
}: {
|
|
24
26
|
/** Brand mark, rendered inside the home link. */
|
|
25
27
|
brand: Snippet;
|
|
26
28
|
brandHref: string;
|
|
27
29
|
/** Accessible name of the home link, e.g. "Home — all projects". */
|
|
28
30
|
brandLabel: string;
|
|
29
|
-
/**
|
|
31
|
+
/**
|
|
32
|
+
* Context navigation, usually `IconButton`s with `href`. Give them `showLabel` to show their
|
|
33
|
+
* label next to the icon (see `labels`).
|
|
34
|
+
*/
|
|
30
35
|
nav?: Snippet;
|
|
31
36
|
/** Development-only tools; the app passes this only in dev builds. */
|
|
32
37
|
dev?: Snippet;
|
|
@@ -41,6 +46,13 @@
|
|
|
41
46
|
hideOnScroll?: boolean;
|
|
42
47
|
/** Max width of the header content (CSS length), e.g. a wider value on dashboard screens. */
|
|
43
48
|
width?: string;
|
|
49
|
+
/**
|
|
50
|
+
* Which nav buttons with `showLabel` show their label: `always` all of them, `active` only the
|
|
51
|
+
* current one (the others show just the icon).
|
|
52
|
+
*/
|
|
53
|
+
labels?: 'always' | 'active';
|
|
54
|
+
/** Show `profileLabel` next to the avatar, like a nav button with `showLabel`. */
|
|
55
|
+
profileShowLabel?: boolean;
|
|
44
56
|
} = $props();
|
|
45
57
|
|
|
46
58
|
let scrolledAway = $state(false);
|
|
@@ -68,18 +80,28 @@
|
|
|
68
80
|
<header class="app-header" class:hidden>
|
|
69
81
|
<div class="inner" style:max-width={width}>
|
|
70
82
|
<a class="brand" href={brandHref} aria-label={brandLabel}>{@render brand()}</a>
|
|
71
|
-
<nav aria-label="Main">
|
|
83
|
+
<nav aria-label="Main" class="labels-{labels}">
|
|
72
84
|
{@render dev?.()}
|
|
73
85
|
{@render nav?.()}
|
|
74
|
-
<a
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
{
|
|
78
|
-
|
|
79
|
-
{
|
|
80
|
-
{
|
|
81
|
-
|
|
82
|
-
|
|
86
|
+
<a
|
|
87
|
+
class="profile"
|
|
88
|
+
class:labeled={profileShowLabel}
|
|
89
|
+
href={profileHref}
|
|
90
|
+
aria-label={profileLabel}
|
|
91
|
+
title={profileShowLabel ? undefined : profileLabel}
|
|
92
|
+
class:active={profileActive}
|
|
93
|
+
>
|
|
94
|
+
<span class="avatar">
|
|
95
|
+
{#if avatarUrl}
|
|
96
|
+
<img src={avatarUrl} alt="" referrerpolicy="no-referrer" />
|
|
97
|
+
{:else}
|
|
98
|
+
<Icon name="user" size={22} />
|
|
99
|
+
{/if}
|
|
100
|
+
{#if unsynced}
|
|
101
|
+
<span class="dot" title={unsyncedLabel}></span>
|
|
102
|
+
{/if}
|
|
103
|
+
</span>
|
|
104
|
+
{#if profileShowLabel}<span class="text" aria-hidden="true">{profileLabel}</span>{/if}
|
|
83
105
|
</a>
|
|
84
106
|
</nav>
|
|
85
107
|
</div>
|
|
@@ -118,15 +140,47 @@
|
|
|
118
140
|
nav {
|
|
119
141
|
display: flex;
|
|
120
142
|
align-items: center;
|
|
143
|
+
gap: 0.1rem;
|
|
144
|
+
}
|
|
145
|
+
/* labels="active": labeled buttons other than the current one show just their icon. */
|
|
146
|
+
.labels-active :global(.icon-btn.labeled:not(.active)) {
|
|
147
|
+
width: 2.5rem;
|
|
148
|
+
padding: 0;
|
|
149
|
+
justify-content: center;
|
|
150
|
+
border-radius: 50%;
|
|
151
|
+
}
|
|
152
|
+
.labels-active :global(.icon-btn.labeled:not(.active) .text),
|
|
153
|
+
.labels-active .profile.labeled:not(.active) .text {
|
|
154
|
+
display: none;
|
|
121
155
|
}
|
|
122
156
|
.profile {
|
|
157
|
+
display: inline-flex;
|
|
158
|
+
align-items: center;
|
|
159
|
+
justify-content: center;
|
|
160
|
+
gap: 0.4rem;
|
|
161
|
+
min-width: 2.5rem;
|
|
162
|
+
height: 2.5rem;
|
|
163
|
+
border-radius: 1.25rem;
|
|
164
|
+
color: inherit;
|
|
165
|
+
text-decoration: none;
|
|
166
|
+
}
|
|
167
|
+
.profile.labeled {
|
|
168
|
+
padding: 0 0.85rem 0 0.3rem;
|
|
169
|
+
}
|
|
170
|
+
.labels-active .profile.labeled:not(.active) {
|
|
171
|
+
padding: 0;
|
|
172
|
+
}
|
|
173
|
+
.avatar {
|
|
123
174
|
position: relative;
|
|
124
175
|
display: grid;
|
|
125
176
|
place-items: center;
|
|
126
177
|
width: 2.5rem;
|
|
127
178
|
height: 2.5rem;
|
|
128
|
-
|
|
129
|
-
|
|
179
|
+
}
|
|
180
|
+
.text {
|
|
181
|
+
font-size: 0.92rem;
|
|
182
|
+
font-weight: 600;
|
|
183
|
+
white-space: nowrap;
|
|
130
184
|
}
|
|
131
185
|
.profile:hover,
|
|
132
186
|
.profile.active {
|
|
@@ -5,7 +5,10 @@ type $$ComponentProps = {
|
|
|
5
5
|
brandHref: string;
|
|
6
6
|
/** Accessible name of the home link, e.g. "Home — all projects". */
|
|
7
7
|
brandLabel: string;
|
|
8
|
-
/**
|
|
8
|
+
/**
|
|
9
|
+
* Context navigation, usually `IconButton`s with `href`. Give them `showLabel` to show their
|
|
10
|
+
* label next to the icon (see `labels`).
|
|
11
|
+
*/
|
|
9
12
|
nav?: Snippet;
|
|
10
13
|
/** Development-only tools; the app passes this only in dev builds. */
|
|
11
14
|
dev?: Snippet;
|
|
@@ -20,6 +23,13 @@ type $$ComponentProps = {
|
|
|
20
23
|
hideOnScroll?: boolean;
|
|
21
24
|
/** Max width of the header content (CSS length), e.g. a wider value on dashboard screens. */
|
|
22
25
|
width?: string;
|
|
26
|
+
/**
|
|
27
|
+
* Which nav buttons with `showLabel` show their label: `always` all of them, `active` only the
|
|
28
|
+
* current one (the others show just the icon).
|
|
29
|
+
*/
|
|
30
|
+
labels?: 'always' | 'active';
|
|
31
|
+
/** Show `profileLabel` next to the avatar, like a nav button with `showLabel`. */
|
|
32
|
+
profileShowLabel?: boolean;
|
|
23
33
|
};
|
|
24
34
|
declare const AppHeader: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
25
35
|
type AppHeader = ReturnType<typeof AppHeader>;
|
package/dist/IconButton.svelte
CHANGED
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
onclick,
|
|
10
10
|
active = false,
|
|
11
11
|
size = 22,
|
|
12
|
-
disabled = false
|
|
12
|
+
disabled = false,
|
|
13
|
+
showLabel = false
|
|
13
14
|
}: {
|
|
14
15
|
icon: IconName;
|
|
15
16
|
label: string;
|
|
@@ -18,16 +19,43 @@
|
|
|
18
19
|
active?: boolean;
|
|
19
20
|
size?: number;
|
|
20
21
|
disabled?: boolean;
|
|
22
|
+
/**
|
|
23
|
+
* Show the label to the right of the icon. In `AppHeader`'s nav, its `labels` option can
|
|
24
|
+
* limit this to the active button.
|
|
25
|
+
*/
|
|
26
|
+
showLabel?: boolean;
|
|
21
27
|
} = $props();
|
|
22
28
|
</script>
|
|
23
29
|
|
|
30
|
+
{#snippet content()}
|
|
31
|
+
<Icon name={icon} {size} />
|
|
32
|
+
{#if showLabel}<span class="text" aria-hidden="true">{label}</span>{/if}
|
|
33
|
+
{/snippet}
|
|
34
|
+
|
|
24
35
|
{#if href}
|
|
25
|
-
<a
|
|
26
|
-
|
|
36
|
+
<a
|
|
37
|
+
class="icon-btn"
|
|
38
|
+
class:active
|
|
39
|
+
class:labeled={showLabel}
|
|
40
|
+
{href}
|
|
41
|
+
aria-label={label}
|
|
42
|
+
title={showLabel ? undefined : label}
|
|
43
|
+
aria-current={active ? 'page' : undefined}
|
|
44
|
+
>
|
|
45
|
+
{@render content()}
|
|
27
46
|
</a>
|
|
28
47
|
{:else}
|
|
29
|
-
<button
|
|
30
|
-
|
|
48
|
+
<button
|
|
49
|
+
type="button"
|
|
50
|
+
class="icon-btn"
|
|
51
|
+
class:active
|
|
52
|
+
class:labeled={showLabel}
|
|
53
|
+
aria-label={label}
|
|
54
|
+
title={showLabel ? undefined : label}
|
|
55
|
+
{onclick}
|
|
56
|
+
{disabled}
|
|
57
|
+
>
|
|
58
|
+
{@render content()}
|
|
31
59
|
</button>
|
|
32
60
|
{/if}
|
|
33
61
|
|
|
@@ -45,6 +73,19 @@
|
|
|
45
73
|
cursor: pointer;
|
|
46
74
|
text-decoration: none;
|
|
47
75
|
}
|
|
76
|
+
.labeled {
|
|
77
|
+
display: inline-flex;
|
|
78
|
+
align-items: center;
|
|
79
|
+
gap: 0.4rem;
|
|
80
|
+
width: auto;
|
|
81
|
+
padding: 0 0.85rem 0 0.6rem;
|
|
82
|
+
border-radius: 1.25rem;
|
|
83
|
+
}
|
|
84
|
+
.text {
|
|
85
|
+
font-size: 0.92rem;
|
|
86
|
+
font-weight: 600;
|
|
87
|
+
white-space: nowrap;
|
|
88
|
+
}
|
|
48
89
|
.icon-btn:hover:not(:disabled) {
|
|
49
90
|
background: var(--surface-hover);
|
|
50
91
|
}
|
|
@@ -7,6 +7,11 @@ type $$ComponentProps = {
|
|
|
7
7
|
active?: boolean;
|
|
8
8
|
size?: number;
|
|
9
9
|
disabled?: boolean;
|
|
10
|
+
/**
|
|
11
|
+
* Show the label to the right of the icon. In `AppHeader`'s nav, its `labels` option can
|
|
12
|
+
* limit this to the active button.
|
|
13
|
+
*/
|
|
14
|
+
showLabel?: boolean;
|
|
10
15
|
};
|
|
11
16
|
declare const IconButton: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
12
17
|
type IconButton = ReturnType<typeof IconButton>;
|
package/dist/Leaderboards.svelte
CHANGED
package/dist/TabBar.svelte
CHANGED
|
@@ -4,17 +4,21 @@
|
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Fixed bottom navigation for phones: one tab per top-level destination, icon over label.
|
|
7
|
-
* Use it instead of `AppHeader` on
|
|
8
|
-
*
|
|
9
|
-
* `UndoButton` and other fixed bottom UI use to sit above it
|
|
10
|
-
* height at the end of the page.
|
|
7
|
+
* Use it instead of `AppHeader` on phones (`phoneLayout` says when). While mounted it sets
|
|
8
|
+
* `--header-h: 0px` on <html>, so pages start at the top, and `--footer-offset`, which
|
|
9
|
+
* `SearchBar`, `UndoButton` and other fixed bottom UI use to sit above it; it leaves a spacer
|
|
10
|
+
* of its height at the end of the page.
|
|
11
11
|
*/
|
|
12
12
|
let { items, label = 'Main' }: { items: TabItem[]; label?: string } = $props();
|
|
13
13
|
|
|
14
14
|
$effect(() => {
|
|
15
15
|
const root = document.documentElement.style;
|
|
16
16
|
root.setProperty('--footer-offset', 'calc(var(--tabbar-h) + env(safe-area-inset-bottom, 0px))');
|
|
17
|
-
|
|
17
|
+
root.setProperty('--header-h', '0px');
|
|
18
|
+
return () => {
|
|
19
|
+
root.removeProperty('--footer-offset');
|
|
20
|
+
root.removeProperty('--header-h');
|
|
21
|
+
};
|
|
18
22
|
});
|
|
19
23
|
</script>
|
|
20
24
|
|
package/dist/icons.d.ts
CHANGED
|
@@ -9,6 +9,7 @@ export declare const baseIcons: {
|
|
|
9
9
|
readonly chevron: "<path d=\"M6 9l6 6 6-6\"/>";
|
|
10
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
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 download: "<path d=\"M12 4v11M7.5 10.5L12 15l4.5-4.5\"/><path d=\"M4.5 16v3a1.5 1.5 0 0 0 1.5 1.5h12a1.5 1.5 0 0 0 1.5-1.5v-3\"/>";
|
|
12
13
|
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
14
|
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
15
|
readonly home: "<path d=\"M3.5 11L12 4l8.5 7\"/><path d=\"M6 9.5V20h4.5v-5.5h3V20H18V9.5\"/>";
|
|
@@ -19,6 +20,7 @@ export declare const baseIcons: {
|
|
|
19
20
|
readonly search: "<circle cx=\"11\" cy=\"11\" r=\"6.5\"/><path d=\"M20 20l-4.4-4.4\"/>";
|
|
20
21
|
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\"/>";
|
|
21
22
|
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\"/>";
|
|
23
|
+
readonly upload: "<path d=\"M12 15V4M7.5 8.5L12 4l4.5 4.5\"/><path d=\"M4.5 16v3a1.5 1.5 0 0 0 1.5 1.5h12a1.5 1.5 0 0 0 1.5-1.5v-3\"/>";
|
|
22
24
|
readonly undo: "<path d=\"M9 14L4 9l5-5\"/><path d=\"M4 9h10.5a5.5 5.5 0 0 1 0 11H11\"/>";
|
|
23
25
|
readonly user: "<circle cx=\"12\" cy=\"8\" r=\"4\"/><path d=\"M4 21a8 8 0 0 1 16 0\"/>";
|
|
24
26
|
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\"/>";
|
package/dist/icons.js
CHANGED
|
@@ -9,6 +9,7 @@ export const baseIcons = {
|
|
|
9
9
|
chevron: '<path d="M6 9l6 6 6-6"/>',
|
|
10
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
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
|
+
download: '<path d="M12 4v11M7.5 10.5L12 15l4.5-4.5"/><path d="M4.5 16v3a1.5 1.5 0 0 0 1.5 1.5h12a1.5 1.5 0 0 0 1.5-1.5v-3"/>',
|
|
12
13
|
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
14
|
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
15
|
home: '<path d="M3.5 11L12 4l8.5 7"/><path d="M6 9.5V20h4.5v-5.5h3V20H18V9.5"/>',
|
|
@@ -19,6 +20,7 @@ export const baseIcons = {
|
|
|
19
20
|
search: '<circle cx="11" cy="11" r="6.5"/><path d="M20 20l-4.4-4.4"/>',
|
|
20
21
|
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"/>',
|
|
21
22
|
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"/>',
|
|
23
|
+
upload: '<path d="M12 15V4M7.5 8.5L12 4l4.5 4.5"/><path d="M4.5 16v3a1.5 1.5 0 0 0 1.5 1.5h12a1.5 1.5 0 0 0 1.5-1.5v-3"/>',
|
|
22
24
|
undo: '<path d="M9 14L4 9l5-5"/><path d="M4 9h10.5a5.5 5.5 0 0 1 0 11H11"/>',
|
|
23
25
|
user: '<circle cx="12" cy="8" r="4"/><path d="M4 21a8 8 0 0 1 16 0"/>',
|
|
24
26
|
'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"/>',
|
package/dist/index.d.ts
CHANGED
|
@@ -10,5 +10,6 @@ export { default as Modal } from './Modal.svelte';
|
|
|
10
10
|
export { default as SortableTable } from './SortableTable.svelte';
|
|
11
11
|
export { default as Splash } from './Splash.svelte';
|
|
12
12
|
export { default as TabBar } from './TabBar.svelte';
|
|
13
|
+
export { PHONE_LAYOUT_QUERY, phoneLayout } from './layout.js';
|
|
13
14
|
export { baseIcons, hasIcon, iconMarkup, registerIcons, type BaseIconName, type IconName } from './icons.js';
|
|
14
15
|
export { sortRows, type Leaderboard, type SortableColumn, type TabItem } from './types.js';
|
package/dist/index.js
CHANGED
|
@@ -10,5 +10,6 @@ export { default as Modal } from './Modal.svelte';
|
|
|
10
10
|
export { default as SortableTable } from './SortableTable.svelte';
|
|
11
11
|
export { default as Splash } from './Splash.svelte';
|
|
12
12
|
export { default as TabBar } from './TabBar.svelte';
|
|
13
|
+
export { PHONE_LAYOUT_QUERY, phoneLayout } from './layout.js';
|
|
13
14
|
export { baseIcons, hasIcon, iconMarkup, registerIcons } from './icons.js';
|
|
14
15
|
export { sortRows } from './types.js';
|
package/dist/layout.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Screens that get the phone layout (a bottom `TabBar` instead of the `AppHeader`): narrow ones,
|
|
3
|
+
* and touch screens that are short, i.e. a phone held sideways. Tablets and desktops keep the
|
|
4
|
+
* header. Device type comes from how the screen is used (pointer, size), not the user agent, which
|
|
5
|
+
* can't tell an iPad from a Mac.
|
|
6
|
+
*/
|
|
7
|
+
export declare const PHONE_LAYOUT_QUERY = "(max-width: 40rem), (pointer: coarse) and (max-height: 30rem)";
|
|
8
|
+
/**
|
|
9
|
+
* Reactive `PHONE_LAYOUT_QUERY`: read `phoneLayout.current` in a component or effect. False where
|
|
10
|
+
* there's no `matchMedia` (server rendering, tests).
|
|
11
|
+
*/
|
|
12
|
+
export declare const phoneLayout: {
|
|
13
|
+
readonly current: boolean;
|
|
14
|
+
};
|
package/dist/layout.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { MediaQuery } from 'svelte/reactivity';
|
|
2
|
+
/**
|
|
3
|
+
* Screens that get the phone layout (a bottom `TabBar` instead of the `AppHeader`): narrow ones,
|
|
4
|
+
* and touch screens that are short, i.e. a phone held sideways. Tablets and desktops keep the
|
|
5
|
+
* header. Device type comes from how the screen is used (pointer, size), not the user agent, which
|
|
6
|
+
* can't tell an iPad from a Mac.
|
|
7
|
+
*/
|
|
8
|
+
export const PHONE_LAYOUT_QUERY = '(max-width: 40rem), (pointer: coarse) and (max-height: 30rem)';
|
|
9
|
+
let query;
|
|
10
|
+
/**
|
|
11
|
+
* Reactive `PHONE_LAYOUT_QUERY`: read `phoneLayout.current` in a component or effect. False where
|
|
12
|
+
* there's no `matchMedia` (server rendering, tests).
|
|
13
|
+
*/
|
|
14
|
+
export const phoneLayout = {
|
|
15
|
+
get current() {
|
|
16
|
+
if (typeof window === 'undefined' || typeof window.matchMedia !== 'function')
|
|
17
|
+
return false;
|
|
18
|
+
return (query ??= new MediaQuery(PHONE_LAYOUT_QUERY)).current;
|
|
19
|
+
}
|
|
20
|
+
};
|
package/dist/theme.css
CHANGED
|
@@ -109,6 +109,38 @@ input:not([type]) {
|
|
|
109
109
|
background: var(--surface);
|
|
110
110
|
}
|
|
111
111
|
|
|
112
|
+
/*
|
|
113
|
+
* Date and time inputs match the text inputs. iOS draws its own control that ignores the padding
|
|
114
|
+
* and changes size with the value, so there it's reset to a plain box of the same height.
|
|
115
|
+
*/
|
|
116
|
+
input[type='date'],
|
|
117
|
+
input[type='time'],
|
|
118
|
+
input[type='datetime-local'] {
|
|
119
|
+
font: inherit;
|
|
120
|
+
color: inherit;
|
|
121
|
+
width: 100%;
|
|
122
|
+
min-height: calc(1.4em + 1.2rem + 2px);
|
|
123
|
+
padding: 0.6rem 0.75rem;
|
|
124
|
+
border: 1px solid var(--border-strong);
|
|
125
|
+
border-radius: var(--radius);
|
|
126
|
+
background: var(--surface);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
@supports (-webkit-touch-callout: none) {
|
|
130
|
+
input[type='date'],
|
|
131
|
+
input[type='time'],
|
|
132
|
+
input[type='datetime-local'] {
|
|
133
|
+
-webkit-appearance: none;
|
|
134
|
+
appearance: none;
|
|
135
|
+
display: block;
|
|
136
|
+
}
|
|
137
|
+
input::-webkit-date-and-time-value {
|
|
138
|
+
margin: 0;
|
|
139
|
+
text-align: left;
|
|
140
|
+
min-height: 1.4em;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
112
144
|
input:focus-visible {
|
|
113
145
|
outline: none;
|
|
114
146
|
border-color: var(--accent);
|
|
@@ -124,6 +156,27 @@ button {
|
|
|
124
156
|
color: inherit;
|
|
125
157
|
}
|
|
126
158
|
|
|
159
|
+
/*
|
|
160
|
+
* Touch screens: text can't be selected by accident (a long press selects instead of acting like
|
|
161
|
+
* an app). Fields, and anything marked `.selectable` (reports, tables, logs), still can.
|
|
162
|
+
* `Leaderboards` and `SortableTable` are selectable.
|
|
163
|
+
*/
|
|
164
|
+
@media (hover: none) and (pointer: coarse) {
|
|
165
|
+
body {
|
|
166
|
+
-webkit-user-select: none;
|
|
167
|
+
user-select: none;
|
|
168
|
+
-webkit-touch-callout: none;
|
|
169
|
+
}
|
|
170
|
+
input,
|
|
171
|
+
textarea,
|
|
172
|
+
[contenteditable],
|
|
173
|
+
.selectable {
|
|
174
|
+
-webkit-user-select: text;
|
|
175
|
+
user-select: text;
|
|
176
|
+
-webkit-touch-callout: default;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
127
180
|
.visually-hidden {
|
|
128
181
|
position: absolute !important;
|
|
129
182
|
width: 1px;
|