@columenlabs/lumen-ui 0.1.203 → 0.1.205

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.
@@ -24,7 +24,8 @@ import { chatMarkdown, chatTurns, copyMarkdown, turnMarkdown } from './copy.js';
24
24
  import { composeAssistant, selectionRef, useComposeRequest, usePageSelection, useShownAssistantSources, } from './sources.js';
25
25
  import { useAssistantChat } from './use-assistant-chat.js';
26
26
  // The Markdown renderer (tables included) loads with the first answer (lazy-part.tsx).
27
- const ChatMarkdown = lazyPart(() => import('./chat-markdown.js').then((m) => m.default));
27
+ // A tab whose build a deploy replaced shows the answer as plain text instead of reloading mid chat.
28
+ const ChatMarkdown = lazyPart(() => import('./chat-markdown.js').then((m) => m.default), null, ({ children }) => _jsx("p", { className: "whitespace-pre-wrap", children: children }));
28
29
  /** The main model's context drawn full (tokens); the mark sits where the assistant compacts. */
29
30
  const CONTEXT_MAX = 200_000;
30
31
  const COMPACT_AT = 150_000;
@@ -3,6 +3,15 @@
3
3
  * first page never downloads what it doesn't draw. The import must be a dynamic `import()` with the
4
4
  * part's module, and every other import of that module elsewhere in the kit's static graph must be
5
5
  * type-only, or the bundler keeps it in the first file.
6
+ *
7
+ * Every lazy part also loads on its own once the tab is idle (8 s after load). A deploy replaces
8
+ * the hashed files, so a tab that first drew a part after one asked for a file that was gone and
9
+ * reloaded: the assistant's first answer reloaded the page mid chat (2026-11-04,
10
+ * `chat-markdown-*.js`). Loaded early, the part is in the tab's module map before any deploy.
6
11
  */
7
12
  import { type ComponentType, type ReactNode } from 'react';
8
- export declare function lazyPart<P extends object>(load: () => Promise<ComponentType<P>>, fallback?: ReactNode): ComponentType<P>;
13
+ /** A dynamic import that failed to load (Chrome, Firefox, Safari wording), not a page's own bug. */
14
+ export declare function isStaleBuildError(error: unknown): boolean;
15
+ export declare function lazyPart<P extends object>(load: () => Promise<ComponentType<P>>, fallback?: ReactNode,
16
+ /** Drawn instead when a deploy removed the part's file, so the tab keeps working (no reload). */
17
+ stale?: ComponentType<P>): ComponentType<P>;
package/dist/lazy-part.js CHANGED
@@ -4,10 +4,59 @@ import { jsx as _jsx } from "react/jsx-runtime";
4
4
  * first page never downloads what it doesn't draw. The import must be a dynamic `import()` with the
5
5
  * part's module, and every other import of that module elsewhere in the kit's static graph must be
6
6
  * type-only, or the bundler keeps it in the first file.
7
+ *
8
+ * Every lazy part also loads on its own once the tab is idle (8 s after load). A deploy replaces
9
+ * the hashed files, so a tab that first drew a part after one asked for a file that was gone and
10
+ * reloaded: the assistant's first answer reloaded the page mid chat (2026-11-04,
11
+ * `chat-markdown-*.js`). Loaded early, the part is in the tab's module map before any deploy.
7
12
  */
8
13
  import { Suspense, lazy } from 'react';
9
- export function lazyPart(load, fallback = null) {
10
- const Part = lazy(() => load().then((c) => ({ default: c })));
14
+ /** A dynamic import that failed to load (Chrome, Firefox, Safari wording), not a page's own bug. */
15
+ export function isStaleBuildError(error) {
16
+ const message = error instanceof Error ? error.message : '';
17
+ return (message.startsWith('Failed to fetch dynamically imported module') ||
18
+ message.startsWith('error loading dynamically imported module') ||
19
+ message.startsWith('Importing a module script failed') ||
20
+ message.includes('Unable to preload CSS') ||
21
+ // Safari, when a missing file comes back as a page instead of a script.
22
+ message.includes('is not a valid JavaScript MIME type'));
23
+ }
24
+ /** How long after load the lazy parts start loading: the first page's own work comes first. */
25
+ const PRELOAD_AFTER_MS = 8_000;
26
+ const loaders = new Set();
27
+ let scheduled = false;
28
+ function schedulePreload() {
29
+ if (scheduled || typeof window === 'undefined')
30
+ return;
31
+ scheduled = true;
32
+ const run = () => {
33
+ for (const load of loaders)
34
+ load().catch(() => { });
35
+ };
36
+ const idle = () => setTimeout(() => 'requestIdleCallback' in window ? window.requestIdleCallback(run) : setTimeout(run, 1000), PRELOAD_AFTER_MS);
37
+ if (document.readyState === 'complete')
38
+ idle();
39
+ else
40
+ window.addEventListener('load', idle, { once: true });
41
+ }
42
+ export function lazyPart(load, fallback = null,
43
+ /** Drawn instead when a deploy removed the part's file, so the tab keeps working (no reload). */
44
+ stale) {
45
+ let pending = null;
46
+ const once = () => {
47
+ pending ??= load().catch((error) => {
48
+ pending = null;
49
+ throw error;
50
+ });
51
+ return pending;
52
+ };
53
+ loaders.add(once);
54
+ schedulePreload();
55
+ const Part = lazy(() => once().then((c) => ({ default: c }), (error) => {
56
+ if (stale && isStaleBuildError(error))
57
+ return { default: stale };
58
+ throw error;
59
+ }));
11
60
  function LazyPart(props) {
12
61
  return (_jsx(Suspense, { fallback: fallback, children: _jsx(Part, { ...props }) }));
13
62
  }
@@ -499,11 +499,13 @@ const colPropsOf = (units) => (f, ...more) => ({
499
499
  * theme.css), so one long name never pushes the other columns off screen; `false` lifts it.
500
500
  */
501
501
  const capOf = (f, i) => f.maxWidth === false ? undefined : (f.maxWidth ?? (i === 0 ? 'var(--lead-col-max)' : undefined));
502
- /** Holds a cell to its column's cap on one line: text past it ends in an ellipsis. */
503
- function CappedCell({ max, title, children, }) {
502
+ /** Holds a cell to its column's cap on one line: text past it ends in an ellipsis. `end` keeps a
503
+ * right-aligned column's value on its right edge (the wrapper is a flex row, which `text-right`
504
+ * alone does not move), so a capped or resized column keeps one edge. */
505
+ function CappedCell({ max, end, title, children, }) {
504
506
  if (!max)
505
507
  return children;
506
- return (_jsx("div", { className: "flex min-w-0 *:min-w-0 *:max-w-full *:truncate", style: { maxWidth: max }, title: title, children: children }));
508
+ return (_jsx("div", { className: cn('flex min-w-0 *:min-w-0 *:max-w-full *:truncate', end && 'justify-end'), style: { maxWidth: max }, title: title, children: children }));
507
509
  }
508
510
  /** The flat or grouped table: sortable headers, a Total row, group rows that collapse. */
509
511
  // Sorting, and paging of flat tables (the page is LumenView's, set right of the row count).
@@ -534,11 +536,6 @@ function LumenTableRows({ domain, columns = domain.fields, rows, groupBy = null,
534
536
  const resizing = resizable && !isMobile && !fit;
535
537
  const colWidths = useColumnWidths(resizing ? (widthsKey ?? '') || undefined : undefined);
536
538
  const widthOf = (f) => resizing && f ? colWidths.widths[f.key] : undefined;
537
- // A dragged width caps the column's cells in its place (the cell's own padding taken off).
538
- const capFor = (f, i) => {
539
- const w = widthOf(f);
540
- return w ? `calc(${w}px - 2 * var(--cell-x))` : capOf(f, i);
541
- };
542
539
  const [sorting, setSorting] = useState(() => sortingOf(sort, columns, serverSorts));
543
540
  // A saved sort patched while the table is open (the assistant's card move) applies at once.
544
541
  const savedSort = JSON.stringify(sort ?? null);
@@ -554,11 +551,18 @@ function LumenTableRows({ domain, columns = domain.fields, rows, groupBy = null,
554
551
  pageIndex: paging ? page.index : 0,
555
552
  pageSize: paging ? page.size : Math.max(rows.length, 1),
556
553
  };
557
- // Groups whose state differs from where they start: open, or past OPEN_ALL rows only the first.
558
554
  // Columns a UnitValue has drawn in (`UnitColumns`): their cells share the numbers' edge.
559
555
  const [units, setUnits] = useState(() => new Set());
560
556
  const markUnits = useCallback((key) => setUnits((s) => (s.has(key) ? s : new Set(s).add(key))), []);
561
557
  const colProps = colPropsOf(units);
558
+ // A dragged width caps the column's cells in its place (the cell's own padding taken off).
559
+ const capFor = (f, i) => {
560
+ const w = widthOf(f);
561
+ // A units column's right padding also holds the unit slot (theme.css `[data-units]`).
562
+ const pad = units.has(f.key) ? '2 * var(--cell-x) - var(--unit-slot)' : '2 * var(--cell-x)';
563
+ return w ? `calc(${w}px - ${pad})` : capOf(f, i);
564
+ };
565
+ // Groups whose state differs from where they start: open, or past OPEN_ALL rows only the first.
562
566
  const [flipped, setFlipped] = useState(new Set());
563
567
  const [more, setMore] = useState(new Map());
564
568
  // Under a sub-group's `flat` grouping each row names itself by its sub-group key instead of
@@ -683,7 +687,7 @@ function LumenTableRows({ domain, columns = domain.fields, rows, groupBy = null,
683
687
  const field = now[i] ?? f;
684
688
  const chip = chips.some((c) => c.key === field.key);
685
689
  const v = computed.value(field, row.original);
686
- return (_jsx(CappedCell, { max: capFor(field, i), title: !field.render && typeof v === 'string' ? v : undefined, children: i === 0 && inline.length ? (_jsx(FirstCell, { value: cellOf(field, row.original, chip), chips: inline.flatMap((x) => {
690
+ return (_jsx(CappedCell, { max: capFor(field, i), end: (field.format ?? 'text') !== 'text', title: !field.render && typeof v === 'string' ? v : undefined, children: i === 0 && inline.length ? (_jsx(FirstCell, { value: cellOf(field, row.original, chip), chips: inline.flatMap((x) => {
687
691
  const v = computed.value(x, row.original);
688
692
  if (!x.edit && (v === null || v === ''))
689
693
  return [];
@@ -1,6 +1,5 @@
1
1
  import { type ErrorComponentProps } from '@tanstack/react-router';
2
- /** A dynamic import that failed to load (Chrome, Firefox, Safari wording), not a page's own bug. */
3
- export declare function isStaleBuildError(error: unknown): boolean;
2
+ export { isStaleBuildError } from './lazy-part.js';
4
3
  /**
5
4
  * Reloads onto the new build unless it already did in the last 10 s (a file that is truly missing
6
5
  * then shows its error instead of reloading forever); says whether a reload is under way.
@@ -39,4 +38,3 @@ export declare function ErrorPage({ error, onRetry, what, }: {
39
38
  * tried once more on its own, then shown with Try again and Reload. Each one is logged.
40
39
  */
41
40
  export declare function RouteError({ error, reset }: ErrorComponentProps): import("react").JSX.Element;
42
- export {};
@@ -17,6 +17,7 @@ import { ChevronRight, RefreshCw, RotateCcw, TriangleAlert } from 'lucide-react'
17
17
  import { useEffect, useState } from 'react';
18
18
  import { toast } from 'sonner';
19
19
  import { fetchWithin } from './fetch-within.js';
20
+ import { isStaleBuildError } from './lazy-part.js';
20
21
  import { reportClientError } from './telemetry.js';
21
22
  import { Button } from './ui/button.js';
22
23
  import { Collapsible, CollapsibleContent, CollapsibleTrigger } from './ui/collapsible.js';
@@ -24,16 +25,7 @@ import { Empty, EmptyContent, EmptyDescription, EmptyHeader, EmptyMedia, EmptyTi
24
25
  import { Spinner } from './ui/spinner.js';
25
26
  const KEY = 'lumen_new_build_reload';
26
27
  const GAP_MS = 10_000;
27
- /** A dynamic import that failed to load (Chrome, Firefox, Safari wording), not a page's own bug. */
28
- export function isStaleBuildError(error) {
29
- const message = error instanceof Error ? error.message : '';
30
- return (message.startsWith('Failed to fetch dynamically imported module') ||
31
- message.startsWith('error loading dynamically imported module') ||
32
- message.startsWith('Importing a module script failed') ||
33
- message.includes('Unable to preload CSS') ||
34
- // Safari, when a missing file comes back as a page instead of a script.
35
- message.includes('is not a valid JavaScript MIME type'));
36
- }
28
+ export { isStaleBuildError } from './lazy-part.js';
37
29
  /** A reload this tab has already started; anything failing meanwhile waits for it. */
38
30
  let reloadStarted = false;
39
31
  /** Whether this tab has not reloaded for a new build in the last 10 s. */
package/dist/theme.css CHANGED
@@ -433,6 +433,9 @@
433
433
  }
434
434
  [data-units] [data-slot="unit-value"] {
435
435
  position: relative;
436
+ /* A resized column wraps its cells in a truncating box; the unit hangs outside the value's box,
437
+ so it must not be clipped with it. */
438
+ overflow: visible;
436
439
  }
437
440
  [data-units] [data-slot="unit-value-unit"] {
438
441
  position: absolute;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@columenlabs/lumen-ui",
3
- "version": "0.1.203",
3
+ "version": "0.1.205",
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.203",
52
+ "@columenlabs/lumen-ui-types": "0.1.205",
53
53
  "@dnd-kit/core": "6.3.1",
54
54
  "@dnd-kit/sortable": "10.0.0",
55
55
  "@dnd-kit/utilities": "3.2.2",