@columenlabs/lumen-ui 0.1.63 → 0.1.64

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/dist/index.d.ts CHANGED
@@ -54,7 +54,7 @@ export { LumenPageState } from './lumen-page-state.js';
54
54
  export { type FormIssue, goToIssue, SaveButton } from './form-issues.js';
55
55
  export { LumenForm, type LumenFormField, lumenFormIssues, type LumenFormValue, type LumenFormValues, } from './lumen-form.js';
56
56
  export { RouteError, isStaleBuildError, reloadForNewBuild, reportError, watchNewBuilds, } from './route-error.js';
57
- export { LumenSettings, SettingRow, SettingsActions, SettingsFooter, type SettingsSectionDef, } from './settings.js';
57
+ export { LumenSettings, SettingRow, SettingsActions, SettingsFooter, type SettingsGroupDef, type SettingsSectionDef, } from './settings.js';
58
58
  export { LumenCard } from './summary-cards.js';
59
59
  export { WhatsNew } from './whats-new.js';
60
60
  export { type LumenApp, LumenAppSwitcher } from './app-switcher.js';
@@ -13,6 +13,11 @@ export interface SettingsSectionDef {
13
13
  * every section, empty when it has nothing; `SettingsFooter` fills it too. */
14
14
  footer?: ReactNode;
15
15
  }
16
+ /** A labelled group of sections in Settings: one per app when a product shows several apps' settings together. */
17
+ export interface SettingsGroupDef {
18
+ label: string;
19
+ sections: SettingsSectionDef[];
20
+ }
16
21
  /** Renders its children in the section header, right of the title. */
17
22
  export declare function SettingsActions({ children }: {
18
23
  children: ReactNode;
@@ -32,9 +37,9 @@ export declare function SettingRow({ label, description, htmlFor, children, }: {
32
37
  export declare function ThemeToggle({ compact }: {
33
38
  compact?: boolean;
34
39
  }): import("react").JSX.Element;
35
- /** Every Settings section in the order the dialog lists them: General, the product's, the app's. */
36
- export declare function allSettings(profile: ReactNode | undefined, shared?: SettingsSectionDef[], sections?: SettingsSectionDef[], security?: ReactNode): SettingsSectionDef[];
37
- export declare function LumenSettings({ open, section, onOpen, onClose, appLabel, profile, security, sections, shared, sharedLabel, }: {
40
+ /** Every Settings section in the order the dialog lists them: General, the product's, the groups', the app's. */
41
+ export declare function allSettings(profile: ReactNode | undefined, shared?: SettingsSectionDef[], sections?: SettingsSectionDef[], security?: ReactNode, groups?: SettingsGroupDef[]): SettingsSectionDef[];
42
+ export declare function LumenSettings({ open, section, onOpen, onClose, appLabel, profile, security, sections, shared, sharedLabel, groups, }: {
38
43
  open: boolean;
39
44
  /** The section shown; unknown ids show Profile. */
40
45
  section: string;
@@ -50,4 +55,6 @@ export declare function LumenSettings({ open, section, onOpen, onClose, appLabel
50
55
  /** Sections the product adds to every app (the assistant), listed under `sharedLabel` after General. */
51
56
  shared?: SettingsSectionDef[];
52
57
  sharedLabel?: string;
58
+ /** More labelled groups (one per app), listed after the shared ones and before the app's own. */
59
+ groups?: SettingsGroupDef[];
53
60
  }): import("react").JSX.Element;
package/dist/settings.js CHANGED
@@ -101,18 +101,25 @@ const GENERAL = [
101
101
  content: _jsx(Preferences, {}),
102
102
  },
103
103
  ];
104
- /** Every Settings section in the order the dialog lists them: General, the product's, the app's. */
105
- export function allSettings(profile, shared = [], sections = [], security) {
106
- const general = [
104
+ function generalSettings(profile, security) {
105
+ return [
107
106
  ...(profile ? [{ ...PROFILE, content: profile }] : []),
108
107
  ...(security ? [{ ...SECURITY, content: security }] : []),
109
108
  ...GENERAL,
110
109
  ];
111
- return [...general, ...shared, ...sections];
112
110
  }
113
- export function LumenSettings({ open, section, onOpen, onClose, appLabel, profile, security, sections = [], shared = [], sharedLabel = 'All apps', }) {
114
- const all = allSettings(profile, shared, sections, security);
115
- const general = all.slice(0, all.length - shared.length - sections.length);
111
+ /** Every Settings section in the order the dialog lists them: General, the product's, the groups', the app's. */
112
+ export function allSettings(profile, shared = [], sections = [], security, groups = []) {
113
+ return [
114
+ ...generalSettings(profile, security),
115
+ ...shared,
116
+ ...groups.flatMap((g) => g.sections),
117
+ ...sections,
118
+ ];
119
+ }
120
+ export function LumenSettings({ open, section, onOpen, onClose, appLabel, profile, security, sections = [], shared = [], sharedLabel = 'All apps', groups = [], }) {
121
+ const all = allSettings(profile, shared, sections, security, groups);
122
+ const general = generalSettings(profile, security);
116
123
  const current = all.find((s) => s.id === section) ?? all[0];
117
124
  // The assistant opens Settings at a section (it never changes a setting); Undo puts it back as it was.
118
125
  useAssistantMoves('settings', {
@@ -151,9 +158,10 @@ export function LumenSettings({ open, section, onOpen, onClose, appLabel, profil
151
158
  setQuery('');
152
159
  onOpen(id);
153
160
  };
154
- const groups = [
161
+ const navGroups = [
155
162
  { label: 'General', items: general.filter(shown) },
156
163
  { label: sharedLabel, items: shared.filter(shown) },
164
+ ...groups.map((g) => ({ label: g.label, items: g.sections.filter(shown) })),
157
165
  ...(sections.length ? [{ label: appLabel, items: sections.filter(shown) }] : []),
158
166
  ].filter((g) => g.items.length);
159
167
  const search = (_jsxs(InputGroup, { className: "bg-background", children: [_jsx(InputGroupAddon, { children: _jsx(SearchIcon, {}) }), _jsx(InputGroupInput, { type: "search", className: "[&::-webkit-search-cancel-button]:hidden", "aria-label": "Search settings", placeholder: "Search", value: query, onChange: (e) => setQuery(e.target.value) }), query && (_jsx(InputGroupAddon, { align: "inline-end", children: _jsx(InputGroupButton, { size: "icon-xs", "aria-label": "Clear search", onClick: () => setQuery(''), children: _jsx(XIcon, {}) }) }))] }));
@@ -172,7 +180,7 @@ export function LumenSettings({ open, section, onOpen, onClose, appLabel, profil
172
180
  e.preventDefault();
173
181
  setQuery('');
174
182
  }
175
- }, className: "flex h-[min(720px,calc(100svh-2rem))] flex-col gap-0 overflow-hidden p-0 sm:max-w-4xl md:flex-row", children: [_jsx(DialogTitle, { className: "sr-only", children: "Settings" }), _jsx(DialogDescription, { className: "sr-only", children: "Your preferences and this desk's settings." }), _jsxs("nav", { "aria-label": "Settings sections", className: "hidden w-56 shrink-0 flex-col border-r bg-sidebar py-2 md:flex", children: [_jsx("div", { className: "px-2 pt-1 pb-1", children: search }), groups.map((g) => (_jsxs(SidebarGroup, { className: "py-1", children: [_jsx(SidebarGroupLabel, { children: g.label }), _jsx(SidebarGroupContent, { children: _jsx(SidebarMenu, { children: g.items.map((s) => (_jsx(SidebarMenuItem, { children: _jsxs(SidebarMenuButton, { isActive: !q && s.id === current.id, className: s.sub ? 'pl-7' : undefined, onClick: () => go(s.id), children: [_jsx(s.icon, {}), _jsx("span", { children: s.title })] }) }, s.id))) }) })] }, g.label)))] }), _jsxs("div", { className: "flex flex-col gap-2 border-b p-3 pr-12 md:hidden", children: [search, !q && (_jsx(LumenSelector, { label: "Settings section", className: "w-full", groups: groups.map((g) => ({
183
+ }, className: "flex h-[min(720px,calc(100svh-2rem))] flex-col gap-0 overflow-hidden p-0 sm:max-w-4xl md:flex-row", children: [_jsx(DialogTitle, { className: "sr-only", children: "Settings" }), _jsx(DialogDescription, { className: "sr-only", children: "Your preferences and this desk's settings." }), _jsxs("nav", { "aria-label": "Settings sections", className: "hidden w-56 shrink-0 flex-col border-r bg-sidebar py-2 md:flex", children: [_jsx("div", { className: "px-2 pt-1 pb-1", children: search }), navGroups.map((g) => (_jsxs(SidebarGroup, { className: "py-1", children: [_jsx(SidebarGroupLabel, { children: g.label }), _jsx(SidebarGroupContent, { children: _jsx(SidebarMenu, { children: g.items.map((s) => (_jsx(SidebarMenuItem, { children: _jsxs(SidebarMenuButton, { isActive: !q && s.id === current.id, className: s.sub ? 'pl-7' : undefined, onClick: () => go(s.id), children: [_jsx(s.icon, {}), _jsx("span", { children: s.title })] }) }, s.id))) }) })] }, g.label)))] }), _jsxs("div", { className: "flex flex-col gap-2 border-b p-3 pr-12 md:hidden", children: [search, !q && (_jsx(LumenSelector, { label: "Settings section", className: "w-full", groups: navGroups.map((g) => ({
176
184
  label: g.label,
177
185
  options: g.items.map((s) => ({
178
186
  value: s.id,
package/dist/shell.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { type ReactNode } from 'react';
2
2
  import { type LumenFinderConfig } from './lumen-finder.js';
3
- import { type SettingsSectionDef } from './settings.js';
3
+ import { type SettingsGroupDef, type SettingsSectionDef } from './settings.js';
4
4
  interface ShellContext {
5
5
  /** The app this page renders in (`data-app` on the root `<html>`). */
6
6
  app: string;
@@ -65,6 +65,8 @@ export interface LumenShellProps {
65
65
  /** Sections every app of the product shows, listed after General under `sharedLabel`. */
66
66
  shared?: SettingsSectionDef[];
67
67
  sharedLabel?: string;
68
+ /** More labelled Settings groups (one per app of a product), listed after `shared`, before the app's own. */
69
+ settingsGroups?: SettingsGroupDef[];
68
70
  /** Who is signed in (the profile button); null while it loads. */
69
71
  user?: ShellUser | null;
70
72
  /** Where Sign out goes; no Sign out without it. */
@@ -91,7 +93,7 @@ export interface LumenShellProps {
91
93
  /** The routed page (usually `<Outlet />`). */
92
94
  children: ReactNode;
93
95
  }
94
- export declare function LumenShell({ app, appLabel, brand, nav, status, health, statusEnd, releases, releaseApps, settings, shared, sharedLabel, user, signOutUrl, profile, security, onRefresh, hideAmounts, onLogs, headerActions, panel, finder, wrap, children, }: LumenShellProps): import("react").JSX.Element;
96
+ export declare function LumenShell({ app, appLabel, brand, nav, status, health, statusEnd, releases, releaseApps, settings, shared, sharedLabel, settingsGroups, user, signOutUrl, profile, security, onRefresh, hideAmounts, onLogs, headerActions, panel, finder, wrap, children, }: LumenShellProps): import("react").JSX.Element;
95
97
  /** A status bar entry: an optional health dot, a label and a mono value. */
96
98
  export declare function StatusItem({ label, value, ok, className, }: {
97
99
  label: ReactNode;
package/dist/shell.js CHANGED
@@ -11,7 +11,7 @@ import { cn } from './lib/utils.js';
11
11
  import { LumenFinder } from './lumen-finder.js';
12
12
  import { usePrefs } from './prefs.js';
13
13
  import { withRefreshToken } from './refresh.js';
14
- import { LumenSettings, ThemeToggle, allSettings } from './settings.js';
14
+ import { LumenSettings, ThemeToggle, allSettings, } from './settings.js';
15
15
  import { SourceHealthProvider } from './source-health.js';
16
16
  import { StatusTimeZone } from './status.js';
17
17
  import { Avatar, AvatarFallback } from './ui/avatar.js';
@@ -88,7 +88,7 @@ const settingsFromUrl = (() => {
88
88
  window.history.replaceState(window.history.state, '', url);
89
89
  return at || 'profile';
90
90
  })();
91
- export function LumenShell({ app, appLabel, brand, nav, status, health, statusEnd, releases, releaseApps, settings, shared, sharedLabel, user = null, signOutUrl, profile, security, onRefresh, hideAmounts = false, onLogs, headerActions = null, panel, finder, wrap = (frame) => frame, children, }) {
91
+ export function LumenShell({ app, appLabel, brand, nav, status, health, statusEnd, releases, releaseApps, settings, shared, sharedLabel, settingsGroups, user = null, signOutUrl, profile, security, onRefresh, hideAmounts = false, onLogs, headerActions = null, panel, finder, wrap = (frame) => frame, children, }) {
92
92
  const [statusSlot, setStatusSlot] = useState(null);
93
93
  const [refreshing, setRefreshing] = useState(false);
94
94
  // Refresh reloads the layout too: once the data is fresh the page mounts
@@ -117,7 +117,10 @@ export function LumenShell({ app, appLabel, brand, nav, status, health, statusEn
117
117
  document.documentElement.dataset.app = app;
118
118
  }, [app]);
119
119
  // Find lists every Settings section by name.
120
- const sectionNames = JSON.stringify(allSettings(profile, shared, settings, security).map((s) => ({ id: s.id, title: s.title })));
120
+ const sectionNames = JSON.stringify(allSettings(profile, shared, settings, security, settingsGroups).map((s) => ({
121
+ id: s.id,
122
+ title: s.title,
123
+ })));
121
124
  const findSettings = useMemo(() => JSON.parse(sectionNames), [sectionNames]);
122
125
  return (_jsx(Ctx.Provider, { value: {
123
126
  app,
@@ -129,7 +132,7 @@ export function LumenShell({ app, appLabel, brand, nav, status, health, statusEn
129
132
  refreshing,
130
133
  hideAmounts,
131
134
  headerActions,
132
- }, children: _jsx(SourceHealthProvider, { children: wrap(_jsx(TooltipProvider, { delayDuration: 200, children: _jsx(SidebarProvider, { defaultOpen: sidebarOpen(), children: _jsxs(LumenFinder, { app: app, settings: findSettings, openSettings: openSettings, config: finder, children: [_jsx(CloseDrawerOnNavigate, {}), _jsxs(Sidebar, { collapsible: "icon", children: [_jsx(SidebarHeader, { children: brand }), _jsx(SidebarContent, { children: nav }), _jsx(SidebarFooter, { children: _jsx(NavUser, { who: user, signOutUrl: signOutUrl, onSettings: () => openSettings(), onLogs: onLogs }) }), _jsx(SidebarRail, {})] }), _jsxs(SidebarInset, { className: "flex min-h-svh min-w-0 flex-col", children: [_jsx("div", { className: "flex min-w-0 flex-1 flex-col", children: children }, epoch), _jsxs("footer", { "data-slot": "shell-status", "aria-label": "Status", className: "sticky bottom-0 z-10 flex h-(--shell-status-height) shrink-0 items-center gap-4 overflow-x-auto overflow-y-hidden border-t bg-background px-(--shell-gutter) text-xs whitespace-nowrap text-muted-foreground", children: [status, _jsx("div", { ref: setStatusSlot, className: "flex shrink-0 items-center gap-4" }), _jsxs("span", { className: "ml-auto flex shrink-0 items-center gap-1", children: [health, releases && _jsx(WhatsNew, { api: releases, apps: releaseApps }), statusEnd, _jsx(StatusTimeZone, {})] })] })] }), panel, _jsx(LumenSettings, { open: settingsAt !== null, section: settingsAt ?? lastSection, onOpen: openSettings, onClose: () => setSettingsAt(null), appLabel: appLabel, profile: profile, security: security, sections: settings, shared: shared, sharedLabel: sharedLabel }), _jsx(Toaster, { position: "bottom-right" })] }) }) })) }) }));
135
+ }, children: _jsx(SourceHealthProvider, { children: wrap(_jsx(TooltipProvider, { delayDuration: 200, children: _jsx(SidebarProvider, { defaultOpen: sidebarOpen(), children: _jsxs(LumenFinder, { app: app, settings: findSettings, openSettings: openSettings, config: finder, children: [_jsx(CloseDrawerOnNavigate, {}), _jsxs(Sidebar, { collapsible: "icon", children: [_jsx(SidebarHeader, { children: brand }), _jsx(SidebarContent, { children: nav }), _jsx(SidebarFooter, { children: _jsx(NavUser, { who: user, signOutUrl: signOutUrl, onSettings: () => openSettings(), onLogs: onLogs }) }), _jsx(SidebarRail, {})] }), _jsxs(SidebarInset, { className: "flex min-h-svh min-w-0 flex-col", children: [_jsx("div", { className: "flex min-w-0 flex-1 flex-col", children: children }, epoch), _jsxs("footer", { "data-slot": "shell-status", "aria-label": "Status", className: "sticky bottom-0 z-10 flex h-(--shell-status-height) shrink-0 items-center gap-4 overflow-x-auto overflow-y-hidden border-t bg-background px-(--shell-gutter) text-xs whitespace-nowrap text-muted-foreground", children: [status, _jsx("div", { ref: setStatusSlot, className: "flex shrink-0 items-center gap-4" }), _jsxs("span", { className: "ml-auto flex shrink-0 items-center gap-1", children: [health, releases && _jsx(WhatsNew, { api: releases, apps: releaseApps }), statusEnd, _jsx(StatusTimeZone, {})] })] })] }), panel, _jsx(LumenSettings, { open: settingsAt !== null, section: settingsAt ?? lastSection, onOpen: openSettings, onClose: () => setSettingsAt(null), appLabel: appLabel, profile: profile, security: security, sections: settings, shared: shared, sharedLabel: sharedLabel, groups: settingsGroups }), _jsx(Toaster, { position: "bottom-right" })] }) }) })) }) }));
133
136
  }
134
137
  /** A status bar entry: an optional health dot, a label and a mono value. */
135
138
  export function StatusItem({ label, value, ok, className, }) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@columenlabs/lumen-ui",
3
- "version": "0.1.63",
3
+ "version": "0.1.64",
4
4
  "description": "Lumen: shared React components (frame, views, widgets, charts, drawers).",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -41,7 +41,7 @@
41
41
  "@codemirror/lang-javascript": "6.2.5",
42
42
  "@codemirror/lang-json": "6.0.2",
43
43
  "@codemirror/lang-sql": "6.10.0",
44
- "@columenlabs/lumen-ui-types": "0.1.63",
44
+ "@columenlabs/lumen-ui-types": "0.1.64",
45
45
  "@dnd-kit/core": "6.3.1",
46
46
  "@dnd-kit/sortable": "10.0.0",
47
47
  "@dnd-kit/utilities": "3.2.2",