@columenlabs/lumen-ui 0.1.23 → 0.1.25

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/NOTICE CHANGED
@@ -8,5 +8,5 @@ chart's own logo, so per that library's NOTICE, an app that shows these charts
8
8
  should link to https://www.tradingview.com/ somewhere a user can find it (an
9
9
  About page or footer is enough).
10
10
 
11
- The components under packages/lumen/src/ui are based on shadcn/ui
11
+ The components under packages/ui/src/ui are based on shadcn/ui
12
12
  (https://ui.shadcn.com), MIT License, Copyright (c) 2023 shadcn.
package/README.md CHANGED
@@ -2,4 +2,4 @@
2
2
 
3
3
  Lumen's components: the app frame (`LumenShell`), pages and views (`LumenLayout`, `LumenView`, `LumenTable`, `LumenWidget`, `LumenCard`), charts (`LumenChart`), `LumenDrawer`, `LumenSelector`, the shadcn parts under `ui/*` and one `theme.css`. Its types are in `@columenlabs/lumen-ui-types`.
4
4
 
5
- See the [Lumen README](https://github.com/columenlabs/lumen-ui#readme) for install and use. MIT.
5
+ See the [Lumen README](https://github.com/columenlabs/lumen#readme) for install and use. MIT.
package/dist/shell.d.ts CHANGED
@@ -58,6 +58,8 @@ export interface LumenShellProps {
58
58
  /** Turns on What's new: the app's releases API (`GET ?limit=&before=` answering `{releases}`).
59
59
  * The status bar shows the newest version, which opens each release's Key takeaways. */
60
60
  releases?: string;
61
+ /** What's new's app chips: each app's label by id, matched to a takeaway's `apps`. */
62
+ releaseApps?: Record<string, ReactNode>;
61
63
  /** The app's own Settings sections, listed after General and the product's. */
62
64
  settings?: SettingsSectionDef[];
63
65
  /** Sections every app of the product shows, listed after General under `sharedLabel`. */
@@ -89,7 +91,7 @@ export interface LumenShellProps {
89
91
  /** The routed page (usually `<Outlet />`). */
90
92
  children: ReactNode;
91
93
  }
92
- export declare function LumenShell({ app, appLabel, brand, nav, status, health, statusEnd, releases, settings, shared, sharedLabel, user, signOutUrl, profile, security, onRefresh, hideAmounts, onLogs, headerActions, panel, finder, wrap, children, }: LumenShellProps): import("react").JSX.Element;
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;
93
95
  /** A status bar entry: an optional health dot, a label and a mono value. */
94
96
  export declare function StatusItem({ label, value, ok, className, }: {
95
97
  label: ReactNode;
package/dist/shell.js CHANGED
@@ -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, 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, 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
@@ -129,7 +129,7 @@ export function LumenShell({ app, appLabel, brand, nav, status, health, statusEn
129
129
  refreshing,
130
130
  hideAmounts,
131
131
  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 }), 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" })] }) }) })) }) }));
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" })] }) }) })) }) }));
133
133
  }
134
134
  /** A status bar entry: an optional health dot, a label and a mono value. */
135
135
  export function StatusItem({ label, value, ok, className, }) {
@@ -1,3 +1,8 @@
1
- export declare function WhatsNew({ api }: {
1
+ import { type ReactNode } from 'react';
2
+ export declare function WhatsNew({ api, apps, everyone, }: {
2
3
  api: string;
4
+ /** Each app's chip label by id, in the order groups list them; left out: no app chips. */
5
+ apps?: Record<string, ReactNode>;
6
+ /** The chip of a takeaway about no one app. */
7
+ everyone?: string;
3
8
  }): import("react").JSX.Element | null;
package/dist/whats-new.js CHANGED
@@ -3,7 +3,9 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-run
3
3
  // every app): the version in the status bar opens the release notes, newest
4
4
  // first, each release's Key takeaways only. An app turns it on with LumenShell's `releases`, its
5
5
  // releases API (`GET ?limit=&before=` answering `{releases}`), which leaves out what the person can't
6
- // open; a host that answers 404 (self-host an app) keeps the version hidden.
6
+ // open; a host that answers 404 (self-host an app) keeps the version hidden. With `apps`, each
7
+ // takeaway carries a chip of the app it is about (`apps` on the takeaway; none: everyone), and a
8
+ // release where one app has `GROUP_AT` or more is split into one sub-section per app.
7
9
  import { useInfiniteQuery } from '@tanstack/react-query';
8
10
  import { useState } from 'react';
9
11
  import Markdown from 'react-markdown';
@@ -22,6 +24,10 @@ const TYPES = {
22
24
  };
23
25
  const ORDER = Object.keys(TYPES);
24
26
  const PAGE = 20;
27
+ /** A release is split by app once one app has this many takeaways in it. */
28
+ const GROUP_AT = 3;
29
+ /** The group of takeaways about no one app. */
30
+ const EVERYONE = '';
25
31
  async function page(api, before) {
26
32
  const q = new URLSearchParams({ limit: String(PAGE) });
27
33
  if (before)
@@ -33,7 +39,7 @@ async function page(api, before) {
33
39
  throw new Error(`releases ${r.status}`);
34
40
  return (await r.json()).releases;
35
41
  }
36
- export function WhatsNew({ api }) {
42
+ export function WhatsNew({ api, apps, everyone = 'Global', }) {
37
43
  const [open, setOpen] = useState(false);
38
44
  const { timeZone } = usePrefs();
39
45
  const q = useInfiniteQuery({
@@ -49,5 +55,34 @@ export function WhatsNew({ api }) {
49
55
  if (!latest)
50
56
  return null;
51
57
  const day = (at) => new Date(at).toLocaleDateString('en-US', { month: 'short', day: 'numeric', timeZone });
52
- return (_jsxs(_Fragment, { children: [_jsx(Button, { variant: "ghost", size: "sm", className: "num h-6 px-1.5 text-muted-foreground text-xs", onClick: () => setOpen(true), title: "What's new", children: latest.tag }), _jsx(LumenDrawer, { open: open, onClose: () => setOpen(false), title: "What's new", description: "Key takeaways of each release, newest first", footerStart: `${releases.length} releases`, footer: _jsxs(_Fragment, { children: [q.hasNextPage && (_jsx(Button, { variant: "outline", size: "sm", disabled: q.isFetchingNextPage, onClick: () => q.fetchNextPage(), children: "Show more" })), _jsx(Button, { size: "sm", onClick: () => setOpen(false), children: "Close" })] }), children: releases.map((r, i) => (_jsx(DrawerSection, { title: r.tag, hint: day(r.published_at), defaultOpen: i === 0, children: r.takeaways.length ? (_jsx("ul", { className: "list-disc space-y-1.5 py-1 pr-1 pl-5 text-sm leading-relaxed [&_strong]:font-semibold", children: r.takeaways.map((t) => (_jsxs("li", { children: [t.types.length > 0 && (_jsx("span", { className: "mr-1.5 inline-flex flex-wrap gap-1 align-middle", children: ORDER.filter((k) => t.types.includes(k)).map((k) => (_jsx(Badge, { variant: TYPES[k].variant, className: "px-1.5", children: TYPES[k].label }, k))) })), _jsx(Markdown, { allowedElements: ['strong', 'em', 'code'], unwrapDisallowed: true, children: t.text })] }, t.text))) })) : (_jsx("p", { className: "py-1 pl-1 text-muted-foreground text-sm", children: "Nothing new for you in this one." })) }, r.tag))) })] }));
58
+ return (_jsxs(_Fragment, { children: [_jsx(Button, { variant: "ghost", size: "sm", className: "num h-6 px-1.5 text-muted-foreground text-xs", onClick: () => setOpen(true), title: "What's new", children: latest.tag }), _jsx(LumenDrawer, { open: open, onClose: () => setOpen(false), title: "What's new", description: "Key takeaways of each release, newest first", footerStart: `${releases.length} releases`, footer: _jsxs(_Fragment, { children: [q.hasNextPage && (_jsx(Button, { variant: "outline", size: "sm", disabled: q.isFetchingNextPage, onClick: () => q.fetchNextPage(), children: "Show more" })), _jsx(Button, { size: "sm", onClick: () => setOpen(false), children: "Close" })] }), children: releases.map((r, i) => (_jsx(DrawerSection, { title: r.tag, hint: day(r.published_at), defaultOpen: i === 0, children: r.takeaways.length ? (_jsx(Notes, { takeaways: r.takeaways, apps: apps, everyone: everyone })) : (_jsx("p", { className: "py-1 pl-1 text-muted-foreground text-sm", children: "Nothing new for you in this one." })) }, r.tag))) })] }));
59
+ }
60
+ /** One release's takeaways: a flat list, or one sub-section per app once one app has many. */
61
+ function Notes({ takeaways, apps, everyone, }) {
62
+ const appOf = (t) => t.apps?.find((a) => apps?.[a] != null) ?? EVERYONE;
63
+ const groups = new Map();
64
+ for (const t of takeaways) {
65
+ const k = apps ? appOf(t) : EVERYONE;
66
+ groups.set(k, [...(groups.get(k) ?? []), t]);
67
+ }
68
+ const split = apps && groups.size > 1 && [...groups.values()].some((g) => g.length >= GROUP_AT);
69
+ if (!split)
70
+ return _jsx(List, { takeaways: takeaways, apps: apps, everyone: everyone });
71
+ const order = Object.keys(apps);
72
+ const rank = (k) => (k === EVERYONE ? order.length : order.indexOf(k));
73
+ const keys = [...groups.keys()].sort((a, b) => (groups.get(b)?.length ?? 0) - (groups.get(a)?.length ?? 0) || rank(a) - rank(b));
74
+ return (_jsx("div", { className: "space-y-2 py-1", children: keys.map((k) => (_jsxs("div", { children: [_jsxs("div", { className: "flex items-center gap-1.5 pl-1 text-muted-foreground text-xs", children: [_jsx(AppChip, { label: k ? apps[k] : everyone }), _jsx("span", { className: "num", children: groups.get(k)?.length })] }), _jsx(List, { takeaways: groups.get(k) ?? [], apps: apps, everyone: everyone, group: k })] }, k || 'everyone'))) }));
75
+ }
76
+ /** Takeaways as bullets: app chips (bar the group's own), then type chips, then the text. */
77
+ function List({ takeaways, apps, everyone, group, }) {
78
+ return (_jsx("ul", { className: "list-disc space-y-1.5 py-1 pr-1 pl-5 text-sm leading-relaxed [&_strong]:font-semibold", children: takeaways.map((t) => {
79
+ const own = (t.apps ?? []).filter((a) => apps?.[a] != null);
80
+ const chips = apps
81
+ ? (own.length ? own : [EVERYONE]).filter((a) => group === undefined || a !== group)
82
+ : [];
83
+ return (_jsxs("li", { children: [(chips.length > 0 || t.types.length > 0) && (_jsxs("span", { className: "mr-1.5 inline-flex flex-wrap gap-1 align-middle", children: [chips.map((a) => (_jsx(AppChip, { label: a ? apps?.[a] : everyone }, a || 'everyone'))), ORDER.filter((k) => t.types.includes(k)).map((k) => (_jsx(Badge, { variant: TYPES[k].variant, className: "px-1.5", children: TYPES[k].label }, k)))] })), _jsx(Markdown, { allowedElements: ['strong', 'em', 'code'], unwrapDisallowed: true, children: t.text })] }, t.text));
84
+ }) }));
85
+ }
86
+ function AppChip({ label }) {
87
+ return (_jsx(Badge, { variant: "outline", className: "px-1.5 font-semibold", children: label }));
53
88
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@columenlabs/lumen-ui",
3
- "version": "0.1.23",
3
+ "version": "0.1.25",
4
4
  "description": "Lumen: shared React components (frame, views, widgets, charts, drawers).",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -25,13 +25,13 @@
25
25
  "sideEffects": [
26
26
  "**/*.css"
27
27
  ],
28
- "homepage": "https://github.com/columenlabs/lumen-ui",
28
+ "homepage": "https://github.com/columenlabs/lumen",
29
29
  "repository": {
30
30
  "type": "git",
31
- "url": "git+https://github.com/columenlabs/lumen-ui.git",
32
- "directory": "packages/lumen"
31
+ "url": "git+https://github.com/columenlabs/lumen.git",
32
+ "directory": "packages/ui"
33
33
  },
34
- "bugs": "https://github.com/columenlabs/lumen-ui/issues",
34
+ "bugs": "https://github.com/columenlabs/lumen/issues",
35
35
  "publishConfig": {
36
36
  "access": "public",
37
37
  "registry": "https://registry.npmjs.org/"
@@ -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.23",
44
+ "@columenlabs/lumen-ui-types": "0.1.25",
45
45
  "@dnd-kit/core": "6.3.1",
46
46
  "@dnd-kit/sortable": "10.0.0",
47
47
  "@dnd-kit/utilities": "3.2.2",