tosijs-ui 1.7.2 → 1.7.4

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.
Files changed (41) hide show
  1. package/CHANGELOG.md +46 -0
  2. package/dist/color-input.d.ts +0 -1
  3. package/dist/color-input.js +24 -10
  4. package/dist/doc-browser.d.ts +9 -0
  5. package/dist/doc-browser.js +54 -9
  6. package/dist/doc-system/doc-system-styles.d.ts +2 -0
  7. package/dist/doc-system/doc-system-styles.js +15 -1
  8. package/dist/doc-system/doc-system.js +1 -0
  9. package/dist/doc-system/site/check-examples-cli.js +5 -3
  10. package/dist/doc-system/site/check-examples.d.ts +13 -0
  11. package/dist/doc-system/site/check-examples.js +26 -10
  12. package/dist/doc-system/site/generate-site.d.ts +2 -0
  13. package/dist/doc-system/site/generate-site.js +1 -0
  14. package/dist/doc-system/site/orchestrator.js +16 -2
  15. package/dist/doc-system/site/site-config.d.ts +7 -0
  16. package/dist/form.d.ts +0 -1
  17. package/dist/form.js +35 -21
  18. package/dist/hydrate/hydrate.js +82 -82
  19. package/dist/hydrate/hydrate.js.map +16 -15
  20. package/dist/iife.js +114 -114
  21. package/dist/iife.js.map +16 -15
  22. package/dist/index.d.ts +1 -0
  23. package/dist/index.js +1 -0
  24. package/dist/live-example/code-transform.d.ts +14 -0
  25. package/dist/live-example/code-transform.js +20 -2
  26. package/dist/live-example/component.d.ts +24 -51
  27. package/dist/live-example/component.js +44 -51
  28. package/dist/live-example/styles.d.ts +22 -50
  29. package/dist/live-example/styles.js +48 -49
  30. package/dist/live-example/types.d.ts +2 -2
  31. package/dist/pocket-bar.d.ts +99 -0
  32. package/dist/pocket-bar.js +298 -0
  33. package/dist/pop-float.d.ts +1 -1
  34. package/dist/pop-float.js +13 -7
  35. package/dist/segmented.d.ts +0 -1
  36. package/dist/segmented.js +38 -20
  37. package/dist/tsconfig.tsbuildinfo +1 -1
  38. package/dist/version.d.ts +1 -1
  39. package/dist/version.js +1 -1
  40. package/llms.txt +3 -1
  41. package/package.json +3 -3
package/CHANGELOG.md CHANGED
@@ -1,5 +1,51 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.7.4
4
+
5
+ - **Configurable header logo.** A new `logo` option lets a doc site set the brand mark shown
6
+ left of the site title, instead of always inheriting the tosijs-ui logo. Accepts the name of a
7
+ known icon (from `tosijs-ui`'s `icons`, e.g. `'tosiUi'`), an image URL / path / `data:` URI
8
+ (rendered as an `<img>`), or a raw inline `<svg>…</svg>` string. Threaded through
9
+ `SiteConfig.logo` (`tosijs-ui/site`), `createDocBrowser({ logo })`, and an embedded
10
+ `<tosi-doc-system config='{"logo":"…"}'>`. Omit it to keep the prior behavior: the tosijs-ui
11
+ logo when `projectLinks.tosijs` is set, otherwise no mark. Previously the header logo was
12
+ hard-wired to the tosijs-ui mark — the one branding element `SiteConfig` couldn't override.
13
+
14
+ ## 1.7.3
15
+
16
+ - **New `<tosi-pocket-bar>` component.** A "pocket buttonbar": a single translucent icon at
17
+ rest that expands into a bar of slotted icon-button controls on hover/tap, then collapses
18
+ again. `direction` (the full `FloatPosition` vocabulary, `auto` by default) determines both
19
+ layout and growth direction; the handle stays put and toggles the bar; touch counts as hover.
20
+ Built as a thin wrapper around `positionFloat`.
21
+ - **The live-example toolbar is now a pocket bar**, pinned to the top-right of the preview (so
22
+ it never sits over the code editors in side-by-side mode). The collapsed handle carries the
23
+ example's pass/fail color, "run tests" is a checkbox that greys when off, and the bar stays
24
+ visible when the example is maximized.
25
+ - **Build system is fragility-tolerant.** An *unsupported import* in a live example (a bare
26
+ specifier the runtime can't resolve) now downgrades that block to display-only with a warning
27
+ instead of failing the whole build; genuine syntax errors still fail the build.
28
+ - **Removed a render-skip anti-pattern** from `<tosi-segmented>`, `<tosi-color>`, and
29
+ `<tosi-form>`. They previously depended on `render()` being *skipped* after interaction via a
30
+ `valueChanged` flag — fragile timing that broke under tosijs's parts changes. Renders are now
31
+ idempotent: they skip only *provably-redundant* writes by comparing values (colors compared
32
+ **parsed**, so a `#rrggbb ↔ rgba()` round-trip no longer clobbers the caret/selection in the
33
+ field being edited).
34
+ - **Code editor background is now a neutral off-white** (off-black in dark mode) instead of a
35
+ brand-tinted color — both the default `<tosi-code>` and live examples.
36
+ - **Fix: nav flickered on refresh / reflowed on hydration.** The static no-JS nav relied on the
37
+ browser's native `<details>` collapse (`::details-content { content-visibility: hidden }`),
38
+ which Chromium applies lazily and non-deterministically on first paint — a closed section
39
+ could render at full height for a frame before snapping shut. Closed sections now collapse
40
+ synchronously via `display: none` (still opening on a no-JS `<summary>` toggle). Enforced by
41
+ `tests/hydration.pw.ts`.
42
+ - **Moved the doc-browser test-counter widget to the bottom-left** so it no longer sits under
43
+ the haltija dev overlay.
44
+ - **tosijs bumped to `^1.7.8`**, which fixes the `this.parts` proxy crossing into nested
45
+ component instances (tosijs#20 — surfaced by the self-hosting doc demo, where clicking "edit"
46
+ on one example opened the editor in the wrong one) and change-handler value staleness
47
+ (tosijs#21).
48
+
3
49
  ## 1.7.2
4
50
 
5
51
  - **Fix: doc sites are now mount-point-agnostic** (issue #25). The build baked `basePath` into
@@ -33,7 +33,6 @@ declare class ColorInput extends Component<ColorParts> {
33
33
  };
34
34
  };
35
35
  content: HTMLInputElement[];
36
- private valueChanged;
37
36
  update: (event: Event) => void;
38
37
  connectedCallback(): void;
39
38
  render(): void;
@@ -71,7 +71,6 @@ class ColorInput extends Component {
71
71
  }),
72
72
  input({ title: 'css color spec', part: 'css' }),
73
73
  ];
74
- valueChanged = false;
75
74
  update = (event) => {
76
75
  const { rgb, alpha, css } = this.parts;
77
76
  if (event.type === 'input') {
@@ -86,7 +85,6 @@ class ColorInput extends Component {
86
85
  }
87
86
  rgb.style.opacity = String(this.color.a);
88
87
  this.value = this.color.rgba;
89
- this.valueChanged = true;
90
88
  };
91
89
  connectedCallback() {
92
90
  super.connectedCallback();
@@ -96,16 +94,32 @@ class ColorInput extends Component {
96
94
  css.addEventListener('change', this.update);
97
95
  }
98
96
  render() {
99
- if (this.valueChanged) {
100
- this.valueChanged = false;
101
- return;
102
- }
103
97
  const { rgb, alpha, css } = this.parts;
104
98
  this.color = Color.fromCss(this.value);
105
- rgb.value = this.color.html.substring(0, 7);
106
- rgb.style.opacity = String(this.color.a);
107
- alpha.value = String(this.color.a);
108
- css.value = this.color.html;
99
+ const rgba = this.color.rgba;
100
+ const alphaStr = String(this.color.a);
101
+ // Idempotent: only WRITE an input whose value doesn't already represent this
102
+ // colour. Compare the PARSED colour, not the string — `#rrggbb` and `rgba(...)`
103
+ // are the same colour, so a string compare (or a blind write) would clobber the
104
+ // caret / selection and re-round-trip the user's format on the field they're
105
+ // editing, every render. Skipping provably-redundant work is right; a `valueChanged`
106
+ // timing flag to decide it was the fragile part (broke under tosijs 1.7.7).
107
+ const sameColor = (v) => {
108
+ try {
109
+ return Color.fromCss(v).rgba === rgba;
110
+ }
111
+ catch {
112
+ return false;
113
+ }
114
+ };
115
+ if (!sameColor(rgb.value))
116
+ rgb.value = this.color.html.substring(0, 7);
117
+ if (rgb.style.opacity !== alphaStr)
118
+ rgb.style.opacity = alphaStr;
119
+ if (alpha.value !== alphaStr)
120
+ alpha.value = alphaStr;
121
+ if (!sameColor(css.value))
122
+ css.value = this.color.html;
109
123
  }
110
124
  }
111
125
  export const colorInput = ColorInput.elementCreator();
@@ -61,6 +61,15 @@ export interface DocBrowserOptions {
61
61
  context?: Record<string, any>;
62
62
  projectName?: string;
63
63
  projectLinks?: ProjectLinks;
64
+ /**
65
+ * The brand mark shown left of the site title in the header. One of:
66
+ * - the name of a known icon (from `icons`), e.g. `'tosiUi'`
67
+ * - an image URL / path / data: URI (rendered as an `<img>`)
68
+ * - a raw inline `<svg …>…</svg>` string
69
+ * Omit to fall back to the tosijs-ui logo when `projectLinks.tosijs` is set,
70
+ * or to no mark at all otherwise.
71
+ */
72
+ logo?: string;
64
73
  navSize?: number;
65
74
  minSize?: number;
66
75
  routing?: DocRoutingMode;
@@ -163,7 +163,7 @@ import { tosiLocalized } from './localize';
163
163
  import { popMenu } from './menu';
164
164
  import { codeEditor } from './code-editor';
165
165
  import { tosiDiff } from './diff';
166
- const { div, span, a, header, button, template, input, h2, details, summary, ul, li, } = elements;
166
+ const { div, span, a, header, button, template, input, h2, img, details, summary, ul, li, } = elements;
167
167
  // Test colors
168
168
  const testColor = {
169
169
  pass: varDefault.testColorPass('#0a0'),
@@ -206,12 +206,14 @@ const testIndicatorStyleSpec = {
206
206
  background: testColor.fail,
207
207
  animation: 'test-pulse 2s ease-in-out infinite',
208
208
  },
209
- // Floating widget - position and colors only, inherits button structure
209
+ // Floating widget - position and colors only, inherits button structure.
210
+ // Bottom-LEFT: bottom-right is where the injected haltija dev widget sits, and it
211
+ // was overlaying this.
210
212
  '.test-widget': {
211
213
  _testBg: testColor.running,
212
214
  position: 'fixed',
213
215
  bottom: vars.spacing,
214
- right: vars.spacing,
216
+ left: vars.spacing,
215
217
  zIndex: '1000',
216
218
  background: vars.testBg,
217
219
  color: 'white',
@@ -252,11 +254,58 @@ const testIndicatorStyleSpec = {
252
254
  borderRadius: vars.roundedRadius,
253
255
  },
254
256
  };
257
+ // The brand mark shown left of the site title. `logo` (from config) may name a
258
+ // known icon, be an inline `<svg>` string, or be an image URL; when absent we
259
+ // keep the legacy behavior — the tosijs-ui logo iff this is a tosijs project,
260
+ // else nothing. Sized to match the header height so hydration doesn't reflow.
261
+ const LOGO_SIZE = 40;
262
+ const LOGO_GAP = 10;
263
+ // `icons` is a Proxy that returns a factory for ANY string (unknown names get a
264
+ // placeholder glyph), so we can't ask it whether a name is real — we route by the
265
+ // shape of the string instead: an inline `<svg>` renders as-is, a URL/path/data:
266
+ // URI renders as an <img>, and anything else is treated as an icon(-composition)
267
+ // name.
268
+ function looksLikeImageUrl(s) {
269
+ return (/^(https?:)?\/\//.test(s) ||
270
+ s.startsWith('data:') ||
271
+ s.startsWith('/') ||
272
+ s.startsWith('./') ||
273
+ s.startsWith('../') ||
274
+ /\.(png|jpe?g|gif|webp|avif|svg|ico)(\?.*)?$/i.test(s));
275
+ }
276
+ function logoMark(logo, projectLinks) {
277
+ if (logo) {
278
+ if (logo.trimStart().startsWith('<')) {
279
+ const holder = span({
280
+ style: {
281
+ display: 'inline-flex',
282
+ height: `${LOGO_SIZE}px`,
283
+ marginRight: `${LOGO_GAP}px`,
284
+ },
285
+ });
286
+ holder.innerHTML = logo;
287
+ return holder;
288
+ }
289
+ if (looksLikeImageUrl(logo)) {
290
+ return img({
291
+ src: logo,
292
+ alt: '',
293
+ style: { height: `${LOGO_SIZE}px`, marginRight: `${LOGO_GAP}px` },
294
+ });
295
+ }
296
+ return icons[logo]({
297
+ style: { _xinIconSize: LOGO_SIZE, marginRight: LOGO_GAP },
298
+ });
299
+ }
300
+ return projectLinks.tosijs
301
+ ? icons.tosiUi({ style: { _xinIconSize: LOGO_SIZE, marginRight: LOGO_GAP } })
302
+ : span();
303
+ }
255
304
  // Monotonic per-page counter so each createDocBrowser() call gets a distinct
256
305
  // tosi() registry key (see stateKey below) and two browsers can't share state.
257
306
  let docBrowserSeq = 0;
258
307
  export function createDocBrowser(options) {
259
- const { docs, context = {}, projectName = '', projectLinks = {}, navSize = 200, minSize = 600, routing = 'query', initialRoute, onRouteChange, navbarLinks, contentElement, } = options;
308
+ const { docs, context = {}, projectName = '', projectLinks = {}, logo, navSize = 200, minSize = 600, routing = 'query', initialRoute, onRouteChange, navbarLinks, contentElement, } = options;
260
309
  // Memory routing is fully self-contained: it never reads or writes
261
310
  // window.history/location, so an embedded or nested browser can't hijack the
262
311
  // host page's URL (or recurse into it).
@@ -520,11 +569,7 @@ export function createDocBrowser(options) {
520
569
  alignItems: 'center',
521
570
  borderBottom: 'none',
522
571
  },
523
- }, projectLinks.tosijs
524
- ? icons.tosiUi({
525
- style: { _xinIconSize: 40, marginRight: 10 },
526
- })
527
- : span(), h2(projectName)));
572
+ }, logoMark(logo, projectLinks), h2(projectName)));
528
573
  }
529
574
  headerContent.push(span({ class: 'elastic' }));
530
575
  // A header link renders as an icon (if `icon` names a known icon) or its label.
@@ -8,6 +8,8 @@ export interface DocSystemTheme {
8
8
  text?: string;
9
9
  buttonBg?: string;
10
10
  inputBg?: string;
11
+ /** code-editor surface (`--code-bg`); neutral off-white by default, not brand-tinted. */
12
+ codeBg?: string;
11
13
  }
12
14
  /** Compute the full set of `:root` color variables from a few base colors. */
13
15
  export declare function docSystemColors(theme?: DocSystemTheme): XinStyleRule;
@@ -34,7 +34,9 @@ export function docSystemColors(theme = {}) {
34
34
  _placeholderColor: brandColor.opacity(0.4),
35
35
  _brandTextColor: brandColor.rotate(30).brighten(0.9),
36
36
  _insetBg: brandColor.rotate(45).brighten(0.8),
37
- _codeBg: brandColor.rotate(-15).desaturate(0.5).brighten(0.9),
37
+ // Neutral off-white (not brand-tinted); dark mode inverts its luminance to
38
+ // off-black, so the editor surface stays a clean neutral in both themes.
39
+ _codeBg: theme.codeBg ?? '#fdfdfd',
38
40
  _linkColor: brandColor.rotate(-30).darken(0.5),
39
41
  _shadowColor: '#0004',
40
42
  _menuBg: '#fafafa',
@@ -476,6 +478,18 @@ export function docSystemStyleSpec(theme = {}) {
476
478
  '.doc-nav details > ul': {
477
479
  paddingLeft: vars.spacing,
478
480
  },
481
+ // Collapse a closed section's list SYNCHRONOUSLY. The UA collapses <details>
482
+ // via `::details-content { content-visibility: hidden }`, which is applied
483
+ // lazily and — for this static tree — non-deterministically on first paint:
484
+ // sometimes the closed section is still laid out at full height for a frame
485
+ // before it snaps shut. That frame is a visible flicker on refresh, and it made
486
+ // the "nav does not move on hydration" test measure a 36px row that hydration
487
+ // then collapses to 0. An explicit display:none collapses it in the first layout
488
+ // pass with no content-visibility race, and still opens on a no-JS <summary>
489
+ // toggle (the UA adds [open], so this rule stops matching).
490
+ '.doc-nav details:not([open]) > ul': {
491
+ display: 'none',
492
+ },
479
493
  // <summary> is the section header (its triangle toggles; the link navigates).
480
494
  '.doc-nav summary': {
481
495
  cursor: 'pointer',
@@ -409,6 +409,7 @@ export class TosiDocSystem extends Component {
409
409
  },
410
410
  projectName: config.projectName,
411
411
  projectLinks: config.projectLinks,
412
+ logo: config.logo,
412
413
  navbarLinks: navbarLinks.length ? navbarLinks : undefined,
413
414
  contentElement,
414
415
  });
@@ -20,8 +20,9 @@ the bundle step, the ePub step, and generate-css; see the Bun.build note in
20
20
  orchestrator.ts.
21
21
 
22
22
  bun check-examples-cli.ts <docs.json> [contextKey ...]
23
- → writes ExampleProblem[] as JSON to stdout; exit 0 whether or not problems were
24
- found (a problem is data, not a crash — only a real failure exits non-zero).
23
+ → writes { problems, warnings, bakes } as JSON to stdout; exit 0 whether or not
24
+ problems were found (a problem is data, not a crash — only a real failure exits
25
+ non-zero). The parent fails the build on `problems`, warns on `warnings`.
25
26
 
26
27
  Build-time only. Never import this from browser code.
27
28
  */
@@ -36,7 +37,7 @@ const contextKeys = process.argv.slice(3);
36
37
  // non-context imports validate as dynamic `<prefix><spec>` imports rather than failing.
37
38
  const importPrefix = process.env.TOSI_IMPORT_PREFIX || undefined;
38
39
  const corpus = JSON.parse(await Bun.file(docsJson).text());
39
- const { problems, bakes } = await checkExamples(corpus, {
40
+ const { problems, warnings, bakes } = await checkExamples(corpus, {
40
41
  ...(contextKeys.length ? { contextKeys } : {}),
41
42
  ...(importPrefix ? { importPrefix } : {}),
42
43
  });
@@ -45,6 +46,7 @@ const { problems, bakes } = await checkExamples(corpus, {
45
46
  // Maps can't JSON-roundtrip, so bakes go over as [filename, [source, {dialect,js}][]][].
46
47
  process.stdout.write(JSON.stringify({
47
48
  problems,
49
+ warnings,
48
50
  bakes: Array.from(bakes.entries()).map(([filename, docBakes]) => [
49
51
  filename,
50
52
  Array.from(docBakes.entries()),
@@ -8,7 +8,20 @@ export interface ExampleProblem {
8
8
  snippet: string;
9
9
  }
10
10
  export interface ExampleCheck {
11
+ /**
12
+ * Blocks that failed to build for a real reason (a tjs/TS syntax error, a
13
+ * transpile failure). These FAIL the build — broken code shouldn't ship.
14
+ */
11
15
  problems: ExampleProblem[];
16
+ /**
17
+ * Blocks that reference a package the environment can't provide (a non-context
18
+ * import, no import-resolver). The code isn't broken, it just can't run here —
19
+ * almost always illustrative code that should be tagged `typescript` (display-
20
+ * only) rather than `ts`. These WARN and are treated as display-only; they do
21
+ * NOT fail the build. (Enable `importResolver`, or tag the block display-only,
22
+ * to silence the warning.)
23
+ */
24
+ warnings: ExampleProblem[];
12
25
  /**
13
26
  * Build-time transpiled JS for `tjs` blocks, grouped by doc filename, each keyed
14
27
  * by exact source text. The renderer embeds a doc's bakes as hidden scripts (so
@@ -4,17 +4,25 @@ Build-time transpile check for live examples.
4
4
  Every executable code block in the corpus (`js` / `tjs` / `ts` / `test`) is put
5
5
  through the SAME front half of the runtime pipeline the live-example component
6
6
  uses — `rewriteImports` → the tjs-lang transform → `new AsyncFunction(...)` — so a
7
- block that can't build (a tjs/TS syntax error, an unsupported import, or
8
- illustrative config mistakenly tagged with an executable language like `ts`
9
- instead of the display-only `typescript`) fails the build *at authoring time*,
10
- on every page, instead of silently rendering an error only when someone opens
11
- that page. `html` / `css` and display-only languages (`typescript`, `json`, …)
12
- are not executed, so they're skipped.
7
+ block that can't build is caught *at authoring time*, on every page, instead of
8
+ silently rendering an error only when someone opens that page. The two outcomes
9
+ are graded differently (this is the point):
10
+
11
+ - a real **syntax / transpile error** is broken code → a `problem` that FAILS
12
+ the build;
13
+ - an **unsupported import** (a non-context package, no import-resolver) is fine
14
+ code the doc environment just can't run → a `warning`: the block is treated as
15
+ display-only and the build survives. It's almost always illustrative code that
16
+ should have been tagged `typescript` instead of `ts` — a mistagging shouldn't
17
+ take the whole build down with it.
18
+
19
+ `html` / `css` and display-only languages (`typescript`, `json`, …) are not
20
+ executed, so they're skipped.
13
21
 
14
22
  Build-time only (bun). Never import from browser code.
15
23
  */
16
24
  import { marked } from 'marked';
17
- import { rewriteImports, AsyncFunction, loadTransform, } from '../../live-example/code-transform';
25
+ import { rewriteImports, AsyncFunction, loadTransform, UnsupportedImportError, } from '../../live-example/code-transform';
18
26
  // The default live-example context (matches the IIFE globals the pages provide).
19
27
  // A project that sets a custom `context` on its <tosi-doc-system> can pass its
20
28
  // own keys; these are the tosijs-ui defaults.
@@ -58,6 +66,7 @@ function collectCodeTokens(text) {
58
66
  export async function checkExamples(docs, opts = {}) {
59
67
  const contextKeys = opts.contextKeys ?? DEFAULT_CONTEXT_KEYS;
60
68
  const problems = [];
69
+ const warnings = [];
61
70
  const bakes = new Map();
62
71
  for (const doc of docs) {
63
72
  for (const block of collectCodeTokens(doc.text)) {
@@ -97,17 +106,24 @@ export async function checkExamples(docs, opts = {}) {
97
106
  }
98
107
  }
99
108
  catch (err) {
100
- problems.push({
109
+ const entry = {
101
110
  filename: doc.filename,
102
111
  title: doc.title,
103
112
  lang: block.lang,
104
113
  error: err.message || String(err),
105
114
  snippet: block.text.split('\n').slice(0, 3).join('\n'),
106
- });
115
+ };
116
+ // An unsupported import means the block references deps the environment
117
+ // can't provide — not broken code. Warn (display-only), don't fail the
118
+ // build. A real syntax/transpile error is a genuine problem.
119
+ if (err instanceof UnsupportedImportError)
120
+ warnings.push(entry);
121
+ else
122
+ problems.push(entry);
107
123
  }
108
124
  }
109
125
  }
110
- return { problems, bakes };
126
+ return { problems, warnings, bakes };
111
127
  }
112
128
  /** Format problems for a build log. */
113
129
  export function formatExampleProblems(problems) {
@@ -18,6 +18,8 @@ export interface GenerateSiteConfig {
18
18
  /** default og:image (per-page overridable via doc metadata) */
19
19
  ogImage?: string;
20
20
  projectLinks?: ProjectLinks;
21
+ /** brand mark left of the title: icon name, image URL, or inline <svg> */
22
+ logo?: string;
21
23
  /** header-bar links (rendered as real <a> for no-JS, upgraded on hydration) */
22
24
  navbarLinks?: LinkItem[];
23
25
  /** translation table (TSV) for the settings menu's language picker */
@@ -251,6 +251,7 @@ export async function generateSite(config) {
251
251
  const configAttr = escapeAttr(JSON.stringify({
252
252
  projectName: config.projectName,
253
253
  projectLinks: config.projectLinks,
254
+ logo: config.logo,
254
255
  }));
255
256
  // The theme stylesheet (config.stylesUrl) is written separately by
256
257
  // ./generate-css.ts; pages here just <link> to it.
@@ -132,6 +132,7 @@ async function checkExamplesInChild(docsJson, importPrefix) {
132
132
  const payload = JSON.parse(out);
133
133
  return {
134
134
  problems: payload.problems,
135
+ warnings: payload.warnings ?? [],
135
136
  bakes: new Map(payload.bakes.map(([file, e]) => [file, new Map(e)])),
136
137
  };
137
138
  }
@@ -239,14 +240,26 @@ export async function buildSite(config) {
239
240
  : undefined;
240
241
  let exampleBakes;
241
242
  if (config.checkExamples !== false) {
242
- const { problems, bakes } = await checkExamplesInChild(DOCS_JSON, resolverPrefix);
243
+ const { problems, warnings, bakes } = await checkExamplesInChild(DOCS_JSON, resolverPrefix);
243
244
  exampleBakes = bakes;
245
+ // Unsupported imports don't fail the build — the code isn't broken, it just
246
+ // can't run in the doc environment (almost always illustrative code that
247
+ // should be tagged `typescript`). Warn loudly, treat the block as display-only,
248
+ // and keep going. Only real syntax/transpile errors below are fatal.
249
+ if (warnings.length) {
250
+ console.warn(`⚠️ doc-site build: ${warnings.length} live example block(s) can't run ` +
251
+ `here and were left as display-only:\n\n` +
252
+ formatExampleProblems(warnings) +
253
+ `\n\nTag each with a display-only language like \`typescript\` (instead of` +
254
+ ` \`js\`/\`ts\`) to silence this, or enable \`importResolver\` to import` +
255
+ ` other packages.\n`);
256
+ }
244
257
  if (problems.length) {
245
258
  throw new Error(`doc-site build: ${problems.length} live example(s) failed to build:\n\n` +
246
259
  formatExampleProblems(problems) +
247
260
  `\n\nFix the code, or — if a block is illustrative and not meant to run —` +
248
261
  ` tag it with a display-only language like \`typescript\` instead of` +
249
- ` \`js\`/\`ts\`. (Disable with checkExamples: false.)`);
262
+ ` \`js\`/\`ts\`. (Disable all checking with checkExamples: false.)`);
250
263
  }
251
264
  }
252
265
  // Copy static-asset dirs into the web root.
@@ -587,6 +600,7 @@ export async function buildSite(config) {
587
600
  baseUrl: config.baseUrl,
588
601
  lang: config.lang,
589
602
  projectLinks: config.projectLinks,
603
+ logo: config.logo,
590
604
  navbarLinks: config.navbarLinks,
591
605
  localizedStrings: config.localizedStrings,
592
606
  favicon: config.favicon,
@@ -30,6 +30,13 @@ export interface SiteConfig {
30
30
  headExtra?: string;
31
31
  /** logo + view-source links (createDocBrowser projectLinks) */
32
32
  projectLinks?: ProjectLinks;
33
+ /**
34
+ * Brand mark shown left of the site title in the header. One of: the name of a
35
+ * known icon (from `tosijs-ui`'s `icons`, e.g. `'tosiUi'`), an image URL / path
36
+ * / `data:` URI, or a raw inline `<svg …>…</svg>` string. Omit to fall back to
37
+ * the tosijs-ui logo when `projectLinks.tosijs` is set, or to no mark otherwise.
38
+ */
39
+ logo?: string;
33
40
  /** header-bar icon links */
34
41
  navbarLinks?: LinkItem[];
35
42
  /** base theme colors — most of the palette is derived from `accent` */
package/dist/form.d.ts CHANGED
@@ -45,7 +45,6 @@ export declare class TosiField extends XinComponent {
45
45
  };
46
46
  value: any;
47
47
  content: HTMLLabelElement;
48
- private valueChanged;
49
48
  handleChange: () => void;
50
49
  connectedCallback(): void;
51
50
  render(): void;
package/dist/form.js CHANGED
@@ -430,6 +430,10 @@ function getInputValue(input) {
430
430
  : input.value;
431
431
  }
432
432
  }
433
+ // Idempotent: write only when the value actually differs. Blindly re-assigning
434
+ // `input.value` (even the same string) moves the caret / drops the selection of a
435
+ // field being edited, so render must not do it as redundant work — and it lets
436
+ // render run any number of times without side-effects.
433
437
  function setElementValue(input, value) {
434
438
  if (!(input instanceof HTMLElement)) {
435
439
  // do nothing
@@ -437,20 +441,26 @@ function setElementValue(input, value) {
437
441
  else if (input instanceof HTMLInputElement) {
438
442
  switch (input.type) {
439
443
  case 'checkbox':
440
- input.checked = value;
444
+ if (input.checked !== Boolean(value))
445
+ input.checked = value;
441
446
  break;
442
- case 'radio':
443
- input.checked = value === input.value;
447
+ case 'radio': {
448
+ const checked = value === input.value;
449
+ if (input.checked !== checked)
450
+ input.checked = checked;
444
451
  break;
445
- default:
446
- input.value = String(value || '');
452
+ }
453
+ default: {
454
+ const v = String(value || '');
455
+ if (input.value !== v)
456
+ input.value = v;
457
+ }
447
458
  }
448
459
  }
449
- else {
450
- if (value != null || input.value != null) {
451
- ;
452
- input.value = String(value || '');
453
- }
460
+ else if (value != null || input.value != null) {
461
+ const v = String(value || '');
462
+ if (input.value !== v)
463
+ input.value = v;
454
464
  }
455
465
  }
456
466
  export class TosiField extends XinComponent {
@@ -499,7 +509,6 @@ export class TosiField extends XinComponent {
499
509
  };
500
510
  value = null;
501
511
  content = label(tosiSlot({ part: 'caption' }), span({ part: 'field' }, tosiSlot({ part: 'input', name: 'input' }), input({ part: 'valueHolder' })));
502
- valueChanged = false;
503
512
  handleChange = () => {
504
513
  const { input, valueHolder } = this.parts;
505
514
  const inputElement = (input.children[0] || valueHolder);
@@ -507,7 +516,6 @@ export class TosiField extends XinComponent {
507
516
  valueHolder.value = inputElement.value;
508
517
  }
509
518
  this.value = getInputValue(inputElement);
510
- this.valueChanged = true;
511
519
  const form = this.closest('tosi-form');
512
520
  if (form && this.key !== '') {
513
521
  switch (this.type) {
@@ -537,25 +545,31 @@ export class TosiField extends XinComponent {
537
545
  input.addEventListener('change', this.handleChange, true);
538
546
  }
539
547
  render() {
540
- if (this.valueChanged) {
541
- this.valueChanged = false;
542
- return;
543
- }
544
548
  const { input, caption, valueHolder, field } = this.parts;
545
549
  if (caption.textContent?.trim() === '') {
546
550
  caption.append(this.caption !== '' ? this.caption : this.key);
547
551
  }
552
+ // Build the type-specific child ONCE and reuse it. Re-creating the <textarea> /
553
+ // <tosi-color> every render (the old behavior, only avoided by a `valueChanged`
554
+ // timing flag) destroyed focus, caret and selection mid-edit — provably redundant
555
+ // when the child of the right kind already exists. The value is reconciled
556
+ // idempotently by setElementValue below.
548
557
  if (this.type === 'text') {
549
- input.textContent = '';
550
- const textarea = elements.textarea({ value: this.value });
558
+ let textarea = input.children[0];
559
+ if (!(textarea instanceof HTMLTextAreaElement)) {
560
+ input.textContent = '';
561
+ textarea = elements.textarea({ value: this.value });
562
+ input.append(textarea);
563
+ }
551
564
  if (this.placeholder) {
552
565
  textarea.setAttribute('placeholder', this.placeholder);
553
566
  }
554
- input.append(textarea);
555
567
  }
556
568
  else if (this.type === 'color') {
557
- input.textContent = '';
558
- input.append(colorInput({ value: this.value }));
569
+ if (!input.children[0]?.matches('tosi-color')) {
570
+ input.textContent = '';
571
+ input.append(colorInput({ value: this.value }));
572
+ }
559
573
  }
560
574
  else if (input.children.length === 0) {
561
575
  attr(valueHolder, 'placeholder', this.placeholder);