@wells-nelson/displays 1.0.1 → 1.0.2
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 +12 -2
- package/dist/TabBar.svelte +113 -0
- package/dist/TabBar.svelte.d.ts +8 -0
- package/dist/icons.d.ts +1 -0
- package/dist/icons.js +1 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +1 -0
- package/dist/theme.css +3 -0
- package/dist/types.d.ts +15 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -15,13 +15,14 @@ import '@wells-nelson/displays/palettes/green.css';
|
|
|
15
15
|
Tokens: `--bg`, `--surface`, `--surface-2`, `--surface-hover`, `--text`, `--muted`, `--border`,
|
|
16
16
|
`--border-strong`, `--accent`, `--accent-strong`, `--accent-text`, `--accent-soft`, `--on-accent`,
|
|
17
17
|
`--danger`, `--danger-soft`, `--warning`, `--warning-soft`, `--radius`, `--radius-lg`, `--shadow`,
|
|
18
|
-
`--shadow-lg`, `--header-h`, `--header-offset`, `--content-w
|
|
18
|
+
`--shadow-lg`, `--header-h`, `--header-offset`, `--content-w`, `--tabbar-h`, `--footer-offset`
|
|
19
|
+
(the space fixed bottom UI leaves for a `TabBar`; 0 without one). Helpers: `.page`, `.page-title`,
|
|
19
20
|
`.stack`, `.card`, `.row-card`, `.muted`, `.notice`, `.empty`, `.visually-hidden`, plus
|
|
20
21
|
drag-and-drop classes. Keep app-only tokens in the app's own stylesheet.
|
|
21
22
|
|
|
22
23
|
## Icons
|
|
23
24
|
|
|
24
|
-
The base set: alert, check, chevron, cloud, cloud-pending, gear, grip, mail, pencil, plus,
|
|
25
|
+
The base set: alert, check, chevron, cloud, cloud-pending, gear, grip, home, mail, pencil, plus,
|
|
25
26
|
refresh, search, share, trash, undo, user, wifi-off, x. Register app icons at startup:
|
|
26
27
|
|
|
27
28
|
```ts
|
|
@@ -46,6 +47,15 @@ registerIcons({ calendar: '<rect …/>', folder: '<path …/>' }); // 24x24, str
|
|
|
46
47
|
{#snippet nav()}<IconButton icon="calendar" label="Schedule" href={…} active={…} />{/snippet}
|
|
47
48
|
</AppHeader>
|
|
48
49
|
|
|
50
|
+
<!-- Phones: a bottom tab bar instead of the header (set --header-h: 0px at that width). -->
|
|
51
|
+
<TabBar
|
|
52
|
+
items={[
|
|
53
|
+
{ label: 'Home', icon: 'home', href: resolve('/'), active: routeId === '/' },
|
|
54
|
+
{ label: 'Schedule', icon: 'calendar', href: projectId ? scheduleHref : undefined }, // no href: disabled
|
|
55
|
+
{ label: 'Profile', icon: 'user', href: resolve('/profile'), imageUrl: avatar, dot: unsynced, dotLabel: 'Unsynced changes' }
|
|
56
|
+
]}
|
|
57
|
+
/>
|
|
58
|
+
|
|
49
59
|
<SortableTable
|
|
50
60
|
title="Members"
|
|
51
61
|
rows={stats}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import Icon from './Icon.svelte';
|
|
3
|
+
import type { TabItem } from './types.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Fixed bottom navigation for phones: one tab per top-level destination, icon over label.
|
|
7
|
+
* Use it instead of `AppHeader` on small screens (set `--header-h: 0px` there so the page
|
|
8
|
+
* starts at the top). While mounted it sets `--footer-offset` on <html>, which `SearchBar`,
|
|
9
|
+
* `UndoButton` and other fixed bottom UI use to sit above it, and it leaves a spacer of its
|
|
10
|
+
* height at the end of the page.
|
|
11
|
+
*/
|
|
12
|
+
let { items, label = 'Main' }: { items: TabItem[]; label?: string } = $props();
|
|
13
|
+
|
|
14
|
+
$effect(() => {
|
|
15
|
+
const root = document.documentElement.style;
|
|
16
|
+
root.setProperty('--footer-offset', 'calc(var(--tabbar-h) + env(safe-area-inset-bottom, 0px))');
|
|
17
|
+
return () => root.removeProperty('--footer-offset');
|
|
18
|
+
});
|
|
19
|
+
</script>
|
|
20
|
+
|
|
21
|
+
{#snippet content(item: TabItem)}
|
|
22
|
+
<span class="icon">
|
|
23
|
+
{#if item.imageUrl}
|
|
24
|
+
<img src={item.imageUrl} alt="" referrerpolicy="no-referrer" />
|
|
25
|
+
{:else}
|
|
26
|
+
<Icon name={item.icon} size={24} />
|
|
27
|
+
{/if}
|
|
28
|
+
{#if item.dot}
|
|
29
|
+
<span class="dot" title={item.dotLabel}></span>
|
|
30
|
+
{/if}
|
|
31
|
+
</span>
|
|
32
|
+
<span class="label">{item.label}</span>
|
|
33
|
+
{#if item.dot && item.dotLabel}<span class="visually-hidden">({item.dotLabel})</span>{/if}
|
|
34
|
+
{/snippet}
|
|
35
|
+
|
|
36
|
+
<div class="spacer" aria-hidden="true"></div>
|
|
37
|
+
<nav class="tab-bar" aria-label={label}>
|
|
38
|
+
{#each items as item (item.label)}
|
|
39
|
+
{#if item.href}
|
|
40
|
+
<a class="tab" class:active={item.active} href={item.href} aria-current={item.active ? 'page' : undefined}>
|
|
41
|
+
{@render content(item)}
|
|
42
|
+
</a>
|
|
43
|
+
{:else}
|
|
44
|
+
<span class="tab disabled" aria-disabled="true">{@render content(item)}</span>
|
|
45
|
+
{/if}
|
|
46
|
+
{/each}
|
|
47
|
+
</nav>
|
|
48
|
+
|
|
49
|
+
<style>
|
|
50
|
+
.spacer {
|
|
51
|
+
height: calc(var(--tabbar-h) + env(safe-area-inset-bottom, 0px));
|
|
52
|
+
}
|
|
53
|
+
.tab-bar {
|
|
54
|
+
position: fixed;
|
|
55
|
+
left: 0;
|
|
56
|
+
right: 0;
|
|
57
|
+
bottom: 0;
|
|
58
|
+
z-index: 50;
|
|
59
|
+
display: grid;
|
|
60
|
+
grid-auto-flow: column;
|
|
61
|
+
grid-auto-columns: 1fr;
|
|
62
|
+
padding-bottom: env(safe-area-inset-bottom, 0px);
|
|
63
|
+
background: var(--surface);
|
|
64
|
+
border-top: 1px solid var(--border);
|
|
65
|
+
}
|
|
66
|
+
.tab {
|
|
67
|
+
display: flex;
|
|
68
|
+
flex-direction: column;
|
|
69
|
+
align-items: center;
|
|
70
|
+
justify-content: center;
|
|
71
|
+
gap: 0.15rem;
|
|
72
|
+
height: var(--tabbar-h);
|
|
73
|
+
color: var(--muted);
|
|
74
|
+
font-size: 0.72rem;
|
|
75
|
+
font-weight: 600;
|
|
76
|
+
text-decoration: none;
|
|
77
|
+
-webkit-touch-callout: none;
|
|
78
|
+
user-select: none;
|
|
79
|
+
}
|
|
80
|
+
.icon {
|
|
81
|
+
position: relative;
|
|
82
|
+
display: grid;
|
|
83
|
+
place-items: center;
|
|
84
|
+
width: 3.5rem;
|
|
85
|
+
height: 1.9rem;
|
|
86
|
+
border-radius: 1rem;
|
|
87
|
+
transition: background-color 0.15s;
|
|
88
|
+
}
|
|
89
|
+
.active {
|
|
90
|
+
color: var(--accent-text);
|
|
91
|
+
}
|
|
92
|
+
.active .icon {
|
|
93
|
+
background: var(--accent-soft);
|
|
94
|
+
}
|
|
95
|
+
.disabled {
|
|
96
|
+
opacity: 0.4;
|
|
97
|
+
}
|
|
98
|
+
img {
|
|
99
|
+
width: 1.6rem;
|
|
100
|
+
height: 1.6rem;
|
|
101
|
+
border-radius: 50%;
|
|
102
|
+
}
|
|
103
|
+
.dot {
|
|
104
|
+
position: absolute;
|
|
105
|
+
top: 0.1rem;
|
|
106
|
+
right: 0.95rem;
|
|
107
|
+
width: 0.6rem;
|
|
108
|
+
height: 0.6rem;
|
|
109
|
+
border-radius: 50%;
|
|
110
|
+
background: var(--warning);
|
|
111
|
+
border: 2px solid var(--surface);
|
|
112
|
+
}
|
|
113
|
+
</style>
|
package/dist/icons.d.ts
CHANGED
|
@@ -11,6 +11,7 @@ export declare const baseIcons: {
|
|
|
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
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
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 home: "<path d=\"M3.5 11L12 4l8.5 7\"/><path d=\"M6 9.5V20h4.5v-5.5h3V20H18V9.5\"/>";
|
|
14
15
|
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
16
|
readonly pencil: "<path d=\"M4 20h4L19 9a2.8 2.8 0 0 0-4-4L4 16z\"/><path d=\"M13.5 6.5l4 4\"/>";
|
|
16
17
|
readonly plus: "<path d=\"M12 5v14M5 12h14\"/>";
|
package/dist/icons.js
CHANGED
|
@@ -11,6 +11,7 @@ export const baseIcons = {
|
|
|
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
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
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
|
+
home: '<path d="M3.5 11L12 4l8.5 7"/><path d="M6 9.5V20h4.5v-5.5h3V20H18V9.5"/>',
|
|
14
15
|
mail: '<rect x="3" y="5.5" width="18" height="13" rx="2"/><path d="M3.5 7l8.5 6 8.5-6"/>',
|
|
15
16
|
pencil: '<path d="M4 20h4L19 9a2.8 2.8 0 0 0-4-4L4 16z"/><path d="M13.5 6.5l4 4"/>',
|
|
16
17
|
plus: '<path d="M12 5v14M5 12h14"/>',
|
package/dist/index.d.ts
CHANGED
|
@@ -9,5 +9,6 @@ export { default as Leaderboards } from './Leaderboards.svelte';
|
|
|
9
9
|
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
|
+
export { default as TabBar } from './TabBar.svelte';
|
|
12
13
|
export { baseIcons, hasIcon, iconMarkup, registerIcons, type BaseIconName, type IconName } from './icons.js';
|
|
13
|
-
export { sortRows, type Leaderboard, type SortableColumn } from './types.js';
|
|
14
|
+
export { sortRows, type Leaderboard, type SortableColumn, type TabItem } from './types.js';
|
package/dist/index.js
CHANGED
|
@@ -9,5 +9,6 @@ export { default as Leaderboards } from './Leaderboards.svelte';
|
|
|
9
9
|
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
|
+
export { default as TabBar } from './TabBar.svelte';
|
|
12
13
|
export { baseIcons, hasIcon, iconMarkup, registerIcons } from './icons.js';
|
|
13
14
|
export { sortRows } from './types.js';
|
package/dist/theme.css
CHANGED
|
@@ -35,6 +35,9 @@
|
|
|
35
35
|
--header-h: 3.5rem;
|
|
36
36
|
--header-offset: calc(var(--header-h) + env(safe-area-inset-top, 0px));
|
|
37
37
|
--content-w: 42rem;
|
|
38
|
+
/* Bottom tab bar height (TabBar), and the space fixed bottom UI leaves for it (0 without one). */
|
|
39
|
+
--tabbar-h: 3.75rem;
|
|
40
|
+
--footer-offset: 0px;
|
|
38
41
|
font-family:
|
|
39
42
|
system-ui,
|
|
40
43
|
-apple-system,
|
package/dist/types.d.ts
CHANGED
|
@@ -1,3 +1,18 @@
|
|
|
1
|
+
import type { IconName } from './icons.js';
|
|
2
|
+
/** One destination of `TabBar`. */
|
|
3
|
+
export interface TabItem {
|
|
4
|
+
label: string;
|
|
5
|
+
icon: IconName;
|
|
6
|
+
/** Where it goes; without one the tab shows disabled (e.g. nothing to open yet). */
|
|
7
|
+
href?: string;
|
|
8
|
+
/** The current section (highlighted, `aria-current="page"`). */
|
|
9
|
+
active?: boolean;
|
|
10
|
+
/** A round image instead of the icon, e.g. the signed-in user's avatar. */
|
|
11
|
+
imageUrl?: string | null;
|
|
12
|
+
/** A warning dot on the icon, e.g. unsynced changes; `dotLabel` says what it means. */
|
|
13
|
+
dot?: boolean;
|
|
14
|
+
dotLabel?: string;
|
|
15
|
+
}
|
|
1
16
|
/** A sortable column of `SortableTable`. */
|
|
2
17
|
export interface SortableColumn<T> {
|
|
3
18
|
/** Unique key; also what `sortKey` refers to. */
|