@wells-nelson/displays 1.0.3 → 1.1.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/README.md +27 -3
- 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/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,6 +20,11 @@ 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
30
|
The base set: alert, check, chevron, cloud, cloud-pending, download, gear, grip, home, mail, pencil, plus,
|
|
@@ -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/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;
|