@columenlabs/lumen-ui 0.1.71 → 0.1.73
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 +1 -1
- package/dist/app-switcher.d.ts +40 -5
- package/dist/app-switcher.js +99 -5
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -57,7 +57,7 @@ Use `createDeskQueryClient()` for the app's TanStack Query client: data stays fr
|
|
|
57
57
|
|
|
58
58
|
## What's inside
|
|
59
59
|
|
|
60
|
-
**Frame.** `LumenShell` (sidebar with icons-only collapse and a drawer on phone, header, status bar, Settings, profile menu, ⌘K Find), `LumenAppSwitcher
|
|
60
|
+
**Frame.** `LumenShell` (sidebar with icons-only collapse and a drawer on phone, header, status bar, Settings, profile menu, ⌘K Find), `LumenAppSwitcher` and `LumenSuiteSwitcher` (groups, apps on other sites, a list read from an address), `WhatsNew` (release notes in a drawer), `TasksButton` and `TasksProvider` (background work in the header), `NotificationsSettings`, `McpSettings`, sign-in screens (`LumenAuthStage`, `LumenSecondStep`, `LumenSignInMethods`, `LumenBackupCodes`), `StatusHealth`, `LumenBoundary` (keeps a crash inside its card), `LumenPageState` (loading, empty and error), `lumenToast`.
|
|
61
61
|
|
|
62
62
|
**Pages and views.** `LumenLayout` (a page of cards from one view config: filter, tiles, widgets; Edit layout and Edit widget when you pass `onConfigChange`), `LumenGrid` (the 6-column grid under it), `LumenView` and `LumenViewCard` (a table that filters, groups, pivots and totals the rows you load), `LumenTable`, `LumenTokenizedFilter`, `LumenWidget` and `LumenSavedWidget` (the frame every card sits in, with its link square and controls), `LumenCard` (one number and its lines), `LumenPager`, `DetailsTable`, `LumenTree`, `useAppView` (a page whose layout each person can change and reset).
|
|
63
63
|
|
package/dist/app-switcher.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { type
|
|
2
|
-
import type {
|
|
1
|
+
import { type ReactNode } from 'react';
|
|
2
|
+
import type { NavIcon } from './nav.js';
|
|
3
3
|
/** One app of the product. */
|
|
4
4
|
export interface LumenApp {
|
|
5
5
|
id: string;
|
|
@@ -7,15 +7,50 @@ export interface LumenApp {
|
|
|
7
7
|
name: string;
|
|
8
8
|
/** How its name is drawn (e.g. the app's accent, then a shared suffix); `name` when left out. */
|
|
9
9
|
label?: ReactNode;
|
|
10
|
-
|
|
10
|
+
/** Its mark; `iconUrl`, then the name's first letter, when left out. */
|
|
11
|
+
icon?: NavIcon;
|
|
12
|
+
/** An image of its mark (another site's favicon). */
|
|
13
|
+
iconUrl?: string;
|
|
11
14
|
href: string;
|
|
15
|
+
/** The heading it sits under; apps with none lead the menu under `menuLabel`. */
|
|
16
|
+
group?: string;
|
|
17
|
+
/** On another site: the menu shows its host and an arrow. */
|
|
18
|
+
external?: boolean;
|
|
12
19
|
}
|
|
13
20
|
export declare function LumenAppSwitcher({ current, apps, product, menuLabel, }: {
|
|
14
21
|
/** The app this page belongs to; it leads `apps` even when the menu leaves it out. */
|
|
15
22
|
current: LumenApp;
|
|
16
|
-
/** The apps the menu lists, in order. */
|
|
23
|
+
/** The apps the menu lists, in order; a group shows where its first app is. */
|
|
17
24
|
apps: LumenApp[];
|
|
18
|
-
/** The product's name under the app's. */
|
|
25
|
+
/** The product's name under the app's (or where it is, such as its host). */
|
|
26
|
+
product?: string;
|
|
27
|
+
menuLabel?: string;
|
|
28
|
+
}): import("react").JSX.Element;
|
|
29
|
+
/** One app as a suite list gives it: `icon` is a lucide name, `url` an absolute address. */
|
|
30
|
+
export interface SuiteApp {
|
|
31
|
+
id: string;
|
|
32
|
+
name: string;
|
|
33
|
+
url: string;
|
|
34
|
+
icon?: string;
|
|
35
|
+
iconUrl?: string;
|
|
36
|
+
group?: string;
|
|
37
|
+
}
|
|
38
|
+
/** What a suite list address answers. */
|
|
39
|
+
export interface SuiteList {
|
|
40
|
+
apps: SuiteApp[];
|
|
41
|
+
}
|
|
42
|
+
/** The suite's apps as switcher apps; an app on another host is `external`. */
|
|
43
|
+
export declare function suiteApps(list: SuiteList, here: URL): LumenApp[];
|
|
44
|
+
/**
|
|
45
|
+
* The switcher of an app that joins a suite someone else runs: it reads the suite's apps from
|
|
46
|
+
* `source` (sent with this browser's cookies, so the suite must allow this origin) and marks this
|
|
47
|
+
* app in it. With no `source`, or when the list can't be read, the menu lists only `current`.
|
|
48
|
+
*/
|
|
49
|
+
export declare function LumenSuiteSwitcher({ source, current, product, menuLabel, }: {
|
|
50
|
+
/** The suite list address (answers a `SuiteList`); none: no suite. */
|
|
51
|
+
source?: string;
|
|
52
|
+
/** This app; the list's entry for this page's address takes its place. */
|
|
53
|
+
current: LumenApp;
|
|
19
54
|
product?: string;
|
|
20
55
|
menuLabel?: string;
|
|
21
56
|
}): import("react").JSX.Element;
|
package/dist/app-switcher.js
CHANGED
|
@@ -1,12 +1,106 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
// The app switcher (in Lumen for any product with more than one app): the
|
|
3
3
|
// sidebar's top shows the current app and opens a menu of the product's apps. LumenShell's `brand`
|
|
4
|
-
// takes it; the product passes the apps the person can open.
|
|
5
|
-
|
|
4
|
+
// takes it; the product passes the apps the person can open. Apps may sit in groups, and an app on
|
|
5
|
+
// another site (`external`) shows its host and an arrow, so a person can tell a page of this app from
|
|
6
|
+
// a separate one. `LumenSuiteSwitcher` reads the list from an address instead, for an app that
|
|
7
|
+
// joins a suite it doesn't run (the address is set per deployment; none, the menu lists only itself).
|
|
8
|
+
import { useQuery } from '@tanstack/react-query';
|
|
9
|
+
import { ArrowUpRightIcon, ChevronsUpDownIcon } from 'lucide-react';
|
|
10
|
+
import { Fragment, useState } from 'react';
|
|
6
11
|
import { cn } from './lib/utils.js';
|
|
7
|
-
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuTrigger, } from './ui/dropdown-menu.js';
|
|
12
|
+
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger, } from './ui/dropdown-menu.js';
|
|
8
13
|
import { SidebarMenu, SidebarMenuButton, SidebarMenuItem, useSidebar } from './ui/sidebar.js';
|
|
14
|
+
import { lucideIcon } from './view-icons.js';
|
|
15
|
+
function AppMark({ app, className }) {
|
|
16
|
+
const [broken, setBroken] = useState(false);
|
|
17
|
+
if (app.icon)
|
|
18
|
+
return _jsx(app.icon, { className: className });
|
|
19
|
+
if (app.iconUrl && !broken)
|
|
20
|
+
return (_jsx("img", { src: app.iconUrl, alt: "", className: cn(className, 'object-contain'), onError: () => setBroken(true) }));
|
|
21
|
+
return (_jsx("span", { className: "text-xs font-semibold leading-none", children: app.name.slice(0, 1).toUpperCase() }));
|
|
22
|
+
}
|
|
23
|
+
const hostOf = (href) => {
|
|
24
|
+
try {
|
|
25
|
+
return new URL(href).host;
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return href;
|
|
29
|
+
}
|
|
30
|
+
};
|
|
9
31
|
export function LumenAppSwitcher({ current, apps, product, menuLabel = 'Apps', }) {
|
|
10
32
|
const { isMobile } = useSidebar();
|
|
11
|
-
|
|
33
|
+
const groups = [];
|
|
34
|
+
for (const a of apps) {
|
|
35
|
+
const label = a.group ?? menuLabel;
|
|
36
|
+
const g = groups.find((x) => x.label === label);
|
|
37
|
+
if (g)
|
|
38
|
+
g.apps.push(a);
|
|
39
|
+
else if (a.group)
|
|
40
|
+
groups.push({ label, apps: [a] });
|
|
41
|
+
else
|
|
42
|
+
groups.unshift({ label, apps: [a] });
|
|
43
|
+
}
|
|
44
|
+
return (_jsx(SidebarMenu, { children: _jsx(SidebarMenuItem, { children: _jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsxs(SidebarMenuButton, { size: "lg", tooltip: current.name, className: "data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground", children: [_jsx("div", { className: "flex aspect-square size-8 items-center justify-center rounded-lg bg-sidebar-primary text-sidebar-primary-foreground", children: _jsx(AppMark, { app: current, className: "size-4" }) }), _jsxs("div", { className: "grid flex-1 text-left text-sm leading-tight", children: [_jsx("span", { className: "truncate font-semibold", children: current.label ?? current.name }), product && (_jsx("span", { className: "truncate text-xs text-muted-foreground", children: product }))] }), _jsx(ChevronsUpDownIcon, { className: "ml-auto" })] }) }), _jsx(DropdownMenuContent, { className: "min-w-56 max-w-96 rounded-lg", align: "start", side: isMobile ? 'bottom' : 'right', sideOffset: 4, children: groups.map((g, i) => (_jsxs(Fragment, { children: [i > 0 && _jsx(DropdownMenuSeparator, {}), _jsx(DropdownMenuLabel, { className: "text-xs text-muted-foreground", children: g.label }), g.apps.map((a) => (_jsx(DropdownMenuItem, { asChild: true, className: cn('gap-2 p-2', a.id === current.id && 'bg-muted'), children: _jsxs("a", { href: a.href, "aria-current": a.id === current.id ? 'page' : undefined, children: [_jsx("div", { className: "flex size-6 shrink-0 items-center justify-center rounded-md border", children: _jsx(AppMark, { app: a, className: "size-3.5 shrink-0" }) }), _jsx("span", { className: cn('truncate', a.external && 'shrink-0'), children: a.label ?? a.name }), a.external && (_jsxs(_Fragment, { children: [_jsx("span", { className: "ml-auto min-w-0 truncate text-xs text-muted-foreground", children: hostOf(a.href) }), _jsx(ArrowUpRightIcon, { className: "size-3.5 shrink-0 text-muted-foreground" })] }))] }) }, a.id)))] }, g.label))) })] }) }) }));
|
|
45
|
+
}
|
|
46
|
+
/** How far `url` matches this page: its host, then the longest path it starts. */
|
|
47
|
+
const matchLength = (url, here) => {
|
|
48
|
+
try {
|
|
49
|
+
const u = new URL(url);
|
|
50
|
+
if (u.host !== here.host)
|
|
51
|
+
return -1;
|
|
52
|
+
const path = u.pathname.replace(/\/$/, '');
|
|
53
|
+
return path === '' || here.pathname === path || here.pathname.startsWith(`${path}/`)
|
|
54
|
+
? path.length
|
|
55
|
+
: -1;
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
return -1;
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
/** The suite's apps as switcher apps; an app on another host is `external`. */
|
|
62
|
+
export function suiteApps(list, here) {
|
|
63
|
+
return list.apps.map((a) => ({
|
|
64
|
+
id: a.id,
|
|
65
|
+
name: a.name,
|
|
66
|
+
href: a.url,
|
|
67
|
+
icon: a.icon ? lucideIcon(a.icon) : undefined,
|
|
68
|
+
iconUrl: a.iconUrl,
|
|
69
|
+
group: a.group,
|
|
70
|
+
external: hostOf(a.url) !== here.host,
|
|
71
|
+
}));
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* The switcher of an app that joins a suite someone else runs: it reads the suite's apps from
|
|
75
|
+
* `source` (sent with this browser's cookies, so the suite must allow this origin) and marks this
|
|
76
|
+
* app in it. With no `source`, or when the list can't be read, the menu lists only `current`.
|
|
77
|
+
*/
|
|
78
|
+
export function LumenSuiteSwitcher({ source, current, product, menuLabel, }) {
|
|
79
|
+
const list = useQuery({
|
|
80
|
+
queryKey: ['lumen-suite', source],
|
|
81
|
+
enabled: !!source,
|
|
82
|
+
queryFn: async () => {
|
|
83
|
+
const res = await fetch(source, { credentials: 'include' });
|
|
84
|
+
if (!res.ok)
|
|
85
|
+
throw new Error(`suite list ${res.status}`);
|
|
86
|
+
return res.json();
|
|
87
|
+
},
|
|
88
|
+
staleTime: 5 * 60_000,
|
|
89
|
+
retry: false,
|
|
90
|
+
});
|
|
91
|
+
const here = new URL(location.href);
|
|
92
|
+
const apps = list.data ? suiteApps(list.data, here) : [];
|
|
93
|
+
// This page's entry is the one whose address it sits under (longest path wins); this app keeps its own mark.
|
|
94
|
+
let best = -1;
|
|
95
|
+
let mine;
|
|
96
|
+
for (const a of list.data?.apps ?? []) {
|
|
97
|
+
const n = matchLength(a.url, here);
|
|
98
|
+
if (n > best) {
|
|
99
|
+
best = n;
|
|
100
|
+
mine = a.id;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
const shown = apps.map((a) => a.id === mine ? { ...a, ...current, id: a.id, group: a.group, external: false } : a);
|
|
104
|
+
const me = shown.find((a) => a.id === mine) ?? current;
|
|
105
|
+
return (_jsx(LumenAppSwitcher, { current: me, apps: shown.length ? shown : [current], product: product ?? here.host, menuLabel: menuLabel }));
|
|
12
106
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -58,7 +58,7 @@ export { RouteError, isStaleBuildError, reloadForNewBuild, reportError, watchNew
|
|
|
58
58
|
export { LumenSettings, SettingRow, SettingsActions, SettingsFooter, type SettingsGroupDef, type SettingsSectionDef, } from './settings.js';
|
|
59
59
|
export { LumenCard } from './summary-cards.js';
|
|
60
60
|
export { WhatsNew } from './whats-new.js';
|
|
61
|
-
export { type LumenApp, LumenAppSwitcher } from './app-switcher.js';
|
|
61
|
+
export { type LumenApp, LumenAppSwitcher, LumenSuiteSwitcher, type SuiteApp, type SuiteList, suiteApps, } from './app-switcher.js';
|
|
62
62
|
export { type BriefActions, type BriefChange, type ScheduleChange, type ScheduleItem, type ScheduleRun, SchedulesSection, } from './schedules.js';
|
|
63
63
|
export { NotificationsSettings } from './notifications.js';
|
|
64
64
|
export { type TasksConfig, TasksButton, TasksProvider, useTasks } from './tasks.js';
|
package/dist/index.js
CHANGED
|
@@ -65,7 +65,7 @@ export { RouteError, isStaleBuildError, reloadForNewBuild, reportError, watchNew
|
|
|
65
65
|
export { LumenSettings, SettingRow, SettingsActions, SettingsFooter, } from './settings.js';
|
|
66
66
|
export { LumenCard } from './summary-cards.js';
|
|
67
67
|
export { WhatsNew } from './whats-new.js';
|
|
68
|
-
export { LumenAppSwitcher } from './app-switcher.js';
|
|
68
|
+
export { LumenAppSwitcher, LumenSuiteSwitcher, suiteApps, } from './app-switcher.js';
|
|
69
69
|
export { SchedulesSection, } from './schedules.js';
|
|
70
70
|
export { NotificationsSettings } from './notifications.js';
|
|
71
71
|
export { TasksButton, TasksProvider, useTasks } from './tasks.js';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@columenlabs/lumen-ui",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.73",
|
|
4
4
|
"description": "Lumen: shared React components (frame, views, widgets, charts, drawers).",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
"@codemirror/lang-javascript": "6.2.5",
|
|
46
46
|
"@codemirror/lang-json": "6.0.2",
|
|
47
47
|
"@codemirror/lang-sql": "6.10.0",
|
|
48
|
-
"@columenlabs/lumen-ui-types": "0.1.
|
|
48
|
+
"@columenlabs/lumen-ui-types": "0.1.73",
|
|
49
49
|
"@dnd-kit/core": "6.3.1",
|
|
50
50
|
"@dnd-kit/sortable": "10.0.0",
|
|
51
51
|
"@dnd-kit/utilities": "3.2.2",
|