@wells-nelson/displays 1.0.0 → 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 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`. Helpers: `.page`, `.page-title`,
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}
@@ -66,5 +76,5 @@ registerIcons({ calendar: '<rect …/>', folder: '<path …/>' }); // 24x24, str
66
76
  ```
67
77
 
68
78
  `Button` (primary / secondary / danger / ghost, optional icon, `block`), `IconButton` (button or
69
- link, required label, `active`), `Modal` (native `<dialog>`, sm / md) and `ConfirmDialog`
79
+ link, required label, `active`), `Modal` (native `<dialog>`, sm / md / lg) and `ConfirmDialog`
70
80
  (danger variant, busy state) complete the set. See them all in the sandbox (`npm run sandbox`).
package/dist/Modal.svelte CHANGED
@@ -15,7 +15,8 @@
15
15
  children: Snippet;
16
16
  footer?: Snippet;
17
17
  onclose?: () => void;
18
- size?: 'sm' | 'md';
18
+ /** sm: confirmations; md: forms (default); lg: near full width and height, for dense pickers. */
19
+ size?: 'sm' | 'md' | 'lg';
19
20
  } = $props();
20
21
 
21
22
  let dialog: HTMLDialogElement | undefined = $state();
@@ -66,6 +67,10 @@
66
67
  max-height: min(46rem, calc(100dvh - 2rem));
67
68
  box-shadow: var(--shadow-lg);
68
69
  }
70
+ .modal.lg {
71
+ width: min(64rem, calc(100vw - 2rem));
72
+ max-height: calc(100dvh - 2rem);
73
+ }
69
74
  .modal.sm {
70
75
  width: min(24rem, calc(100vw - 2rem));
71
76
  }
@@ -5,7 +5,8 @@ type $$ComponentProps = {
5
5
  children: Snippet;
6
6
  footer?: Snippet;
7
7
  onclose?: () => void;
8
- size?: 'sm' | 'md';
8
+ /** sm: confirmations; md: forms (default); lg: near full width and height, for dense pickers. */
9
+ size?: 'sm' | 'md' | 'lg';
9
10
  };
10
11
  declare const Modal: import("svelte").Component<$$ComponentProps, {}, "open">;
11
12
  type Modal = ReturnType<typeof Modal>;
@@ -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>
@@ -0,0 +1,8 @@
1
+ import type { TabItem } from './types.js';
2
+ type $$ComponentProps = {
3
+ items: TabItem[];
4
+ label?: string;
5
+ };
6
+ declare const TabBar: import("svelte").Component<$$ComponentProps, {}, "">;
7
+ type TabBar = ReturnType<typeof TabBar>;
8
+ export default TabBar;
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. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wells-nelson/displays",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "description": "Svelte 5 display components and theme CSS shared by Wells Nelson apps.",
5
5
  "license": "MIT",
6
6
  "repository": {