tosijs-ui 1.7.0 → 1.7.1

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/CHANGELOG.md CHANGED
@@ -1,5 +1,61 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.7.1
4
+
5
+ - **Fix: components looked wrong in the doc site's dark mode** (e.g. `<tosi-table>` stayed
6
+ white). The doc-system theme drives the legacy `--background`/`--text-color` family and never
7
+ set the `--tosi-*` component palette, so components fell back to their baked-in light defaults
8
+ (`var(--tosi-bg, #fff)`). The doc-system now bridges `--tosi-accent`/`--tosi-bg`/`--tosi-text`/
9
+ `--tosi-bg-inset` onto its own colors **as references**, so every `--tosi-*` component follows
10
+ the site theme automatically — including dark mode (the referenced vars flip via
11
+ `invertLuminance`). The data-table pinned-row example now uses `var(--tosi-table-bg)` instead of
12
+ a hardwired `#eee`.
13
+ - **Data-table `type` example** demonstrating the new column types (currency, number, percent,
14
+ fixed, bytes, boolean) added to the docs.
15
+
16
+ - **Fix: "edit page source" showed HTML instead of source in a dev server without
17
+ `editableSources`.** The `/__docstore/source` endpoint was only routed when
18
+ `editableSources` was on; otherwise the request fell through to the SPA `index.html`
19
+ fallback and returned the rendered page at status 200 — so the client loaded HTML *as* the
20
+ source (and save read HTML). The endpoint is now handled unconditionally and answers a clean
21
+ `501` when editing is disabled, so `loadSource` falls back to the GitHub raw source (read-only
22
+ editing, matching the deployed site). Belt-and-suspenders: the client also rejects any
23
+ `text/html` response from that endpoint (guards SPA-rewrite hosts too). Documented the option
24
+ more fully in `doc-site-system.md`.
25
+ - **Local example save now gives feedback.** "Save changes (local)" writes a per-browser
26
+ scratchpad (not the file) — it used to do so silently, reading as a no-op. It now posts a
27
+ toast (`postNotification`): *"Saved to this browser only — use 'Save to source' to write the
28
+ file."*
29
+
30
+ - **`valueRenderer(type)` + declarative `<tosi-table>` column `type`.** A new exported helper
31
+ turns a compact type string into a reusable, locale-aware renderer with a sensible default
32
+ alignment: `number`, `currency`/`currency(USD)`, `fixed`/`fixed(n)` (default 2),
33
+ `percent`/`percent(n)` (of a fraction; default 0), `sci`, `eng`, `bytes`/`bytes(iec)` (SI ÷1000
34
+ or IEC ÷1024 units, right-aligned), and `boolean`/`boolean(t)`/`boolean(t,f)` (icons via the
35
+ `icons` proxy, centered; default `checkSquare`/`square`). Numeric cells also get a
36
+ `-negative` / `-zero` **state class** by value sign, so you get red negatives (etc.) from one
37
+ CSS rule — no custom cell renderer. Data-table columns take a `type`
38
+ (`ValueRendererType`) and format + align automatically — no hand-rolled `dataCell`; an explicit
39
+ `align`/`dataCell` still wins. Formatting follows `setLocale()`. Verified end-to-end (unit +
40
+ browser).
41
+
42
+ - **Deprecation cleanup: `on<Event>` component callbacks → `handle<Event>`.** tosijs reserves the
43
+ `on<Event>` prefix for elements-factory event-handler sugar and now warns when a component
44
+ *defines* such a property. Renamed the internal ones: `onResize` → `handleResize` (`size-break`,
45
+ `side-nav`, `code-editor`, `babylon-3d`, `tab-selector`) and `onScrollEnd` → `handleScrollEnd`
46
+ (`data-table`). Verified: those pages now load with zero `on<Event>` deprecation warnings.
47
+ (`<tosi-tabs>`'s public `onCloseTab` still uses the old name — a breaking rename deferred to a
48
+ deprecation-alias pass; and the `tosiValue()`/`tosiPath()` accessor deprecation is a separate,
49
+ larger cleanup — both tracked.)
50
+
51
+ - **CSS canary** — an inline doc-test on the `live-theme` page that smoke-tests the whole
52
+ styling chain in a real browser (`StyleSheet()` → the `vars` proxy → scaled-var `calc()` →
53
+ `createTheme`/`applyTheme` → cascade → `getComputedStyle`), plus a theme color-change
54
+ propagating to computed style, dark-mode luminance inversion, and `Color.inverseLuminance`.
55
+ Each link is silent when it breaks (happy-dom can't resolve `var()`/the cascade), so a red
56
+ now means a system-level CSS break, not a component quirk. Runs in the `doc-tests.pw.ts` CI
57
+ gate.
58
+
3
59
  ## 1.7.0
4
60
 
5
61
  > ### ⚠️ BREAKING CHANGE — `<tosi-code>` moved from ACE to CodeMirror 6
@@ -51,7 +51,7 @@ export declare class B3d extends WebComponent {
51
51
  sceneCreated: B3dCallback;
52
52
  update: B3dCallback;
53
53
  private _update;
54
- onResize(): void;
54
+ handleResize(): void;
55
55
  loadScene: (path: string, file: string, processCallback?: MeshProcessCallback) => Promise<void>;
56
56
  loadUI: (options: B3dUIOptions) => Promise<any>;
57
57
  connectedCallback(): void;
@@ -299,7 +299,7 @@ export class B3d extends WebComponent {
299
299
  }
300
300
  }
301
301
  };
302
- onResize() {
302
+ handleResize() {
303
303
  if (this.engine) {
304
304
  this.engine.resize();
305
305
  }
@@ -92,7 +92,7 @@ export declare class CodeEditor extends WebComponent<CodeEditorParts> {
92
92
  fontFamily: string;
93
93
  };
94
94
  };
95
- onResize(): void;
95
+ handleResize(): void;
96
96
  connectedCallback(): void;
97
97
  disconnectedCallback(): void;
98
98
  private isTjsMode;
@@ -267,7 +267,7 @@ export class CodeEditor extends WebComponent {
267
267
  fontFamily: "Menlo, Monaco, Consolas, 'Courier New', monospace",
268
268
  },
269
269
  };
270
- onResize() {
270
+ handleResize() {
271
271
  this._handle?.refresh();
272
272
  }
273
273
  connectedCallback() {
@@ -1,11 +1,13 @@
1
1
  import { Component as WebComponent, ElementCreator } from 'tosijs';
2
2
  import { SortCallback } from './make-sorter';
3
+ import { ValueRendererType } from './value-renderer';
3
4
  export interface ColumnOptions {
4
5
  name?: string;
5
6
  prop: string;
6
7
  width: number;
7
8
  visible?: boolean;
8
9
  align?: string;
10
+ type?: ValueRendererType;
9
11
  pinned?: 'left' | 'right';
10
12
  sort?: false | 'ascending' | 'descending';
11
13
  headerCell?: (options: ColumnOptions) => HTMLElement;
@@ -201,7 +203,7 @@ export declare class TosiTable extends WebComponent {
201
203
  private updateSelection;
202
204
  private findCell;
203
205
  private _pendingFocus;
204
- private onScrollEnd;
206
+ private handleScrollEnd;
205
207
  private focusCell;
206
208
  private handleKeyNav;
207
209
  connectedCallback(): void;
@@ -194,6 +194,7 @@ export interface ColumnOptions {
194
194
  width: number
195
195
  visible?: boolean
196
196
  align?: string
197
+ type?: string // valueRenderer type: 'currency(USD)', 'fixed(2)', 'bytes', 'boolean(check,x)', …
197
198
  pinned?: 'left' | 'right'
198
199
  sort?: false | 'ascending' | 'descending'
199
200
  headerCell?: (options: ColumnOptions) => HTMLElement
@@ -201,6 +202,59 @@ export interface ColumnOptions {
201
202
  }
202
203
  ```
203
204
 
205
+ ## Column value types
206
+
207
+ Give a column a `type` and it's formatted (and aligned) automatically — no hand-rolled
208
+ `dataCell`. The `type` is a [`valueRenderer`](value-renderer) string:
209
+
210
+ ```
211
+ [
212
+ { prop: 'name', width: 160 },
213
+ { prop: 'price', width: 100, type: 'currency(USD)' }, // localized $, right-aligned
214
+ { prop: 'weight', width: 100, type: 'fixed(3)' }, // 3 decimals, right-aligned
215
+ { prop: 'size', width: 100, type: 'bytes' }, // 1.5 MB, right-aligned
216
+ { prop: 'active', width: 60, type: 'boolean' }, // checkSquare / square, centered
217
+ ]
218
+ ```
219
+
220
+ Numeric types (`number`, `currency`, `fixed`, `percent`, `sci`, `eng`, `bytes`) right-align
221
+ by default; `boolean` centers and renders icons. An explicit `align` — or a `dataCell` —
222
+ always wins. Formatting follows the app locale (`setLocale()`). Numeric cells also get a
223
+ `-negative` or `-zero` state class by value sign, so the red "Change" values above come from
224
+ one CSS rule (`.-negative { color: #e44 }`), no cell renderer.
225
+
226
+ ```js
227
+ import { tosiTable } from 'tosijs-ui'
228
+
229
+ const rows = [
230
+ { item: 'Alpha Widget', price: 12.5, qty: 1240, change: 3.25, rate: 0.075, mass: 1.23456, size: 1_536_000, active: true, flagged: false },
231
+ { item: 'Beta Gadget', price: 4.99, qty: 42, change: -1.5, rate: 0.2, mass: 0.5, size: 512, active: false, flagged: true },
232
+ { item: 'Gamma Sprocket', price: 199, qty: 8, change: 0, rate: 1.5, mass: 12.005, size: 2_500_000_000, active: true, flagged: true },
233
+ { item: 'Delta Cog', price: 0.75, qty: 99999, change: -0.12, rate: 0.004, mass: 0.001, size: 48_200, active: false, flagged: false },
234
+ ]
235
+
236
+ const table = tosiTable({ style: { display: 'block', height: '240px' } })
237
+ table.value = {
238
+ array: rows,
239
+ columns: [
240
+ { prop: 'item', name: 'Item', width: 150 },
241
+ { prop: 'price', name: 'Price', width: 110, type: 'currency(USD)' },
242
+ { prop: 'change', name: 'Change', width: 100, type: 'currency(USD)' }, // red negatives via CSS
243
+ { prop: 'qty', name: 'Qty', width: 90, type: 'number' },
244
+ { prop: 'rate', name: 'Rate', width: 80, type: 'percent(1)' },
245
+ { prop: 'mass', name: 'Mass', width: 90, type: 'fixed(2)' },
246
+ { prop: 'size', name: 'Size', width: 100, type: 'bytes' },
247
+ { prop: 'active', name: 'Active', width: 80, type: 'boolean' },
248
+ { prop: 'flagged', name: 'Flag', width: 80, type: 'boolean(check, x)' },
249
+ ],
250
+ }
251
+ preview.append(table)
252
+ ```
253
+ ```css
254
+ .preview .-negative { color: #e44; }
255
+ .preview .-zero { opacity: 0.45; }
256
+ ```
257
+
204
258
  ## Pinned Columns and Rows
205
259
 
206
260
  Set `pinned: 'left'` or `pinned: 'right'` on individual columns to pin
@@ -314,11 +368,11 @@ preview.append(table)
314
368
  width: 100%;
315
369
  }
316
370
  .preview tosi-table .pinned-bottom {
317
- background: #eee;
371
+ background: var(--tosi-table-bg, var(--tosi-bg, #fff));
318
372
  font-weight: bold;
319
373
  }
320
374
  .preview .row-pinned .td {
321
- background: #eee;
375
+ background: var(--tosi-table-bg, var(--tosi-bg, #fff));
322
376
  }
323
377
  .preview .num-cell {
324
378
  font-variant-numeric: tabular-nums;
@@ -513,6 +567,7 @@ As well as any column names you want localized.
513
567
  import { Component as WebComponent, elements, vars, varDefault, tosiValue, getListItem, getListBinding, tosi, } from 'tosijs';
514
568
  import { trackDrag } from './track-drag';
515
569
  import { icons } from './icons';
570
+ import { valueRenderer } from './value-renderer';
516
571
  import { popMenu } from './menu';
517
572
  import * as dragAndDrop from './drag-and-drop';
518
573
  import { tosiLocalized, localize } from './localize';
@@ -545,6 +600,20 @@ function defaultWidth(array, prop, charWidth) {
545
600
  }
546
601
  const { div, span, button } = elements;
547
602
  const passThru = (array) => array;
603
+ // One ValueRenderer per typed column, cached: `Intl.*Format` construction is the
604
+ // expensive part and a column's renderer is stable for its lifetime. Returns null
605
+ // when the column has no `type` or supplies its own `dataCell` (which always wins).
606
+ const columnRenderers = new WeakMap();
607
+ function columnRenderer(col) {
608
+ if (!col.type || col.dataCell)
609
+ return null;
610
+ let renderer = columnRenderers.get(col);
611
+ if (!renderer) {
612
+ renderer = valueRenderer(col.type);
613
+ columnRenderers.set(col, renderer);
614
+ }
615
+ return renderer;
616
+ }
548
617
  export class TosiTable extends WebComponent {
549
618
  static preferredTagName = 'tosi-table';
550
619
  // Layout: a single .scroll-area inside :host is the only scroll container
@@ -968,7 +1037,9 @@ export class TosiTable extends WebComponent {
968
1037
  // position: sticky lives in `.col-pinned` (added by cellClasses), so only
969
1038
  // the per-cell offsets need to be set inline here.
970
1039
  const style = {
971
- justifyContent: col.align || 'left',
1040
+ // Explicit align wins; else the column type's default (numerics right,
1041
+ // booleans center); else left.
1042
+ justifyContent: col.align || columnRenderer(col)?.align || 'left',
972
1043
  ...extra,
973
1044
  };
974
1045
  if (si.left != null)
@@ -993,6 +1064,27 @@ export class TosiTable extends WebComponent {
993
1064
  this.applyGridCellAttrs(cell, colIndex, si, style);
994
1065
  return cell;
995
1066
  }
1067
+ // A `type` column formats through its cached ValueRenderer. The binding's toDOM
1068
+ // runs per stamped row, so it stays locale-reactive and works for icon cells
1069
+ // (which replace children) as well as text.
1070
+ const renderer = columnRenderer(col);
1071
+ if (renderer) {
1072
+ return span({
1073
+ class: this.cellClasses('td', si),
1074
+ role: 'gridcell',
1075
+ tabindex: -1,
1076
+ ariaColindex: String(colIndex + 1),
1077
+ style,
1078
+ bind: {
1079
+ value: item[col.prop],
1080
+ binding: {
1081
+ toDOM(el, val) {
1082
+ renderer.toDOM(el, val);
1083
+ },
1084
+ },
1085
+ },
1086
+ });
1087
+ }
996
1088
  return span({
997
1089
  class: this.cellClasses('td', si),
998
1090
  role: 'gridcell',
@@ -1287,7 +1379,7 @@ export class TosiTable extends WebComponent {
1287
1379
  return cells?.[colIndex] ?? null;
1288
1380
  }
1289
1381
  _pendingFocus = null;
1290
- onScrollEnd = () => {
1382
+ handleScrollEnd = () => {
1291
1383
  if (!this._pendingFocus)
1292
1384
  return;
1293
1385
  const { row, col } = this._pendingFocus;
@@ -1659,7 +1751,7 @@ export class TosiTable extends WebComponent {
1659
1751
  ...visibleBinding,
1660
1752
  this._tbodyBottom,
1661
1753
  ].filter(Boolean));
1662
- this._scrollArea.addEventListener('scrollend', this.onScrollEnd);
1754
+ this._scrollArea.addEventListener('scrollend', this.handleScrollEnd);
1663
1755
  this.append(this._scrollArea);
1664
1756
  this.observePinnedRowMutations();
1665
1757
  this.tagPinnedRows();
@@ -726,7 +726,12 @@ export function createDocBrowser(options) {
726
726
  const loadSource = async (p) => {
727
727
  try {
728
728
  const r = await fetch(`/__docstore/source?file=${encodeURIComponent(p)}`);
729
- if (r.ok)
729
+ // Accept only a genuine source response. A misconfigured dev server (no
730
+ // editableSources) or a SPA-rewrite host answers an unknown path with
731
+ // index.html at status 200 — taking that would load the PAGE as the source.
732
+ // Reject HTML and fall through to the GitHub raw source instead.
733
+ const contentType = r.headers.get('content-type') || '';
734
+ if (r.ok && !/text\/html/i.test(contentType))
730
735
  return await r.text();
731
736
  }
732
737
  catch {
@@ -62,6 +62,17 @@ export function docSystemStyleSpec(theme = {}) {
62
62
  _fontSize: '16px',
63
63
  _codeFontSize: '14px',
64
64
  ...colors,
65
+ // Bridge the tosijs component palette (`--tosi-*`) onto the doc-system's color
66
+ // family. Defined as REFERENCES, so a component reading `var(--tosi-bg)` follows
67
+ // the site theme automatically — including dark mode, where `invertLuminance()`
68
+ // flips the referenced `--background`/`--text-color`/… in `.darkmode`. Without
69
+ // this the doc-system never set `--tosi-*`, so components fell back to their
70
+ // baked-in light defaults (e.g. `var(--tosi-bg, #fff)` → white <tosi-table> in
71
+ // dark mode). Colors only — component metrics keep their own defaults.
72
+ _tosiAccent: vars.brandColor,
73
+ _tosiBg: vars.background,
74
+ _tosiText: vars.textColor,
75
+ _tosiBgInset: vars.insetBg,
65
76
  _spacing: '10px',
66
77
  _lineHeight: 'calc(var(--font-size) * 1.6)',
67
78
  _h1Scale: '2',
@@ -581,11 +581,21 @@ export async function devServer(config, opts = {}) {
581
581
  // Source read/write for in-browser "edit page source" (opt-in, dev only).
582
582
  // A write lands in the repo file; the chokidar watcher then rebuilds and
583
583
  // the page refreshes — the build itself is the preview.
584
- if (config.editableSources && reqPath === '/__docstore/source') {
584
+ if (reqPath === '/__docstore/source') {
585
+ // Handle this endpoint UNCONDITIONALLY so it never falls through to the SPA
586
+ // index.html fallback below. A 200-with-HTML there is silently corrupting:
587
+ // the client loads the PAGE as the "source" (edit-page-source shows HTML;
588
+ // save-to-source reads HTML). When editing isn't enabled, answer with a clean
589
+ // status so `loadSource` falls back to the GitHub raw source (read-only), which
590
+ // is what the deployed static site already does.
591
+ if (!config.editableSources) {
592
+ return new Response('editableSources is not enabled in this doc-site config (set editableSources: true to edit/save source in dev)', { status: 501, headers: { 'Content-Type': 'text/plain; charset=utf-8' } });
593
+ }
585
594
  if (request.method === 'GET')
586
595
  return handleReadSource(request);
587
596
  if (request.method === 'POST')
588
597
  return handleWriteSource(request);
598
+ return new Response('method not allowed', { status: 405 });
589
599
  }
590
600
  if (reqPath === '/')
591
601
  reqPath = '/index.html';