@columenlabs/lumen-ui 0.1.100 → 0.1.102

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
@@ -47,7 +47,7 @@ export { type ChartBand, type ChartMarker, GrowthChart, KEY_DATE_KINDS, KeyDateC
47
47
  export { FoldGroup, NavGroup, type NavItem, type SectionItem, type SectionSubItem, SidebarSection, } from './nav.js';
48
48
  export { type OverviewPageProps, type OverviewStage, type OverviewTone, type OverviewWidget, OverviewList, OverviewPage, ToneDot, } from './overview.js';
49
49
  export { type Crumb, IconButton, type IconButtonProps, Page, PageBody, PageHeader, type PageHeaderProps, } from './page.js';
50
- export { DEFAULT_PREFS, deviceTimeZone, type Prefs, TIME_ZONES, type Theme, type TimeZoneId, getPrefs, resolvedTheme, setPref, timeZoneLabel, usePrefs, ASSISTANT_WIDTH, } from './prefs.js';
50
+ export { DEFAULT_PREFS, deviceTimeZone, type Prefs, TIME_ZONES, type Theme, type TimeZoneId, getPrefs, resolvedTheme, setPref, timeZoneLabel, usePrefs, zoneAbbr, ASSISTANT_WIDTH, } from './prefs.js';
51
51
  export { KIT, type KitConfig, configureLumen } from './kit-config.js';
52
52
  export { ConnectionBlock, ErrorLine, IntegrationRow, KeyTestLine, type KeyTestResult, TestBadge, errorText, useClock, } from './integrations.js';
53
53
  export { ConnectionBadge, type ConnectionField, type ConnectionLine, ConnectionLogo, type ConnectionSignin, type ConnectionStatus, type ConnectionTest, type ConnectionWay, LumenConnection, type LumenConnectionProps, activeWay, connectionStatus, } from './connection.js';
package/dist/index.js CHANGED
@@ -54,7 +54,7 @@ export { GrowthChart, KEY_DATE_KINDS, KeyDateChips, PALETTE, benchColour, keyDat
54
54
  export { FoldGroup, NavGroup, SidebarSection, } from './nav.js';
55
55
  export { OverviewList, OverviewPage, ToneDot, } from './overview.js';
56
56
  export { IconButton, Page, PageBody, PageHeader, } from './page.js';
57
- export { DEFAULT_PREFS, deviceTimeZone, TIME_ZONES, getPrefs, resolvedTheme, setPref, timeZoneLabel, usePrefs, ASSISTANT_WIDTH, } from './prefs.js';
57
+ export { DEFAULT_PREFS, deviceTimeZone, TIME_ZONES, getPrefs, resolvedTheme, setPref, timeZoneLabel, usePrefs, zoneAbbr, ASSISTANT_WIDTH, } from './prefs.js';
58
58
  export { KIT, configureLumen } from './kit-config.js';
59
59
  export { ConnectionBlock, ErrorLine, IntegrationRow, KeyTestLine, TestBadge, errorText, useClock, } from './integrations.js';
60
60
  export { ConnectionBadge, ConnectionLogo, LumenConnection, activeWay, connectionStatus, } from './connection.js';
@@ -3,7 +3,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  // month or year, drawn as a line, area, stacked area, bars over time, combo, a spark or a tracker.
4
4
  // With the allocation, donut, bars and card styles they cover every Tremor visualization, drawn on
5
5
  // our own Recharts in ChartContainer. Worked out in the browser from the rows; nothing is fetched.
6
- import { isDollarFormat, } from '@columenlabs/lumen-ui-types/views';
6
+ import { formatBytes, isDollarFormat, } from '@columenlabs/lumen-ui-types/views';
7
7
  import { useMemo } from 'react';
8
8
  import { Area, AreaChart, Bar, BarChart, CartesianGrid, ComposedChart, Line, LineChart, XAxis, YAxis, } from 'recharts';
9
9
  import { cn } from './lib/utils.js';
@@ -24,6 +24,8 @@ function short(v, format, hidden) {
24
24
  const abs = Math.abs(v);
25
25
  if (format === 'percent' || format === 'weight')
26
26
  return `${sign}${abs.toFixed(1)}%`;
27
+ if (format === 'bytes')
28
+ return `${sign}${formatBytes(abs)}`;
27
29
  if (isDollarFormat(format))
28
30
  return `${sign}$${COMPACT.format(abs)}`;
29
31
  return `${sign}${COMPACT.format(abs)}`;
@@ -1,5 +1,5 @@
1
1
  import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { controlOn, isCountAgg, isDollarFormat, viewPageSizes, viewPaginationSchema, } from '@columenlabs/lumen-ui-types/views';
2
+ import { bytesParts, controlOn, isCountAgg, isDollarFormat, viewPageSizes, viewPaginationSchema, } from '@columenlabs/lumen-ui-types/views';
3
3
  // Custom views: any number of views on any app, one page for all of them.
4
4
  // A data domain (holdings, chains, bars, series, research results) describes its fields once in a
5
5
  // `ViewDomain`; a view is a saved `CustomViewConfig` over that domain, written in the page's Edit
@@ -42,7 +42,7 @@ import { Skeleton } from './ui/skeleton.js';
42
42
  import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from './ui/table.js';
43
43
  import { ToggleGroup, ToggleGroupItem } from './ui/toggle-group.js';
44
44
  import { Tooltip, TooltipContent, TooltipTrigger } from './ui/tooltip.js';
45
- import { UnitColumns } from './unit-value.js';
45
+ import { UnitColumns, UnitValue } from './unit-value.js';
46
46
  import { EditCell, EditInputCell, EditManyCell } from './view-edit-cell.js';
47
47
  import { fieldOptions } from './view-formulas.js';
48
48
  import { CHIP_TONE, FORMULAS_GROUP, chipsOf, computedOf, filterFieldsOf, groupOf, label, pick, rollUp, rollsUp, subgroupOf, withSavedFilters, withViewFields, } from './view-model.js';
@@ -104,6 +104,11 @@ export function ViewValueCell({ value, format, chip, tone, color, amount, compac
104
104
  return _jsx("span", { children: text });
105
105
  return (_jsxs(Tooltip, { children: [_jsx(TooltipTrigger, { asChild: true, children: _jsx("span", { className: "block max-w-[24rem] truncate", children: text }) }), _jsx(TooltipContent, { className: "max-w-md whitespace-normal", children: text })] }));
106
106
  }
107
+ if (format === 'bytes' && !hidden) {
108
+ // A size: its number and unit in the column's unit slot, so a column of sizes lines up.
109
+ const [n, unit] = bytesParts(value);
110
+ return _jsx(UnitValue, { value: `${value < 0 ? MINUS : ''}${n}`, unit: unit });
111
+ }
107
112
  const abs = hidden ? maskAmount('0') : digitsOf(format, compact).format(Math.abs(value));
108
113
  const signed = format === 'signed' || format === 'percent' || format === 'price_signed';
109
114
  const sign = value < 0 ? MINUS : signed && value > 0 ? '+' : '';
@@ -197,7 +197,7 @@ export declare function cardsAfter(cards: ViewCard[], i: number, action: CardAct
197
197
  key: string;
198
198
  placement: "column" | "inline";
199
199
  }[];
200
- formats?: Record<string, "compact" | "integer" | "number" | "percent" | "price" | "price_rounded" | "price_signed" | "rounded" | "signed" | "weight"> | undefined;
200
+ formats?: Record<string, "bytes" | "compact" | "integer" | "number" | "percent" | "price" | "price_rounded" | "price_signed" | "rounded" | "signed" | "weight"> | undefined;
201
201
  aggs?: Record<string, "count" | "distinct" | "first" | "last" | "max" | "mean" | "min" | "none" | "sum"> | undefined;
202
202
  shows?: Record<string, string> | undefined;
203
203
  group: {
@@ -8,6 +8,7 @@ import { useMemo, useState } from 'react';
8
8
  import { LumenLayout } from '../lumen-layout.js';
9
9
  import { LumenSelector } from '../lumen-selector.js';
10
10
  import { useLumenLoad } from '../lumen-source.js';
11
+ import { getPrefs } from '../prefs.js';
11
12
  import { Tile, TileRow } from '../tile.js';
12
13
  import { Button } from '../ui/button.js';
13
14
  import { CopyLink } from './invite-drawer.js';
@@ -258,7 +259,8 @@ export function AuditSection({ frame }) {
258
259
  key: 'day',
259
260
  label: 'Day',
260
261
  icon: ClockIcon,
261
- value: (e) => new Date(e.at).toLocaleDateString(undefined, {
262
+ value: (e) => new Date(e.at).toLocaleDateString('en-US', {
263
+ timeZone: getPrefs().timeZone,
262
264
  weekday: 'short',
263
265
  day: 'numeric',
264
266
  month: 'short',
@@ -270,7 +272,13 @@ export function AuditSection({ frame }) {
270
272
  key: 'time',
271
273
  label: 'When',
272
274
  icon: ClockIcon,
273
- value: (e) => new Date(e.at).toLocaleTimeString(undefined, { hour: '2-digit', minute: '2-digit' }),
275
+ value: (e) => new Date(e.at).toLocaleTimeString('en-US', {
276
+ timeZone: getPrefs().timeZone,
277
+ // Two-digit hours keep a column of times aligned.
278
+ hour: '2-digit',
279
+ minute: '2-digit',
280
+ timeZoneName: 'shortGeneric',
281
+ }),
274
282
  format: 'text',
275
283
  },
276
284
  {
@@ -6,7 +6,10 @@ import type { ViewChipTone } from '../view-model.js';
6
6
  import type { PeopleAdapter, WithSync } from './client.js';
7
7
  export declare const STATUS_LABEL: Record<PersonStatus, string>;
8
8
  export declare const statusTone: (p: Pick<Person, 'status'>) => ViewChipTone | undefined;
9
- /** `today 09:12`, `yesterday 18:42`, `24 Oct`, `24 Oct 2025`; a dash for never. */
9
+ /**
10
+ * `today 9:12 AM PT`, `yesterday 6:42 PM PT`, `Oct 24`, `Oct 24, 2025`; a dash for never. Days and
11
+ * times are the person's time zone (Settings ▸ Preferences), not the device's.
12
+ */
10
13
  export declare function when(ts: number | null | undefined, now?: number): string;
11
14
  /** `in 5 d`, `in 3 h`, `2 d ago`: how far a time is from now. */
12
15
  export declare function fromNow(ts: number | null | undefined, now?: number): string;
@@ -8,6 +8,7 @@ import { useCallback, useState } from 'react';
8
8
  import { errorText } from '../integrations.js';
9
9
  import { LumenSelector } from '../lumen-selector.js';
10
10
  import { lumenToast } from '../lumen-toast.js';
11
+ import { getPrefs } from '../prefs.js';
11
12
  export const STATUS_LABEL = {
12
13
  active: 'Active',
13
14
  invited: 'Invited',
@@ -22,24 +23,33 @@ export const statusTone = (p) => p.status === 'active'
22
23
  ? 'amber'
23
24
  : 'muted';
24
25
  const DAY = 86_400_000;
25
- /** Non-breaking spaces, so `today 07:18 PM` and `in 7 d` never split across lines in any text. */
26
+ /** Non-breaking spaces, so `today 7:18 PM PT` and `in 7 d` never split across lines in any text. */
26
27
  const glue = (s) => s.replace(/ /g, '\u00a0');
27
- const sameDay = (a, b) => a.toDateString() === b.toDateString();
28
- /** `today 09:12`, `yesterday 18:42`, `24 Oct`, `24 Oct 2025`; a dash for never. */
28
+ /**
29
+ * `today 9:12 AM PT`, `yesterday 6:42 PM PT`, `Oct 24`, `Oct 24, 2025`; a dash for never. Days and
30
+ * times are the person's time zone (Settings ▸ Preferences), not the device's.
31
+ */
29
32
  export function when(ts, now = Date.now()) {
30
33
  if (!ts)
31
34
  return '—';
32
- const d = new Date(ts);
33
- const n = new Date(now);
34
- const time = d.toLocaleTimeString(undefined, { hour: '2-digit', minute: '2-digit' });
35
- if (sameDay(d, n))
35
+ const { timeZone } = getPrefs();
36
+ const day = (t) => new Date(t).toLocaleDateString('en-US', { timeZone });
37
+ const year = (t) => new Date(t).toLocaleDateString('en-US', { timeZone, year: 'numeric' });
38
+ const time = new Date(ts).toLocaleTimeString('en-US', {
39
+ timeZone,
40
+ hour: 'numeric',
41
+ minute: '2-digit',
42
+ timeZoneName: 'shortGeneric',
43
+ });
44
+ if (day(ts) === day(now))
36
45
  return glue(`today ${time}`);
37
- if (sameDay(d, new Date(now - DAY)))
46
+ if (day(ts) === day(now - DAY))
38
47
  return glue(`yesterday ${time}`);
39
- return glue(d.toLocaleDateString(undefined, {
48
+ return glue(new Date(ts).toLocaleDateString('en-US', {
49
+ timeZone,
40
50
  day: 'numeric',
41
51
  month: 'short',
42
- ...(d.getFullYear() === n.getFullYear() ? {} : { year: 'numeric' }),
52
+ ...(year(ts) === year(now) ? {} : { year: 'numeric' }),
43
53
  }));
44
54
  }
45
55
  /** `in 5 d`, `in 3 h`, `2 d ago`: how far a time is from now. */
package/dist/prefs.d.ts CHANGED
@@ -69,5 +69,7 @@ export declare const resolvedTheme: (p?: Prefs) => 'light' | 'dark';
69
69
  export declare function setPref<K extends keyof Prefs>(key: K, value: Prefs[K]): void;
70
70
  export declare const getPrefs: () => Prefs;
71
71
  export declare function usePrefs(): Prefs;
72
+ /** A zone's short generic name, as people say it next to a time: `PT`, `ET`, `UTC` (never PDT or PST). */
73
+ export declare function zoneAbbr(tz: string, at?: Date): string;
72
74
  /** `Pacific`, `UTC`: the short name shown next to times. */
73
75
  export declare const timeZoneLabel: (id: TimeZoneId) => "America/Chicago" | "America/Los_Angeles" | "America/New_York" | "Central" | "Eastern" | "Pacific" | "UTC";
package/dist/prefs.js CHANGED
@@ -130,5 +130,15 @@ export function usePrefs() {
130
130
  return () => listeners.delete(cb);
131
131
  }, getPrefs, () => DEFAULT_PREFS);
132
132
  }
133
+ /** A zone's short generic name, as people say it next to a time: `PT`, `ET`, `UTC` (never PDT or PST). */
134
+ export function zoneAbbr(tz, at = new Date()) {
135
+ // Intl names UTC "GMT+0"; people say UTC.
136
+ if (/^(Etc\/)?(UTC|GMT|Universal|Zulu)$/.test(tz))
137
+ return 'UTC';
138
+ const part = new Intl.DateTimeFormat('en-US', { timeZone: tz, timeZoneName: 'shortGeneric' })
139
+ .formatToParts(at)
140
+ .find((p) => p.type === 'timeZoneName');
141
+ return part?.value ?? tz;
142
+ }
133
143
  /** `Pacific`, `UTC`: the short name shown next to times. */
134
144
  export const timeZoneLabel = (id) => TIME_ZONES.find((z) => z.id === id)?.label ?? id;
package/dist/schedules.js CHANGED
@@ -11,7 +11,7 @@ import { LumenConfirm } from './confirm.js';
11
11
  import { useIsMobile } from './hooks/use-mobile.js';
12
12
  import { LumenSelector } from './lumen-selector.js';
13
13
  import { IconButton } from './page.js';
14
- import { timeZoneLabel, usePrefs } from './prefs.js';
14
+ import { usePrefs, zoneAbbr } from './prefs.js';
15
15
  import { useShell } from './shell.js';
16
16
  import { Badge } from './ui/badge.js';
17
17
  import { Button } from './ui/button.js';
@@ -47,14 +47,53 @@ const daysText = (days) => {
47
47
  return 'Weekdays';
48
48
  return d.map((x) => DAYS[x]).join(', ');
49
49
  };
50
- const zoneText = (tz) => tz.split('/').pop()?.replace('_', ' ') ?? tz;
51
- /** When it runs, in words. */
52
- function when(s) {
50
+ /** A window that spans the whole day on every day says nothing a person needs. */
51
+ const allDay = (s) => s.days.length === 7 && s.from === '00:00' && (s.to === '23:59' || s.to === '24:00');
52
+ /** The zone's offset from UTC at `at`, in ms (positive east of UTC). */
53
+ const offsetOf = (tz, at) => {
54
+ const p = Object.fromEntries(new Intl.DateTimeFormat('en-US', {
55
+ timeZone: tz,
56
+ hourCycle: 'h23',
57
+ year: 'numeric',
58
+ month: 'numeric',
59
+ day: 'numeric',
60
+ hour: 'numeric',
61
+ minute: 'numeric',
62
+ })
63
+ .formatToParts(at)
64
+ .map((x) => [x.type, Number(x.value)]));
65
+ const n = (k) => p[k] ?? 0;
66
+ return (Date.UTC(n('year'), n('month') - 1, n('day'), n('hour'), n('minute')) -
67
+ Math.floor(at / 60_000) * 60_000);
68
+ };
69
+ /** `03:15` in `from` (today) as the same moment's clock in `to`: `03:15` UTC → `8:15 PM` PT. */
70
+ const clockIn = (hhmm, from, to) => {
71
+ if (from === to)
72
+ return clockText(hhmm);
73
+ const [h = 0, m = 0] = hhmm.split(':').map(Number);
74
+ const now = new Date();
75
+ const wall = Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate(), h, m);
76
+ const at = wall - offsetOf(from, wall);
77
+ return new Date(at).toLocaleTimeString('en-US', {
78
+ timeZone: to,
79
+ hour: 'numeric',
80
+ minute: '2-digit',
81
+ });
82
+ };
83
+ /**
84
+ * When it runs, in words, in the viewer's zone (`zone`, the person's preference): `Every 6 h`,
85
+ * `Every 1 h · Weekdays 6:00 AM–2:00 PM PT`, `Daily at 8:15 PM PT`. Days stay the schedule's own.
86
+ */
87
+ function when(s, zone) {
53
88
  if (s.when)
54
89
  return s.when;
55
- if (s.kind === 'window')
56
- return `${daysText(s.days)} ${clockText(s.from)}–${clockText(s.to)} ${zoneText(s.tz)}`;
57
- return `Daily at ${clockText(s.from)} ${zoneText(s.tz)}`;
90
+ const at = (hhmm) => clockIn(hhmm, s.tz, zone);
91
+ if (s.kind === 'daily')
92
+ return `Daily at ${at(s.from)} ${zoneAbbr(zone)}`;
93
+ const every = `Every ${everyText(s.every)}`;
94
+ if (allDay(s))
95
+ return every;
96
+ return `${every} · ${daysText(s.days)} ${at(s.from)}–${at(s.to)} ${zoneAbbr(zone)}`;
58
97
  }
59
98
  function useTime() {
60
99
  const { timeZone } = usePrefs();
@@ -63,6 +102,7 @@ function useTime() {
63
102
  weekday: 'short',
64
103
  hour: 'numeric',
65
104
  minute: '2-digit',
105
+ timeZoneName: 'shortGeneric',
66
106
  });
67
107
  }
68
108
  function Last({ s, compact = false }) {
@@ -90,6 +130,7 @@ function TimeFields({ kind, draft, set, }) {
90
130
  function Editor({ s, onSave, onRun, onRuns, recent, briefs, onClose, }) {
91
131
  const assistantName = useAssistantName();
92
132
  const time = useTime();
133
+ const { timeZone } = usePrefs();
93
134
  const assistant = useAssistant();
94
135
  const { closeSettings } = useShell();
95
136
  const [brief, setBrief] = useState({
@@ -141,7 +182,7 @@ function Editor({ s, onSave, onRun, onRuns, recent, briefs, onClose, }) {
141
182
  setBusy(null);
142
183
  }
143
184
  };
144
- return (_jsxs(_Fragment, { children: [_jsxs(SheetHeader, { children: [_jsx(SheetTitle, { children: s.name }), _jsx(SheetDescription, { children: when(s) })] }), _jsxs("div", { className: "grid grid-cols-1 gap-5 overflow-y-auto px-4", children: [_jsx("dl", { className: "grid grid-cols-[auto_1fr] gap-x-3 gap-y-1.5 text-xs", children: [
185
+ return (_jsxs(_Fragment, { children: [_jsxs(SheetHeader, { children: [_jsx(SheetTitle, { children: s.name }), _jsx(SheetDescription, { children: when(s, timeZone) })] }), _jsxs("div", { className: "grid grid-cols-1 gap-5 overflow-y-auto px-4", children: [_jsx("dl", { className: "grid grid-cols-[auto_1fr] gap-x-3 gap-y-1.5 text-xs", children: [
145
186
  ['Inspects', s.about.inspects],
146
187
  ['Processes', s.about.processes],
147
188
  ['Enriches', s.about.enriches],
@@ -166,6 +207,7 @@ function Editor({ s, onSave, onRun, onRuns, recent, briefs, onClose, }) {
166
207
  /** The Settings ▸ Schedules section's body. */
167
208
  export function SchedulesSection({ schedules, error, onSave, onRun, onRuns, recent, briefs, }) {
168
209
  const isMobile = useIsMobile();
210
+ const { timeZone } = usePrefs();
169
211
  const { closeSettings } = useShell();
170
212
  const [open, setOpen] = useState(null);
171
213
  const [adding, setAdding] = useState(false);
@@ -178,15 +220,14 @@ export function SchedulesSection({ schedules, error, onSave, onRun, onRuns, rece
178
220
  }
179
221
  : undefined;
180
222
  const editing = schedules?.find((s) => s.id === open);
181
- const { timeZone } = usePrefs();
182
223
  if (error)
183
224
  return _jsx("p", { className: "text-sm text-loss", children: error });
184
225
  if (!schedules)
185
226
  return _jsx(Skeleton, { className: "h-48 w-full" });
186
- return (_jsxs(_Fragment, { children: [isMobile ? (_jsx("div", { className: "grid grid-cols-1 divide-y rounded-lg border", children: schedules.map((s) => (_jsxs("button", { type: "button", className: "grid grid-cols-[1fr_auto] items-center gap-x-3 gap-y-1 p-3 text-left hover:bg-muted/50", onClick: () => setOpen(s.id), children: [_jsx("span", { className: "font-medium", children: s.name }), _jsx(OnSwitch, { s: s, onSave: onSave }), _jsxs("span", { className: "col-span-2 text-xs text-muted-foreground", children: [s.kind === 'daily' ? '' : `Every ${everyText(s.every)} · `, when(s)] }), _jsx("span", { className: "col-span-2 text-xs", children: s.waiting ? _jsx("span", { className: "text-warning", children: s.waiting }) : _jsx(Last, { s: s }) })] }, s.id))) })) : (_jsx("div", { className: "overflow-x-auto overflow-y-hidden rounded-lg border", children: _jsxs(Table, { children: [_jsx(TableHeader, { children: _jsxs(TableRow, { children: [_jsx(TableHead, { className: "w-12", children: "On" }), _jsx(TableHead, { children: "Schedule" }), _jsx(TableHead, { children: "When" }), _jsx(TableHead, { children: "Last run" }), onRuns ? _jsx(TableHead, { className: "w-0" }) : null] }) }), _jsx(TableBody, { children: schedules.map((s) => (_jsxs(TableRow, { className: "cursor-pointer", onClick: () => setOpen(s.id), children: [_jsx(TableCell, { children: _jsx(OnSwitch, { s: s, onSave: onSave }) }), _jsx(TableCell, { className: "font-medium", children: s.name }), _jsxs(TableCell, { className: "min-w-48 whitespace-normal text-muted-foreground", children: [s.kind === 'daily' ? '' : `Every ${everyText(s.every)} · `, when(s)] }), _jsx(TableCell, { className: "max-w-48", children: s.waiting ? (_jsx(Badge, { variant: "outline", className: "text-warning", title: s.waiting, children: "Not connected" })) : (_jsx(Last, { s: s, compact: true })) }), onRuns ? (_jsx(TableCell, { children: _jsx(IconButton, { icon: ListIcon, label: `${s.name} runs`, onClick: (e) => {
227
+ return (_jsxs(_Fragment, { children: [isMobile ? (_jsx("div", { className: "grid grid-cols-1 divide-y rounded-lg border", children: schedules.map((s) => (_jsxs("button", { type: "button", className: "grid grid-cols-[1fr_auto] items-center gap-x-3 gap-y-1 p-3 text-left hover:bg-muted/50", onClick: () => setOpen(s.id), children: [_jsx("span", { className: "font-medium", children: s.name }), _jsx(OnSwitch, { s: s, onSave: onSave }), _jsx("span", { className: "col-span-2 text-xs text-muted-foreground", children: when(s, timeZone) }), _jsx("span", { className: "col-span-2 text-xs", children: s.waiting ? _jsx("span", { className: "text-warning", children: s.waiting }) : _jsx(Last, { s: s }) })] }, s.id))) })) : (_jsx("div", { className: "overflow-x-auto overflow-y-hidden rounded-lg border", children: _jsxs(Table, { children: [_jsx(TableHeader, { children: _jsxs(TableRow, { children: [_jsx(TableHead, { className: "w-12", children: "On" }), _jsx(TableHead, { children: "Schedule" }), _jsx(TableHead, { children: "When" }), _jsx(TableHead, { children: "Last run" }), onRuns ? _jsx(TableHead, { className: "w-0" }) : null] }) }), _jsx(TableBody, { children: schedules.map((s) => (_jsxs(TableRow, { className: "cursor-pointer", onClick: () => setOpen(s.id), children: [_jsx(TableCell, { children: _jsx(OnSwitch, { s: s, onSave: onSave }) }), _jsx(TableCell, { className: "font-medium", children: s.name }), _jsx(TableCell, { className: "min-w-48 whitespace-normal text-muted-foreground", children: when(s, timeZone) }), _jsx(TableCell, { className: "max-w-48", children: s.waiting ? (_jsx(Badge, { variant: "outline", className: "text-warning", title: s.waiting, children: "Not connected" })) : (_jsx(Last, { s: s, compact: true })) }), onRuns ? (_jsx(TableCell, { children: _jsx(IconButton, { icon: ListIcon, label: `${s.name} runs`, onClick: (e) => {
187
228
  e.stopPropagation();
188
229
  toRuns?.(s.id);
189
- } }) })) : null] }, s.id))) })] }) })), _jsxs("div", { className: "flex flex-wrap items-center justify-between gap-2", children: [_jsxs("p", { className: "text-xs text-muted-foreground", children: ["Last runs shown in ", timeZoneLabel(timeZone), ".", onRuns ? ' Each run is a line in Logs.' : ''] }), briefs ? (_jsxs(Button, { variant: "outline", size: "sm", onClick: () => setAdding(true), children: [_jsx(PlusIcon, {}), "Brief"] })) : null] }), briefs ? (_jsx(Sheet, { open: adding, onOpenChange: setAdding, children: _jsx(SheetContent, { side: isMobile ? 'bottom' : 'right', className: "gap-4", children: _jsx(NewBrief, { create: async (b, time) => {
230
+ } }) })) : null] }, s.id))) })] }) })), _jsxs("div", { className: "flex flex-wrap items-center justify-between gap-2", children: [onRuns ? (_jsx("p", { className: "text-xs text-muted-foreground", children: "Each run is a line in Logs." })) : (_jsx("span", {})), briefs ? (_jsxs(Button, { variant: "outline", size: "sm", onClick: () => setAdding(true), children: [_jsx(PlusIcon, {}), "Brief"] })) : null] }), briefs ? (_jsx(Sheet, { open: adding, onOpenChange: setAdding, children: _jsx(SheetContent, { side: isMobile ? 'bottom' : 'right', className: "gap-4", children: _jsx(NewBrief, { create: async (b, time) => {
190
231
  const id = await briefs.create(b);
191
232
  await onSave(id, time);
192
233
  return id;
@@ -30,6 +30,7 @@ export const FORMAT_LABELS = {
30
30
  price_signed: 'Currency ±',
31
31
  weight: 'Percent',
32
32
  percent: 'Percent ±',
33
+ bytes: 'Size',
33
34
  text: 'Text',
34
35
  };
35
36
  /**
@@ -47,6 +48,7 @@ const FORMAT_GLYPHS = {
47
48
  price_signed: '±$',
48
49
  weight: '%',
49
50
  percent: '±%',
51
+ bytes: 'B',
50
52
  text: 'Aa',
51
53
  };
52
54
  /** Text fields whose icon is a calendar are dates (no date format exists; the icon says it). */
@@ -74,6 +76,7 @@ const FORMAT_EXAMPLES = {
74
76
  price_signed: '+$1,234.56',
75
77
  weight: '12.3%',
76
78
  percent: '+12.3%',
79
+ bytes: '812 KB · 13.1 GB',
77
80
  text: 'Abc',
78
81
  };
79
82
  /** The number formats as picker options, each with how it reads. */
@@ -353,7 +353,7 @@ export declare const formulasOf: (c: CustomViewConfig) => {
353
353
  key: string;
354
354
  label: string;
355
355
  expr: string;
356
- format: "compact" | "integer" | "number" | "percent" | "price" | "price_rounded" | "price_signed" | "rounded" | "signed" | "text" | "weight";
356
+ format: "bytes" | "compact" | "integer" | "number" | "percent" | "price" | "price_rounded" | "price_signed" | "rounded" | "signed" | "text" | "weight";
357
357
  agg: "count" | "distinct" | "first" | "last" | "max" | "mean" | "min" | "none" | "sum" | null;
358
358
  }[];
359
359
  /** Fields listed under Columns but not shown, for their Filter and Group ticks; views saved before it have none. */
@@ -95,7 +95,7 @@ export declare const readsRows: (card: ViewWidget) => card is {
95
95
  key: string;
96
96
  placement: "column" | "inline";
97
97
  }[];
98
- formats?: Record<string, "compact" | "integer" | "number" | "percent" | "price" | "price_rounded" | "price_signed" | "rounded" | "signed" | "weight"> | undefined;
98
+ formats?: Record<string, "bytes" | "compact" | "integer" | "number" | "percent" | "price" | "price_rounded" | "price_signed" | "rounded" | "signed" | "weight"> | undefined;
99
99
  aggs?: Record<string, "count" | "distinct" | "first" | "last" | "max" | "mean" | "min" | "none" | "sum"> | undefined;
100
100
  shows?: Record<string, string> | undefined;
101
101
  group: {
@@ -3,7 +3,7 @@
3
3
  * field. Shared by the widget frame and its chart bodies, so a lazily loaded body never pulls the
4
4
  * frame back in.
5
5
  */
6
- import { isDollarFormat } from '@columenlabs/lumen-ui-types/views';
6
+ import { formatBytes, isDollarFormat } from '@columenlabs/lumen-ui-types/views';
7
7
  import { maskAmount } from './shell.js';
8
8
  import { label } from './view-model.js';
9
9
  export const MINUS = '−';
@@ -19,6 +19,8 @@ export function short(v, format, hidden) {
19
19
  const abs = Math.abs(v);
20
20
  if (format === 'percent' || format === 'weight')
21
21
  return `${sign}${abs.toFixed(1)}%`;
22
+ if (format === 'bytes')
23
+ return `${sign}${formatBytes(abs)}`;
22
24
  if (isDollarFormat(format))
23
25
  return `${sign}$${COMPACT.format(abs)}`;
24
26
  return `${sign}${COMPACT.format(abs)}`;
@@ -31,6 +33,8 @@ export function exact(v, format, hidden) {
31
33
  const abs = Math.abs(v);
32
34
  if (format === 'percent' || format === 'weight')
33
35
  return `${sign}${abs.toFixed(2)}%`;
36
+ if (format === 'bytes')
37
+ return `${sign}${formatBytes(abs)}`;
34
38
  const n = abs.toLocaleString('en-US', {
35
39
  maximumFractionDigits: format === 'rounded' || format === 'price_rounded' ? 0 : 2,
36
40
  useGrouping: format !== 'integer',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@columenlabs/lumen-ui",
3
- "version": "0.1.100",
3
+ "version": "0.1.102",
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.100",
52
+ "@columenlabs/lumen-ui-types": "0.1.102",
53
53
  "@dnd-kit/core": "6.3.1",
54
54
  "@dnd-kit/sortable": "10.0.0",
55
55
  "@dnd-kit/utilities": "3.2.2",