@columenlabs/lumen-ui 0.1.237 → 0.1.239

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.
@@ -19,7 +19,7 @@ import { Empty, EmptyContent, EmptyDescription, EmptyHeader, EmptyMedia, EmptyTi
19
19
  import { InviteDrawer } from './invite-drawer.js';
20
20
  import { PersonDrawer } from './person-drawer.js';
21
21
  import { AuditSection, InvitesSection, PermissionsSection } from './sections.js';
22
- import { LevelPicker, STATUS_LABEL, fromNow, peopleView, scopeKey, statusTone, topScopes, usePeopleAction, usePeopleQuery, useVisitView, when, withGrant, } from './shared.js';
22
+ import { LevelPicker, STATUS_LABEL, fromNow, peopleView, scopeKey, statusTone, syncText, topScopes, usePeopleAction, usePeopleQuery, useVisitView, when, withGrant, } from './shared.js';
23
23
  export function LumenPeople({ adapter, crumbs = [], section: given, onSection, onSetUpMail, fields = [], }) {
24
24
  const [own, setOwn] = useState('people');
25
25
  const section = given ?? own;
@@ -167,8 +167,13 @@ function PeopleList({ frame, fields, onInvite, }) {
167
167
  const invited = people.filter((p) => p.status === 'invited').length;
168
168
  const active = people.filter((p) => p.status === 'active').length;
169
169
  const disabled = people.filter((p) => p.status === 'disabled').length;
170
- const onlyMe = people.length <= 1;
171
- return (_jsxs(_Fragment, { children: [_jsx(LumenLayout, { crumbs: frame.crumbs, actions: frame.invite, intro: _jsxs("div", { className: "flex flex-col gap-3", children: [frame.tabs, _jsxs(TileRow, { fill: true, "aria-label": "People at a glance", children: [_jsx(Tile, { label: "People", value: people.length, caption: `${active} active${disabled ? ` · ${disabled} disabled` : ''}` }), _jsx(Tile, { label: "Admins", value: admins.length, caption: "Can\u2019t remove the last admin" }), _jsx(Tile, { label: "Invites pending", value: invited, caption: expired ? `${expired} expired` : 'none expired' }), view.sync ? (_jsx(Tile, { label: "Sign-in sync", value: view.sync.ok ? 'Synced' : 'Out of step', caption: view.sync.at ? when(view.sync.at) : view.sync.detail, valueClassName: view.sync.ok ? undefined : 'text-loss' })) : (_jsx(Tile, { label: "Email", value: view.mail.configured ? 'Working' : 'Not set up', caption: view.mail.from ?? 'links only' }))] }), frame.mailNote, picked.size > 0 && (_jsx(BulkBar, { frame: frame, ids: [...picked], onClear: () => setPicked(new Set()) }))] }), domain: domain, ...layout, rows: people, load: load, state: onlyMe ? _jsx(FirstRun, { mail: view.mail.configured, onInvite: onInvite }) : undefined, selection: { keys: picked, onChange: setPicked }, rowTitle: (p) => p.name || p.email, onOpen: (p) => setOpenId(p.id) }), _jsx(LumenBoundary, { where: "people-person", what: "panel", children: _jsx(PersonDrawer, { adapter: adapter, view: view, person: open, onClose: () => setOpenId(null) }) })] }));
170
+ // The first-run note stands in for the table only when it has no row; a lone admin is a row.
171
+ const none = people.length === 0;
172
+ return (_jsxs(_Fragment, { children: [_jsx(LumenLayout, { crumbs: frame.crumbs, actions: frame.invite, intro: _jsxs("div", { className: "flex flex-col gap-3", children: [frame.tabs, _jsxs(TileRow, { fill: true, "aria-label": "People at a glance", children: [_jsx(Tile, { label: "People", value: people.length, caption: `${active} active${disabled ? ` · ${disabled} disabled` : ''}` }), _jsx(Tile, { label: "Admins", value: admins.length, caption: "Can\u2019t remove the last admin" }), _jsx(Tile, { label: "Invites pending", value: invited, caption: expired ? `${expired} expired` : 'none expired' }), view.sync ? (_jsx(Tile, { label: "Sign-in sync", value: syncText(view.sync), caption: !view.sync.configured
173
+ ? 'Invites don’t reach sign-in'
174
+ : view.sync.at
175
+ ? when(view.sync.at)
176
+ : view.sync.detail, valueClassName: !view.sync.configured || view.sync.ok ? undefined : 'text-loss' })) : (_jsx(Tile, { label: "Email", value: view.mail.configured ? 'Working' : 'Not set up', caption: view.mail.from ?? 'links only' }))] }), frame.mailNote, picked.size > 0 && (_jsx(BulkBar, { frame: frame, ids: [...picked], onClear: () => setPicked(new Set()) }))] }), domain: domain, ...layout, rows: people, load: load, state: none ? _jsx(FirstRun, { mail: view.mail.configured, onInvite: onInvite }) : undefined, selection: { keys: picked, onChange: setPicked }, rowTitle: (p) => p.name || p.email, onOpen: (p) => setOpenId(p.id) }), _jsx(LumenBoundary, { where: "people-person", what: "panel", children: _jsx(PersonDrawer, { adapter: adapter, view: view, person: open, onClose: () => setOpenId(null) }) })] }));
172
177
  }
173
178
  /** One change over the ticked people. Apply clears the selection; Undo puts the change back. */
174
179
  function BulkBar({ frame, ids, onClear, }) {
@@ -14,7 +14,7 @@ import { LumenSelector } from '../lumen-selector.js';
14
14
  import { NoteLine } from '../note-line.js';
15
15
  import { Badge } from '../ui/badge.js';
16
16
  import { Button } from '../ui/button.js';
17
- import { LevelPicker, STATUS_LABEL, adminGuard, fromNow, topScopes, usePeopleAction, when, withGrant, } from './shared.js';
17
+ import { LevelPicker, STATUS_LABEL, adminGuard, fromNow, syncText, topScopes, usePeopleAction, when, withGrant, } from './shared.js';
18
18
  const ROLES = [
19
19
  { value: 'member', label: 'Member', description: 'Only the levels set per scope' },
20
20
  { value: 'admin', label: 'Admin', description: 'Top level everywhere, and manages People' },
@@ -47,7 +47,9 @@ export function PersonDrawer({ adapter, view, person, onClose, }) {
47
47
  },
48
48
  view.sync && {
49
49
  label: 'Sign-in provider',
50
- value: `${view.sync.ok ? 'Synced' : 'Out of step'} · ${when(view.sync.at)}`,
50
+ value: view.sync.at
51
+ ? `${syncText(view.sync)} · ${when(view.sync.at)}`
52
+ : syncText(view.sync),
51
53
  },
52
54
  ] }) }), adapter.audit && _jsx(ActivitySection, { adapter: adapter, person: p }), _jsx(DangerSection, { adapter: adapter, person: p, guard: guard, onGone: onClose })] })) }));
53
55
  }
@@ -1,4 +1,4 @@
1
- import { type Grant, type PeopleView, type Person, type PersonStatus, type Scope } from '@columenlabs/lumen-ui-types/people';
1
+ import { type Grant, type PeopleView, type Person, type PersonStatus, type Scope, type SyncStatus } from '@columenlabs/lumen-ui-types/people';
2
2
  import { type CustomViewConfig } from '@columenlabs/lumen-ui-types/views';
3
3
  import { type ReactNode } from 'react';
4
4
  import type { Crumb } from '../page.js';
@@ -12,6 +12,8 @@ export declare const statusTone: (p: Pick<Person, 'status'>) => ViewChipTone | u
12
12
  */
13
13
  export declare function when(ts: number | null | undefined, now?: number): string;
14
14
  /** `in 5 d`, `in 3 h`, `2 d ago`: how far a time is from now. */
15
+ /** A sign-in sync's one-word state: never set up is not a failed sync. */
16
+ export declare const syncText: (s: Pick<SyncStatus, 'configured' | 'ok'>) => "Not set up" | "Out of step" | "Synced";
15
17
  export declare function fromNow(ts: number | null | undefined, now?: number): string;
16
18
  /** The top scopes (apps), in order; constructs inside one have a `parent`. */
17
19
  export declare const topScopes: (scopes: readonly Scope[]) => Scope[];
@@ -53,6 +53,8 @@ export function when(ts, now = Date.now()) {
53
53
  }));
54
54
  }
55
55
  /** `in 5 d`, `in 3 h`, `2 d ago`: how far a time is from now. */
56
+ /** A sign-in sync's one-word state: never set up is not a failed sync. */
57
+ export const syncText = (s) => !s.configured ? 'Not set up' : s.ok ? 'Synced' : 'Out of step';
56
58
  export function fromNow(ts, now = Date.now()) {
57
59
  if (!ts)
58
60
  return '—';
@@ -24,9 +24,12 @@ export function SettingField({ label, info, htmlFor, end, stack = false, classNa
24
24
  const query = useContext(SettingsSearch);
25
25
  if (query && !settingMatches(query, label, info))
26
26
  return null;
27
- return (_jsxs("div", { "data-slot": "setting-field", "data-stack": stack || undefined, className: cn('flex min-h-10 min-w-0 items-center gap-3 border-t border-border/60 py-1.5 first:border-t-0', stack && 'max-sm:flex-wrap max-sm:gap-y-1.5', className), children: [_jsxs("div", { className: cn('flex w-32 shrink-0 items-center gap-1.5 max-sm:w-24', stack && 'max-sm:w-auto max-sm:flex-1',
27
+ return (_jsxs("div", { "data-slot": "setting-field", "data-stack": stack || undefined, className: cn('flex min-h-10 min-w-0 items-center gap-3 border-t border-border/60 py-1.5 first:border-t-0', stack && 'max-sm:flex-wrap max-sm:gap-y-1.5', className), children: [_jsxs("div", { className: cn(
28
+ // At least the usual column, wider for a longer name, so a label is never cut; on a phone
29
+ // it may take up to half the row and wraps there instead of cutting.
30
+ 'flex min-w-32 shrink-0 items-center gap-1.5 max-sm:min-w-24 max-sm:max-w-[50%] max-sm:shrink', stack && 'max-sm:w-auto max-sm:flex-1',
28
31
  // Nothing beside it but `end`: the label takes the row, so a short name never cuts off.
29
- children === undefined && 'w-auto min-w-0 flex-1 shrink max-sm:w-auto'), children: [_jsx(Label, { htmlFor: htmlFor, className: "truncate font-normal text-muted-foreground", title: label, children: label }), info && _jsx(InfoTip, { label: `About ${label}`, children: info })] }), children !== undefined && (_jsx("div", { className: cn('flex min-w-0 max-w-md flex-1 items-center gap-2', stack && 'max-sm:order-2 max-sm:max-w-none max-sm:basis-full'), children: children })), end && _jsx("div", { className: cn('ml-auto shrink-0', stack && 'max-sm:order-1'), children: end })] }));
32
+ children === undefined && 'w-auto min-w-0 flex-1 shrink max-sm:w-auto'), children: [_jsx(Label, { htmlFor: htmlFor, className: "whitespace-nowrap font-normal text-muted-foreground max-sm:whitespace-normal max-sm:break-words", title: label, children: label }), info && _jsx(InfoTip, { label: `About ${label}`, children: info })] }), children !== undefined && (_jsx("div", { className: cn('flex min-w-0 max-w-md flex-1 items-center gap-2', stack && 'max-sm:order-2 max-sm:max-w-none max-sm:basis-full'), children: children })), end && _jsx("div", { className: cn('ml-auto shrink-0', stack && 'max-sm:order-1'), children: end })] }));
30
33
  }
31
34
  /** A value read in place: cut to one line with the full text on hover, with Copy and Replace inside. */
32
35
  export function SettingValue({ value, label, mono = false, copy = false, onReplace, replaceLabel = 'Replace', replaceIcon: ReplaceIcon = PencilIcon, }) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@columenlabs/lumen-ui",
3
- "version": "0.1.237",
3
+ "version": "0.1.239",
4
4
  "description": "Lumen: shared React components (frame, views, widgets, charts, drawers).",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -49,7 +49,7 @@
49
49
  "@codemirror/lang-javascript": "6.2.5",
50
50
  "@codemirror/lang-json": "6.0.2",
51
51
  "@codemirror/lang-sql": "6.10.0",
52
- "@columenlabs/lumen-ui-types": "0.1.237",
52
+ "@columenlabs/lumen-ui-types": "0.1.239",
53
53
  "@dnd-kit/core": "6.3.1",
54
54
  "@dnd-kit/sortable": "10.0.0",
55
55
  "@dnd-kit/utilities": "3.2.2",