@broberg/app-shell 0.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 +99 -0
- package/css/app-shell.css +250 -0
- package/dist/index.cjs +204 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +131 -0
- package/dist/index.d.ts +131 -0
- package/dist/index.js +192 -0
- package/dist/index.js.map +1 -0
- package/dist/preact.cjs +686 -0
- package/dist/preact.cjs.map +1 -0
- package/dist/preact.d.cts +254 -0
- package/dist/preact.d.ts +254 -0
- package/dist/preact.js +665 -0
- package/dist/preact.js.map +1 -0
- package/package.json +74 -0
package/dist/preact.d.ts
ADDED
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
import { ComponentChildren, JSX } from 'preact';
|
|
2
|
+
import { NotificationRow } from '@broberg/notifications/types';
|
|
3
|
+
export { NotificationRow } from '@broberg/notifications/types';
|
|
4
|
+
|
|
5
|
+
/** One sidebar entry. `icon` is whatever your framework renders (an element). */
|
|
6
|
+
interface NavItem<Icon = unknown> {
|
|
7
|
+
id: string;
|
|
8
|
+
label: string;
|
|
9
|
+
href: string;
|
|
10
|
+
icon?: Icon;
|
|
11
|
+
/** A count shown on the item (99+ above 99). 0/undefined shows nothing. */
|
|
12
|
+
badge?: number;
|
|
13
|
+
}
|
|
14
|
+
interface NavGroup<Icon = unknown> {
|
|
15
|
+
label: string;
|
|
16
|
+
items: NavItem<Icon>[];
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Is this item the current page? Exact match for "/", otherwise the path is the
|
|
20
|
+
* item or below it ("/board/123" is on "/board", "/boardroom" is not).
|
|
21
|
+
*/
|
|
22
|
+
declare function isActivePath(href: string, currentPath: string): boolean;
|
|
23
|
+
/** "99+" above 99, nothing for 0 or less. */
|
|
24
|
+
declare function badgeLabel(n: number | undefined): string | null;
|
|
25
|
+
/** Read a boolean flag from localStorage; storage errors read as `fallback`. */
|
|
26
|
+
declare function readFlag(key: string, fallback: boolean): boolean;
|
|
27
|
+
declare function writeFlag(key: string, value: boolean): void;
|
|
28
|
+
declare function readSet(key: string): Set<string>;
|
|
29
|
+
declare function writeSet(key: string, value: Set<string>): void;
|
|
30
|
+
/**
|
|
31
|
+
* Two initials for the avatar fallback: first letter of the first and the last
|
|
32
|
+
* word, upper-cased. One word gives one letter; nothing gives "?".
|
|
33
|
+
*
|
|
34
|
+
* There is deliberately no Gravatar here (owner decision D-1dc849): the avatar
|
|
35
|
+
* is the Broberg ID `picture`, and without one, initials. Gravatar would send a
|
|
36
|
+
* hash of the user's email to a US service on every render.
|
|
37
|
+
*/
|
|
38
|
+
declare function initials(name: string | undefined, email?: string): string;
|
|
39
|
+
|
|
40
|
+
/** Where the bell gets its data. Swap it to point at any backend. */
|
|
41
|
+
interface NotificationSource {
|
|
42
|
+
/** The rows to show (newest first). */
|
|
43
|
+
loadRows(): Promise<NotificationRow[]>;
|
|
44
|
+
/** THE count — your server's, which may exclude muted kinds (F074's counting rule). */
|
|
45
|
+
countUnseen(): Promise<number>;
|
|
46
|
+
/** Returns the ids that actually transitioned to seen. */
|
|
47
|
+
markSeen(ids: readonly string[]): Promise<string[]>;
|
|
48
|
+
markAllSeen(): Promise<string[]>;
|
|
49
|
+
/** Called with a callback that re-counts; return an unsubscribe. Optional. */
|
|
50
|
+
subscribe?(onChange: () => void): () => void;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* The default source: a URL on your own backend, shaped after
|
|
54
|
+
* @broberg/notifications' server core so `createNotifications` sits right
|
|
55
|
+
* behind it.
|
|
56
|
+
*
|
|
57
|
+
* GET {url} → { items: NotificationRow[], unseen: number }
|
|
58
|
+
* POST {url}/seen { ids: string[] } → { clearedIds: string[], count: number }
|
|
59
|
+
* POST {url}/seen-all → { clearedIds: string[], count: number }
|
|
60
|
+
*
|
|
61
|
+
* It re-counts when the window regains focus or the tab becomes visible, and on
|
|
62
|
+
* a timer (default 60 s). A failed GET THROWS — the bell shows «could not
|
|
63
|
+
* load», never an empty list: "could not fetch" and "nothing new" must not look
|
|
64
|
+
* the same.
|
|
65
|
+
*/
|
|
66
|
+
declare function createFetchNotificationSource(opts: {
|
|
67
|
+
url: string;
|
|
68
|
+
fetch?: typeof fetch;
|
|
69
|
+
/** Re-count interval in ms. 0 turns it off. Default 60 000. */
|
|
70
|
+
pollMs?: number;
|
|
71
|
+
credentials?: RequestCredentials;
|
|
72
|
+
}): NotificationSource;
|
|
73
|
+
/** An in-memory source for demos and tests. */
|
|
74
|
+
declare function createMemoryNotificationSource(initial?: NotificationRow[]): NotificationSource & {
|
|
75
|
+
push(n: NotificationRow): void;
|
|
76
|
+
rows(): NotificationRow[];
|
|
77
|
+
};
|
|
78
|
+
/** "nu", "5 min", "3 t", "i går", "4 d" (da) or the English equivalents. */
|
|
79
|
+
declare function relativeTime(ts: string | number, lang: "da" | "en", now?: number): string;
|
|
80
|
+
declare const TEXT: {
|
|
81
|
+
readonly da: {
|
|
82
|
+
readonly openMenu: "Åbn menu";
|
|
83
|
+
readonly collapse: "Skjul sidemenu";
|
|
84
|
+
readonly expand: "Vis sidemenu";
|
|
85
|
+
readonly notifications: "Notifikationer";
|
|
86
|
+
readonly markAllRead: "Markér alle læst";
|
|
87
|
+
readonly noNotifications: "Ingen notifikationer endnu.";
|
|
88
|
+
readonly couldNotLoad: "Kunne ikke hente notifikationer.";
|
|
89
|
+
readonly seeAll: "Vis alle";
|
|
90
|
+
readonly close: "Luk";
|
|
91
|
+
readonly account: "Konto";
|
|
92
|
+
readonly signOut: "Log ud";
|
|
93
|
+
readonly theme: "Tema";
|
|
94
|
+
readonly light: "Lys";
|
|
95
|
+
readonly dark: "Mørk";
|
|
96
|
+
readonly system: "System";
|
|
97
|
+
readonly palette: "Palet";
|
|
98
|
+
readonly surfaces: "Flader";
|
|
99
|
+
readonly flat: "Flade";
|
|
100
|
+
readonly layered: "Lag";
|
|
101
|
+
readonly backdrop: "Baggrund";
|
|
102
|
+
readonly neurons: "Neuroner";
|
|
103
|
+
readonly plain: "Ren";
|
|
104
|
+
};
|
|
105
|
+
readonly en: {
|
|
106
|
+
readonly openMenu: "Open menu";
|
|
107
|
+
readonly collapse: "Hide sidebar";
|
|
108
|
+
readonly expand: "Show sidebar";
|
|
109
|
+
readonly notifications: "Notifications";
|
|
110
|
+
readonly markAllRead: "Mark all read";
|
|
111
|
+
readonly noNotifications: "No notifications yet.";
|
|
112
|
+
readonly couldNotLoad: "Could not load notifications.";
|
|
113
|
+
readonly seeAll: "See all";
|
|
114
|
+
readonly close: "Close";
|
|
115
|
+
readonly account: "Account";
|
|
116
|
+
readonly signOut: "Sign out";
|
|
117
|
+
readonly theme: "Theme";
|
|
118
|
+
readonly light: "Light";
|
|
119
|
+
readonly dark: "Dark";
|
|
120
|
+
readonly system: "System";
|
|
121
|
+
readonly palette: "Palette";
|
|
122
|
+
readonly surfaces: "Surfaces";
|
|
123
|
+
readonly flat: "Flat";
|
|
124
|
+
readonly layered: "Layered";
|
|
125
|
+
readonly backdrop: "Backdrop";
|
|
126
|
+
readonly neurons: "Neurons";
|
|
127
|
+
readonly plain: "Plain";
|
|
128
|
+
};
|
|
129
|
+
};
|
|
130
|
+
type Lang = keyof typeof TEXT;
|
|
131
|
+
|
|
132
|
+
/** @jsxImportSource preact */
|
|
133
|
+
|
|
134
|
+
type Icon = ComponentChildren;
|
|
135
|
+
type PreactNavItem = NavItem<Icon>;
|
|
136
|
+
type PreactNavGroup = NavGroup<Icon>;
|
|
137
|
+
/** Called instead of a full page load when given. Return false to let the browser navigate. */
|
|
138
|
+
type Navigate = (href: string) => void;
|
|
139
|
+
/** True below 768px. Follows the window as it resizes. */
|
|
140
|
+
declare function useIsMobile(): boolean;
|
|
141
|
+
interface SidebarProps {
|
|
142
|
+
groups: PreactNavGroup[];
|
|
143
|
+
footer?: PreactNavItem[];
|
|
144
|
+
currentPath: string;
|
|
145
|
+
onNavigate?: Navigate;
|
|
146
|
+
/** Desktop: collapsed to nothing (drawer mode) or to the icon rail (rail mode). */
|
|
147
|
+
collapsed?: boolean;
|
|
148
|
+
onToggleCollapse?: () => void;
|
|
149
|
+
/** Phone, drawer mode: is the drawer open. */
|
|
150
|
+
mobileOpen?: boolean;
|
|
151
|
+
onClose?: () => void;
|
|
152
|
+
mobile?: "drawer" | "rail";
|
|
153
|
+
/** Shown at the top of the sidebar (e.g. the brand on a phone). */
|
|
154
|
+
header?: ComponentChildren;
|
|
155
|
+
/** localStorage key for which groups are folded. */
|
|
156
|
+
groupsKey?: string;
|
|
157
|
+
lang: Lang;
|
|
158
|
+
}
|
|
159
|
+
declare function Sidebar(p: SidebarProps): JSX.Element;
|
|
160
|
+
interface NotificationBellProps {
|
|
161
|
+
/** Where the data comes from. Absent: the bell is still there, with an empty list. */
|
|
162
|
+
source?: NotificationSource;
|
|
163
|
+
onNavigate?: Navigate;
|
|
164
|
+
/** Required on purpose: a default would make "nobody chose" look like a choice. */
|
|
165
|
+
lang: Lang;
|
|
166
|
+
}
|
|
167
|
+
declare function NotificationBell({ source, onNavigate, lang }: NotificationBellProps): JSX.Element;
|
|
168
|
+
interface ShellUser {
|
|
169
|
+
name?: string;
|
|
170
|
+
email?: string;
|
|
171
|
+
/** Broberg ID OIDC `picture`. Without it: initials. Never Gravatar (D-1dc849). */
|
|
172
|
+
picture?: string;
|
|
173
|
+
}
|
|
174
|
+
interface UserMenuItem {
|
|
175
|
+
id: string;
|
|
176
|
+
label: string;
|
|
177
|
+
icon?: Icon;
|
|
178
|
+
href?: string;
|
|
179
|
+
onSelect?: () => void;
|
|
180
|
+
}
|
|
181
|
+
interface UserMenuProps {
|
|
182
|
+
user: ShellUser;
|
|
183
|
+
/** «Konto» → Broberg ID's account page. */
|
|
184
|
+
accountHref?: string;
|
|
185
|
+
items?: UserMenuItem[];
|
|
186
|
+
onSignOut?: () => void | Promise<void>;
|
|
187
|
+
onNavigate?: Navigate;
|
|
188
|
+
/** Show Theme · Palette · Surfaces · Backdrop. Default true. */
|
|
189
|
+
appearance?: boolean;
|
|
190
|
+
lang: Lang;
|
|
191
|
+
}
|
|
192
|
+
declare function Avatar({ user, size }: {
|
|
193
|
+
user: ShellUser;
|
|
194
|
+
size?: number;
|
|
195
|
+
}): JSX.Element;
|
|
196
|
+
declare function UserMenu(p: UserMenuProps): JSX.Element;
|
|
197
|
+
interface TopBarProps {
|
|
198
|
+
brand?: ComponentChildren;
|
|
199
|
+
/** Anything app-specific (project switcher, search) between brand and bell. */
|
|
200
|
+
actions?: ComponentChildren;
|
|
201
|
+
/** Drawer mode on a phone: show the hamburger and call this. */
|
|
202
|
+
onOpenMenu?: () => void;
|
|
203
|
+
notifications?: NotificationSource;
|
|
204
|
+
user?: ShellUser;
|
|
205
|
+
userMenu?: Omit<UserMenuProps, "user" | "lang" | "onNavigate">;
|
|
206
|
+
onNavigate?: Navigate;
|
|
207
|
+
lang: Lang;
|
|
208
|
+
}
|
|
209
|
+
/** Brand left, actions, then the notification bell and the user menu ALWAYS last, in that order. */
|
|
210
|
+
declare function TopBar(p: TopBarProps): JSX.Element;
|
|
211
|
+
declare function PageHeader(p: {
|
|
212
|
+
eyebrow?: ComponentChildren;
|
|
213
|
+
title: ComponentChildren;
|
|
214
|
+
description?: ComponentChildren;
|
|
215
|
+
actions?: ComponentChildren;
|
|
216
|
+
}): JSX.Element;
|
|
217
|
+
interface PageTab {
|
|
218
|
+
id: string;
|
|
219
|
+
label: ComponentChildren;
|
|
220
|
+
/** Path mode: the tab's own route. Query mode: ignored (?tab=id is used). */
|
|
221
|
+
href?: string;
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* Tabs as routes. `mode: "query"` puts the tab in `?tab=<id>` on the current
|
|
225
|
+
* path (first tab when absent); `mode: "path"` gives each tab its own href.
|
|
226
|
+
*/
|
|
227
|
+
declare function PageTabs(p: {
|
|
228
|
+
tabs: PageTab[];
|
|
229
|
+
currentUrl: string;
|
|
230
|
+
mode?: "query" | "path";
|
|
231
|
+
onNavigate?: Navigate;
|
|
232
|
+
}): JSX.Element;
|
|
233
|
+
interface AppShellProps {
|
|
234
|
+
brand?: ComponentChildren;
|
|
235
|
+
actions?: ComponentChildren;
|
|
236
|
+
groups: PreactNavGroup[];
|
|
237
|
+
footer?: PreactNavItem[];
|
|
238
|
+
currentPath: string;
|
|
239
|
+
onNavigate?: Navigate;
|
|
240
|
+
/** The notification centre's source. The bell is shown either way. */
|
|
241
|
+
notifications?: NotificationSource;
|
|
242
|
+
user?: ShellUser;
|
|
243
|
+
userMenu?: Omit<UserMenuProps, "user" | "lang" | "onNavigate">;
|
|
244
|
+
/** Phone behaviour: a slide-in drawer behind a hamburger, or a 60px icon rail. */
|
|
245
|
+
mobile?: "drawer" | "rail";
|
|
246
|
+
/** localStorage key prefix for the collapsed sidebar and folded groups. */
|
|
247
|
+
storageKey?: string;
|
|
248
|
+
/** Required: the shell ships no default language (see @broberg/notifications/shell). */
|
|
249
|
+
lang: Lang;
|
|
250
|
+
children?: ComponentChildren;
|
|
251
|
+
}
|
|
252
|
+
declare function AppShell(p: AppShellProps): JSX.Element;
|
|
253
|
+
|
|
254
|
+
export { AppShell, type AppShellProps, Avatar, type Lang, type NavGroup, type NavItem, type Navigate, NotificationBell, type NotificationBellProps, type NotificationSource, PageHeader, type PageTab, PageTabs, type PreactNavGroup, type PreactNavItem, type ShellUser, Sidebar, type SidebarProps, TEXT, TopBar, type TopBarProps, UserMenu, type UserMenuItem, type UserMenuProps, badgeLabel, createFetchNotificationSource, createMemoryNotificationSource, initials, isActivePath, readFlag, readSet, relativeTime, useIsMobile, writeFlag, writeSet };
|