tosijs-ui 1.16.21 → 1.16.22

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 (44) hide show
  1. package/CHANGELOG.md +70 -0
  2. package/README.md +2 -2
  3. package/bin/make-icon-data.js +1 -1
  4. package/dist/code-editor.js +2 -3
  5. package/dist/data-table.js +1 -1
  6. package/dist/doc-browser.d.ts +4 -0
  7. package/dist/doc-browser.js +46 -101
  8. package/dist/doc-system/doc-system-styles.js +11 -0
  9. package/dist/doc-system/doc-system.js +1 -1
  10. package/dist/doc-system/highlight-styles.js +11 -16
  11. package/dist/doc-system/routing.d.ts +9 -0
  12. package/dist/doc-system/routing.js +22 -6
  13. package/dist/doc-system/site/build-warnings.d.ts +10 -0
  14. package/dist/doc-system/site/build-warnings.js +24 -0
  15. package/dist/doc-system/site/dev-auth.d.ts +13 -0
  16. package/dist/doc-system/site/dev-auth.js +40 -2
  17. package/dist/doc-system/site/dev-server.js +13 -1
  18. package/dist/doc-system/site/docs.d.ts +14 -0
  19. package/dist/doc-system/site/docs.js +61 -81
  20. package/dist/doc-system/site/epub.js +6 -2
  21. package/dist/doc-system/site/generate-site.js +15 -5
  22. package/dist/doc-system/site/orchestrator.js +1 -0
  23. package/dist/doc-system/site/site-config.d.ts +25 -0
  24. package/dist/editable-rect.js +1 -3
  25. package/dist/highlight-block.js +3 -10
  26. package/dist/icon-rules.d.ts +10 -0
  27. package/dist/icon-rules.js +13 -5
  28. package/dist/icon-svg.js +13 -19
  29. package/dist/icons.js +12 -15
  30. package/dist/iife.js +91 -95
  31. package/dist/iife.js.map +23 -23
  32. package/dist/index.d.ts +1 -0
  33. package/dist/index.js +1 -0
  34. package/dist/live-example/component.js +3 -4
  35. package/dist/menu.js +2 -2
  36. package/dist/natural-compare.js +6 -2
  37. package/dist/password-strength.js +1 -1
  38. package/dist/pocket-bar.js +2 -2
  39. package/dist/size-break.js +4 -3
  40. package/dist/track-drag.js +1 -1
  41. package/dist/version.d.ts +1 -1
  42. package/dist/version.js +1 -1
  43. package/llms.txt +12 -8
  44. package/package.json +1 -1
@@ -1899,6 +1899,8 @@ export async function devServer(config, opts = {}) {
1899
1899
  // A share link's session may browse and may not save (asked AFTER validity, which
1900
1900
  // is the call-order `cookieMayWrite` documents).
1901
1901
  sessionMayWrite: cookieMayWrite(auth, session),
1902
+ trustLan: config.trustLan === true,
1903
+ proxied: isProxiedRequest(request.headers),
1902
1904
  }) &&
1903
1905
  /*
1904
1906
  CSRF (#90). The direct path is peer-address-only, so without this any page the
@@ -1908,7 +1910,9 @@ export async function devServer(config, opts = {}) {
1908
1910
  */
1909
1911
  csrfOk(request, viaTunnel);
1910
1912
  if (!authorized) {
1911
- console.warn(`⚠️ refused ${request.method} /__docstore/source from ${peer ?? 'unknown'} — ` + `no session (use \`${TUNNEL_LINK_CMD}\` for an edit link).`);
1913
+ console.warn(`⚠️ refused ${request.method} /__docstore/source from ${peer ?? 'unknown'} — ` +
1914
+ `no session (use \`${TUNNEL_LINK_CMD}\` for an edit link` +
1915
+ `${config.trustLan ? '' : ', or set trustLan: true for LAN devices'}).`);
1912
1916
  return new Response('not authorized to edit source', {
1913
1917
  status: 403,
1914
1918
  headers: { 'Content-Type': 'text/plain; charset=utf-8' },
@@ -2225,6 +2229,14 @@ export async function devServer(config, opts = {}) {
2225
2229
  process.exit(code);
2226
2230
  };
2227
2231
  console.log(`Listening on https://localhost:${PORT}`);
2232
+ // trustLan widens who may write; say so every launch, never silently (#227).
2233
+ if (config.trustLan && config.editableSources) {
2234
+ console.warn(`⚠️ trustLan: any device on a private network that can reach this machine may read and ` +
2235
+ `edit source. Use it on a network you control.`);
2236
+ }
2237
+ else if (config.trustLan) {
2238
+ console.warn(`⚠️ trustLan does nothing without editableSources: true (there is no source endpoint to trust).`);
2239
+ }
2228
2240
  if (SINK_PATH) {
2229
2241
  console.log(`Debug sink: POST /__debug-sink -> tail -f ${SINK_PATH}`);
2230
2242
  }
@@ -22,6 +22,18 @@ export interface Doc {
22
22
  * pre-rendered public page — so "drafts don't ship" was false twice over.
23
23
  */
24
24
  hidden?: boolean;
25
+ /**
26
+ * This doc is the site root (`/`) instead of README, which then gets `/readme/`. Set by the
27
+ * site's `home` option, or by the doc's own `{"home": true}`. At most one doc may be home.
28
+ */
29
+ home?: boolean;
30
+ /**
31
+ * `false` drops the site's header bar on this page, as `layout: "full-screen"` drops the
32
+ * nav: for an app page with its own toolbar, above all as an installed PWA, where the bar
33
+ * is the only thing between the app and the whole screen. The page links back to the docs
34
+ * its own way; the builder adds nothing. Unset means the header shows.
35
+ */
36
+ header?: boolean;
25
37
  /**
26
38
  * Which book(s) this doc binds into. A name, a list of names, or `'none'` to keep it
27
39
  * on the site and out of every book. Unset means the default book; `'default'` names
@@ -91,6 +103,8 @@ export interface ExtractDocsOptions {
91
103
  paths: string[];
92
104
  ignore?: string[];
93
105
  output?: string;
106
+ /** the doc served at `/` instead of README, by slug or filename (the site's `home`) */
107
+ home?: string;
94
108
  }
95
109
  /**
96
110
  * The title a markdown file implies, when nothing declares one.
@@ -1,97 +1,43 @@
1
1
  /*#
2
2
  # docs
3
3
 
4
- Utility for extracting documentation from markdown files and inline comments in source code.
5
-
6
- > `docs.ts` is intended to be run directly using `bun`. You can transpile it to javascript if you
7
- want to run it using node.
8
-
9
- This is used by the `doc-browser` component to build searchable, navigable documentation
10
- from your project's source files.
4
+ `extractDocs` collects documentation from markdown files and `/*#` comment blocks in source
5
+ code into one corpus, the `docs.json` that the [doc-browser](/doc-browser/) and the static
6
+ site render. `tosijs-ui/site` runs it for you; call it yourself only to build a corpus
7
+ outside that pipeline. It reads files, so it runs under bun or node.
11
8
 
12
9
  ## Usage
13
10
 
14
- import { extractDocs } from 'docs'
15
-
16
- extractDocs({
17
- paths: ['src', 'README.md'],
18
- ignore: ['node_modules', 'dist', 'build']
19
- path: 'public/docs.json'
20
- })
21
-
22
- ## API
23
-
24
- ### `extractDocs(options)`
25
-
26
- Scans directories for markdown files and source code comments.
27
-
28
- **Options:**
29
- - `paths`: Array of directory paths or file paths to scan
30
- - `ignore`: Array of directory names to ignore (default: ['node_modules', 'dist'])
31
- - `output`: if provided, path to write json result.
32
-
33
- **Returns:** Array of `Doc` objects
34
-
35
- ### `Doc` object structure
36
-
37
- {
38
- text: string, // Markdown content
39
- title: string, // First heading or filename
40
- filename: string, // Just the filename
41
- path: string, // Full file path
42
- pin?: 'top' | 'bottom' // Optional pinning for sort order
43
- }
44
-
45
- ## Documentation Format
46
-
47
- ### Markdown files
48
-
49
- Any `.md` file will be included in its entirety.
50
-
51
- ### Source code comments
11
+ ```typescript
12
+ import { extractDocs } from 'tosijs-ui/site'
52
13
 
53
- Multi-line comments that start with `/*#` will be extracted as markdown:
14
+ const docs = extractDocs({
15
+ paths: ['src', 'README.md'],
16
+ output: 'public/docs.json',
17
+ })
18
+ ```
54
19
 
55
- /*#
56
- # My Component
20
+ ## `extractDocs(options)`
57
21
 
58
- This is documentation for my component.
22
+ - `paths`: directories to scan, and individual files (list root `.md` files explicitly)
23
+ - `ignore`: directory names to skip (default `['node_modules', 'dist', 'build']`)
24
+ - `output`: if given, the path the corpus is written to as JSON
25
+ - `home`: the doc served at `/` instead of README, by slug or filename
59
26
 
60
- ```html
61
- <my-component></my-component>
62
- ```
63
- ```js
64
- console.log('hello world')
65
- ```
66
- ```css
67
- my-componet {
68
- color: blue
69
- }
70
- ```
71
- *‎/
72
-
73
- export class MyComponent extends Component {
74
- // implementation
75
- }
76
- ...
77
-
78
- The [doc-browser](/?doc-browser.ts) will render the output as a test-bed project with documentation and live examples.
27
+ Returns an array of `Doc` objects: `text` (the markdown), `title`, `filename`, `path`, and
28
+ whatever metadata the doc declares (`pin`, `order`, `parent`, `layout`, `description`, …).
79
29
 
80
- ### Metadata
30
+ ## What is collected
81
31
 
82
- You can include JSON metadata in comments to control sorting:
32
+ - **`.md` files**, whole. YAML frontmatter is read and removed.
33
+ - **`/*#` … `*‎/` blocks** in `.ts`, `.tjs`, `.js` and `.css` files, as markdown. The first heading
34
+ is the page title. A block counts only when `/*#` starts a line.
35
+ - **Files whose name starts with `_`** are skipped: use the prefix for drafts and fragments.
83
36
 
84
- html:
85
- <!--{ "pin": "bottom" }-->
86
-
87
- ts, js, css:
88
- /*{ "pin": "bottom" }*‎/
89
-
90
- This pins the document to the top or bottom of the navigation list. Within a
91
- pin bucket, add `"order"` (a number, lower first; default 500) to rank items —
92
- e.g. `{ "pin": "top", "order": 1 }` above `{ "pin": "top", "order": 2 }`.
37
+ Metadata blocks (`<!--{ … }-->` in markdown, `/*{ … }*‎/` in code), nav order, sections and
38
+ which code fences run live are described under [Doc format](/doc-site-system/#doc-format).
93
39
  */
94
- /*{"pin":"bottom","parent":"Appendices"}*/
40
+ /*{ "parent": "Documentation System", "order": 60 }*/
95
41
  // TODO CLI options
96
42
  import * as fs from 'fs';
97
43
  import * as path from 'path';
@@ -208,6 +154,14 @@ export function parseFrontmatter(content) {
208
154
  else if (val)
209
155
  console.error(`unknown layout ${JSON.stringify(val)} in frontmatter`);
210
156
  }
157
+ else if (key === 'header') {
158
+ if (/^(false|no|0)$/i.test(val))
159
+ data.header = false;
160
+ }
161
+ else if (key === 'home') {
162
+ if (/^(true|yes|1)$/i.test(val))
163
+ data.home = true;
164
+ }
211
165
  }
212
166
  if (!matched)
213
167
  return { data: {}, body: content }; // a bare `---`, not frontmatter
@@ -513,7 +467,7 @@ walked) {
513
467
  return markdownFiles.sort(pinnedSort);
514
468
  }
515
469
  export function extractDocs(options) {
516
- const { paths, ignore = ['node_modules', 'dist', 'build'], output } = options;
470
+ const { paths, ignore = ['node_modules', 'dist', 'build'], output, home, } = options;
517
471
  const walked = new Set();
518
472
  const found = findMarkdownFiles(paths, ignore, new Set(DEFAULT_DOC_IGNORES), walked);
519
473
  /*
@@ -529,7 +483,18 @@ export function extractDocs(options) {
529
483
  Filtering at extraction makes it structural: hidden docs do not exist as far as the
530
484
  rest of the pipeline is concerned. Descendants of a hidden doc go with it.
531
485
  */
486
+ if (home != null)
487
+ markHome(found, home);
488
+ const homes = found.filter((doc) => doc.home);
489
+ if (homes.length > 1) {
490
+ throw new Error(`more than one doc is the home page: ${homes
491
+ .map((doc) => doc.path)
492
+ .join(', ')}. Mark one, with the site's \`home\` option or {"home": true}.`);
493
+ }
532
494
  const docs = withoutHidden(found, buildSlugMap(found));
495
+ if (homes.length === 1 && !docs.includes(homes[0])) {
496
+ throw new Error(`the home page ${homes[0].path} is hidden, so the site would have no root page.`);
497
+ }
533
498
  /*
534
499
  Insets and conditional text are resolved HERE, for the same reason hidden docs are dropped
535
500
  here: the corpus is the thing that ships, so anything assembled later is assembled by each
@@ -550,6 +515,21 @@ export function extractDocs(options) {
550
515
  }
551
516
  return docs;
552
517
  }
518
+ /*
519
+ The site's `home` option names the root doc by slug or filename. Matched against the slugs
520
+ the corpus would have with README as root, which are the names an author sees in the nav
521
+ today. Naming nothing fails the build: a site whose root is not the page its config asks for
522
+ is the silent-failure class this system refuses.
523
+ */
524
+ function markHome(docs, home) {
525
+ const slugs = buildSlugMap(docs.map(({ filename }) => ({ filename })));
526
+ const wanted = home.replace(/^\/+|\/+$/g, '');
527
+ const match = docs.filter((doc) => doc.filename === wanted || slugs[doc.filename] === wanted);
528
+ if (match.length !== 1) {
529
+ throw new Error(`site option home: "${home}" names ${match.length === 0 ? 'no doc' : `${match.length} docs`}. Use a page's slug (as in its URL) or its filename.`);
530
+ }
531
+ match[0].home = true;
532
+ }
553
533
  export function saveDocsJSON(docs, outputPath) {
554
534
  const jsonData = JSON.stringify(docs, null, 2);
555
535
  fs.writeFileSync(outputPath, jsonData, 'utf8');
@@ -620,7 +620,8 @@ export async function buildEpub(config, opts = {}) {
620
620
  docs and anything marked `book: "none"`, so a doc that belongs to no volume cannot leak
621
621
  into the default one just by not naming a book.
622
622
  */
623
- const volumes = partitionByBook(corpus, buildSlugMap(corpus));
623
+ const siteSlugs = buildSlugMap(corpus);
624
+ const volumes = partitionByBook(corpus, siteSlugs);
624
625
  const visible = volumes.get(opts.bookTarget ?? DEFAULT_BOOK) ?? [];
625
626
  /*
626
627
  A volume with no chapters is a failure, not a book.
@@ -659,7 +660,10 @@ export async function buildEpub(config, opts = {}) {
659
660
  }
660
661
  // Curate/reorder for the book (a no-op when config.book is absent).
661
662
  const docs = selectBookDocs(placed, config.book);
662
- const slugMap = buildSlugMap(docs);
663
+ // Slugs come from the whole published corpus, as the site's do: computed from this book's
664
+ // subset, README would take the root whenever the site's `home` doc is outside the book,
665
+ // and the book's links would disagree with the site's URLs.
666
+ const slugMap = siteSlugs;
663
667
  const roots = buildNavTree(docs, slugMap);
664
668
  const fileFor = (d) => `${slugMap[d.filename] || 'index'}.xhtml`;
665
669
  // slug → chapter filename, for the docs actually IN this book — the target set for
@@ -15,7 +15,7 @@ import { highlightHtml } from '../highlight.js';
15
15
  import { buildSlugMap, pathForSlug, rewriteDocLinks, withBase, } from '../routing.js';
16
16
  import { buildNavTree, navOpenPath } from '../nav-tree.js';
17
17
  import { renderDocMarkdown, docDescription, } from '../render.js';
18
- import { deadInPageAnchors, misconfigured } from './build-warnings.js';
18
+ import { bareNameLinks, deadInPageAnchors, misconfigured, } from './build-warnings.js';
19
19
  const escapeAttr = (s) => s
20
20
  .replace(/&/g, '&amp;')
21
21
  .replace(/</g, '&lt;')
@@ -127,9 +127,8 @@ async function pageHtml(doc, config, slugMap, configAttr) {
127
127
  flicker the nav-hydration fix chased in 1.7.3. An attribute in the served markup means the
128
128
  first paint is already correct, and the doc-browser keeps it in step on SPA navigation.
129
129
  */
130
- const layoutAttr = doc.layout
131
- ? ` data-layout="${escapeAttr(doc.layout)}"`
132
- : '';
130
+ const layoutAttr = (doc.layout ? ` data-layout="${escapeAttr(doc.layout)}"` : '') +
131
+ (doc.header === false ? ' data-no-header' : '');
133
132
  // ONE rule, shared with the doc-browser's hydration path — see doc-title.ts. Writing
134
133
  // it twice is what made the title change on hydration (issue #6).
135
134
  const title = pageTitle(doc, projectName);
@@ -311,10 +310,16 @@ export async function generateSite(config) {
311
310
  // ./generate-css.ts; pages here just <link> to it.
312
311
  let count = 0;
313
312
  const deadAnchors = [];
313
+ const bareLinks = [];
314
314
  for (const doc of docs) {
315
315
  const slug = slugMap[doc.filename];
316
316
  const dir = slug === '' ? outputDir : `${outputDir}/${slug}`;
317
- const dead = deadInPageAnchors(renderDocMarkdown(doc.text));
317
+ const rendered = renderDocMarkdown(doc.text);
318
+ const dead = deadInPageAnchors(rendered);
319
+ // On the root page (`/`) a bare name resolves to `/name`, which works; elsewhere it does not.
320
+ const bare = slug === '' ? [] : bareNameLinks(rendered);
321
+ if (bare.length)
322
+ bareLinks.push(` ${doc.path || doc.filename}: ${bare.join(', ')}`);
318
323
  if (dead.length)
319
324
  deadAnchors.push(` ${doc.path || doc.filename}: ${dead
320
325
  .map((d) => `#${d}`)
@@ -326,6 +331,11 @@ export async function generateSite(config) {
326
331
  }
327
332
  count += 1;
328
333
  }
334
+ if (bareLinks.length) {
335
+ misconfigured(config.strict, `\n⚠️ Links written as a bare page name. Every page is served at /slug/, so these\n` +
336
+ ` resolve under the page they are on and reach nothing; write /name/ instead:\n` +
337
+ `${bareLinks.join('\n')}\n`);
338
+ }
329
339
  if (deadAnchors.length) {
330
340
  misconfigured(config.strict, `\n⚠️ In-page links that point at nothing on their page. A heading's id is its text,\n` +
331
341
  ` lowercased, punctuation dropped, spaces as "-" (a repeat gets -1, -2, …):\n` +
@@ -640,6 +640,7 @@ export async function buildSite(config, opts = {}) {
640
640
  ...(config.ignoreDocPaths ?? []),
641
641
  ],
642
642
  output: DOCS_JSON,
643
+ home: config.home,
643
644
  });
644
645
  extract();
645
646
  /*
@@ -53,6 +53,13 @@ export interface SiteConfig {
53
53
  * files explicitly, e.g. ['src', 'README.md', 'Building-Apps.md'].
54
54
  */
55
55
  docPaths?: string[];
56
+ /**
57
+ * The page served at the site root (`/`), by slug or filename, e.g. `home: 'host'` for a
58
+ * site that is an app. Default: README. README then has its own page at `/readme/`, and
59
+ * the nav, canonical links, sitemap, llms.txt and books follow. The build fails if it
60
+ * names no page. A doc can also claim the root itself with `{"home": true}`.
61
+ */
62
+ home?: string;
56
63
  /**
57
64
  * Directories to skip while scanning `docPaths` — by basename (skipped wherever it
58
65
  * appears) or by path (skipped only at that location). Added to the built-in
@@ -603,6 +610,24 @@ export interface SiteConfig {
603
610
  * are confined to the repo root as correctness hygiene. Off by default; opt in.
604
611
  */
605
612
  editableSources?: boolean;
613
+ /**
614
+ * Trust the local network for reading and editing source (#227). With `editableSources` on,
615
+ * a browser on another device on your LAN (a phone, a headset) can open and save source
616
+ * directly, as one at this keyboard can, instead of needing the tunnel and an invite link.
617
+ * That is "Edit page source"; a live example's "Save to source" is still offered only on
618
+ * `localhost`.
619
+ *
620
+ * Trusted: a direct peer at a private address (RFC 1918 IPv4, IPv6 unique-local or
621
+ * link-local). Never trusted: tunnel traffic (it needs a session as before) or a request
622
+ * carrying forwarding headers. Writes still have to come from the dev server's own origin,
623
+ * so a page another site serves cannot write through a LAN browser.
624
+ *
625
+ * What it means: ANYONE on the network can edit your working tree. Turn it on for a network
626
+ * you control, never on shared wifi. And a reverse proxy on your LAN that forwards outside
627
+ * traffic WITHOUT forwarding headers would make that traffic look like a LAN peer. Agent
628
+ * driving, invite links, builds and test reports stay this machine only. Off by default.
629
+ */
630
+ trustLan?: boolean;
606
631
  /**
607
632
  * Give a coding agent (Claude) eyes + hands on your running dev page via
608
633
  * [haltija](https://github.com/tonioloewald/haltija). When on, the dev server
@@ -53,13 +53,11 @@ preview.addEventListener('change', event => console.log(event))
53
53
  background: transparent;
54
54
  box-shadow: none;
55
55
  }
56
-
57
- .previw button
58
56
  ```
59
57
 
60
58
  ## Snapping
61
59
 
62
- When `EditableRect.snapToGrid === true` or the shift-key is depresseed, position will snap to `EditableRect.gridSize` pixels (default = 8).
60
+ When `EditableRect.snapToGrid === true` or the shift-key is depressed, position will snap to `EditableRect.gridSize` pixels (default = 8).
63
61
 
64
62
  Similarly `EditableRect.snapAngle === true` or the shift-key will snap rotation to increments of `EditableRect.angleSize` degrees (default = 15).
65
63
 
@@ -1,3 +1,4 @@
1
+ /*{ "parent": "Documentation System", "order": 70 }*/
1
2
  /*#
2
3
  # highlight-block
3
4
 
@@ -30,16 +31,8 @@ and an unknown language renders as plain, readable code rather than failing.
30
31
  **Colours work out of the box, anywhere.** The element injects its token palette on first use,
31
32
  so a `<tosi-highlight>` on an ordinary page is coloured without importing anything.
32
33
 
33
- That was not always true, and the way it failed is worth knowing if you are debugging
34
- something similar: this package ships **no CSS files at all** — every style is a `StyleSheet()`
35
- call — and the palette used to live inside the doc-system stylesheet. So a standalone element
36
- produced perfectly correct `<span class="token …">` markup rendering in flat black, and this
37
- very paragraph told you to "import the doc-system CSS", which was not an import that existed.
38
-
39
- There are **two palettes** — a light set (the default) and a dark set under `.darkmode` — each
40
- contrast-checked against the `--code-bg` it actually sits on. Not one recomputed from the
41
- other: a light-mode default derived from dark-mode literals is unreadable, which is how this
42
- shipped failing WCAG AA on every token type.
34
+ There are **two palettes**, a light set (the default) and a dark set under `.darkmode`, each
35
+ contrast-checked against the `--code-bg` it sits on (see [highlight-styles](/highlight-styles/)).
43
36
 
44
37
  Every colour is a `varDefault`, so retheme one token type without touching the rest:
45
38
 
@@ -3,6 +3,16 @@ export interface IconRule {
3
3
  prefix: string | RegExp;
4
4
  apply: (baseName: string, match: RegExpMatchArray | string, parts: ElementPart[]) => Element | string | null;
5
5
  }
6
+ /**
7
+ * Style suffixes on a registered icon (`checkCircle90r`), split, or null. Such a name is
8
+ * that icon styled, and it wins over a composition rule (#3295): `checkCircle90r` is
9
+ * `checkCircle` turned, not the `check` rule over `circle90r`. Both the DOM renderer and
10
+ * `iconSvg` resolve in that order by asking this.
11
+ */
12
+ export declare function suffixedIcon(name: string): {
13
+ baseName: string;
14
+ tokens: string[];
15
+ } | null;
6
16
  /** A registered icon, or style suffixes on one (`chevronRight50o`). */
7
17
  export declare function canResolve(name: string): boolean;
8
18
  /**
@@ -10,13 +10,21 @@ before the name is asked for.
10
10
  */
11
11
  import iconData from './icon-data.js';
12
12
  import { splitSuffixes } from './icon-suffixes.js';
13
+ /**
14
+ * Style suffixes on a registered icon (`checkCircle90r`), split, or null. Such a name is
15
+ * that icon styled, and it wins over a composition rule (#3295): `checkCircle90r` is
16
+ * `checkCircle` turned, not the `check` rule over `circle90r`. Both the DOM renderer and
17
+ * `iconSvg` resolve in that order by asking this.
18
+ */
19
+ export function suffixedIcon(name) {
20
+ const split = splitSuffixes(name);
21
+ return split != null && iconData[split.baseName]
22
+ ? split
23
+ : null;
24
+ }
13
25
  /** A registered icon, or style suffixes on one (`chevronRight50o`). */
14
26
  export function canResolve(name) {
15
- const data = iconData;
16
- if (data[name])
17
- return true;
18
- const split = splitSuffixes(name);
19
- return split != null && !!data[split.baseName];
27
+ return (!!iconData[name] || suffixedIcon(name) != null);
20
28
  }
21
29
  /**
22
30
  * `spinN<Icon>` animates, so it is drawn by the DOM renderer, which installs `apply` when
package/dist/icon-svg.js CHANGED
@@ -1,4 +1,4 @@
1
- /*{"parent":"Appendices","order":600}*/
1
+ /*{ "parent": "icons", "order": 20 }*/
2
2
  /*#
3
3
  # `iconSvg` — icon markup without a DOM
4
4
 
@@ -27,21 +27,17 @@ other style suffix (`check50o`), a redirect to a name that is not an icon, or a
27
27
  cycle. Render those with `icons.<name>()` where there is a DOM; the suffixes and rules are
28
28
  described on the [[icons]] page. Only a name that nothing would draw returns `undefined`.
29
29
 
30
- Deliberately a separate module from `icons.ts`: that one imports tosijs to build
31
- elements, so importing it drags in code that needs `HTMLElement` and throws in a build
32
- script — the exact context that wanted the markup in the first place. This module
33
- imports nothing but the data.
34
-
35
- Background: `defineIcons()` could write to the icon map but nothing could read it, and
36
- `icons.foo()` returns a DOM `SVGElement`. So a static-page generator, an ePub pass or a
37
- server-rendered template had no way to get markup except by parsing
38
- `src/icons/data/*.ts` out of the installed package — which is exactly what this project
39
- resorted to when putting its own logo on a generated page. There is nothing to protect:
40
- the map ships in the bundle either way.
30
+ It needs no DOM, so it works in a build script, a static-page generator, an ePub pass or a
31
+ server-rendered template, where `icons.<name>()` (which builds DOM elements) cannot run.
32
+ */
33
+ /*
34
+ Deliberately a separate module from `icons.ts`: that one imports tosijs to build elements, so
35
+ importing it drags in code that needs `HTMLElement` and throws in a build script, the exact
36
+ context that wanted the markup. This module imports nothing but the data and the grammars.
41
37
  */
42
38
  import iconData from './icon-data.js';
43
- import { splitSuffixes, suffixNumber } from './icon-suffixes.js';
44
- import { canResolve, ruleDraws } from './icon-rules.js';
39
+ import { suffixNumber } from './icon-suffixes.js';
40
+ import { canResolve, ruleDraws, suffixedIcon } from './icon-rules.js';
45
41
  const data = iconData;
46
42
  // Rotation and flip, the only suffixes the built-in redirects use (`chevronDown` is
47
43
  // `chevronRight90r`). Anything else needs the DOM renderer.
@@ -77,11 +73,9 @@ function resolve(asked, name, seen) {
77
73
  return resolve(asked, value, seen);
78
74
  }
79
75
  // The renderer's order for a name that is not registered: a stack, then suffixes on a
80
- // registered icon (they win over a rule, #3295), then a rule.
81
- const split = key.includes('$') ? null : splitSuffixes(key);
82
- if (split &&
83
- data[split.baseName] !== undefined &&
84
- split.tokens.every(isGeometry)) {
76
+ // registered icon (suffixedIcon, #3295), then a rule.
77
+ const split = key.includes('$') ? null : suffixedIcon(key);
78
+ if (split && split.tokens.every(isGeometry)) {
85
79
  return transformed(asked, resolve(asked, split.baseName, seen), split.tokens);
86
80
  }
87
81
  const what = key === stripEscape(asked) ? `"${key}"` : `it resolves to "${key}", which`;
package/dist/icons.js CHANGED
@@ -1,3 +1,4 @@
1
+ /*{ "parent": "Components" }*/
1
2
  /*#
2
3
  # icons
3
4
 
@@ -8,8 +9,8 @@
8
9
  </div>
9
10
 
10
11
  `icons` is a proxy that generates an `ElementCreator` for a given icon on demand,
11
- e.g. `icons.chevronDown()` produces an `<svg>` element containing a downward-pointing chevron
12
- icon with the class `icon-chevron-down`.
12
+ e.g. `icons.chevronDown()` produces an `<svg>` element (class `tosi-icon`) containing a
13
+ downward-pointing chevron.
13
14
 
14
15
  Most of the built-in icons come from [Feather](https://feathericons.com). If you want more
15
16
  icons in the same style, [lucide](https://lucide.dev) is a fork and expansion of Feather and a
@@ -200,12 +201,12 @@ test('filter input and prefix select exist', () => {
200
201
  })
201
202
  ```
202
203
 
203
- These icons are completely unstyled and can be colored using the css `fill` property. This will
204
- probably be broken out as a standalone library to allow the use of whatever icons you like
205
- (its source data is currently generated from an [icomoon](https://icomoon.com/app)
206
- `selection.json` file, but could just as easily be generated from a directory full of SVGs).
204
+ Icons take their colour from the text around them (`currentColor`). Override fill and stroke
205
+ with `--tosi-icon-fill` and `--tosi-icon-stroke`, or per icon with the `F` and `S` suffixes
206
+ (see [Icon modifier suffixes](#icon-modifier-suffixes)). The icon data is generated from a
207
+ directory of SVGs (see [Building icon data](#building-icon-data)).
207
208
 
208
- ### Need More Icons?
209
+ ## Need More Icons?
209
210
 
210
211
  [lucide](https://lucide.dev) is a fork and expansion of feather that maintains the style and
211
212
  adds a lot more options. Obviously it's completely compatible with this icon system and generously licensed. A
@@ -571,7 +572,7 @@ copies or substantial portions of the Software.
571
572
  import { elements, svgElements, Color, vars, varDefault, withAttributes, } from 'tosijs';
572
573
  import iconData from './icon-data.js';
573
574
  import { splitSuffixes, suffixNumber } from './icon-suffixes.js';
574
- import { iconRules, spinRule, matchRule, canResolve } from './icon-rules.js';
575
+ import { iconRules, spinRule, matchRule, canResolve, suffixedIcon, } from './icon-rules.js';
575
576
  export { iconRules } from './icon-rules.js';
576
577
  export const defineIcons = (newIcons) => {
577
578
  const data = iconData;
@@ -901,14 +902,10 @@ export function resolveIcon(prop, parts) {
901
902
  });
902
903
  return wrapIcon(prop, parts, base, ...overlays);
903
904
  }
904
- /*
905
- Style suffixes on a registered icon win over a composition rule (#3295): `checkCircle90r`
906
- is `checkCircle` turned, not the `check` rule over `circle90r`. A rule still applies
907
- when its suffixes are on an icon that only the rule makes (`spin90Loader50o`).
908
- */
905
+ // Suffixes on a registered icon win over a rule (suffixedIcon, #3295). A rule still
906
+ // applies when its suffixes are on an icon only the rule makes (`spin90Loader50o`).
909
907
  const parsed = parseStyleSuffixes(prop);
910
- const native = parsed != null && iconData[parsed.baseName];
911
- if (!native) {
908
+ if (!suffixedIcon(prop)) {
912
909
  const composed = composeIcon(prop, parts);
913
910
  if (composed)
914
911
  return composed;