tosijs-ui 1.6.10 → 1.6.12
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 +37 -0
- package/dist/doc-system/site/make-llms-txt.d.ts +29 -2
- package/dist/doc-system/site/make-llms-txt.js +59 -27
- package/dist/doc-system/site/orchestrator.js +17 -3
- package/dist/doc-system/site/output-guard.d.ts +9 -0
- package/dist/doc-system/site/output-guard.js +22 -0
- package/dist/float.js +33 -22
- package/dist/iife.js +89 -89
- package/dist/iife.js.map +6 -6
- package/dist/menu.js +32 -15
- package/dist/tooltip.js +14 -2
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/llms.txt +71 -49
- package/package.json +37 -1
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 1.6.12
|
|
4
|
+
|
|
5
|
+
Packaging improvements for independent, tree-shakeable consumption. All changes
|
|
6
|
+
are **additive — no breaking changes**; one minor behavioral edge case is noted
|
|
7
|
+
below.
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- **Subpath exports**, so an app can import only what it needs without the barrel
|
|
12
|
+
dragging in dev tools:
|
|
13
|
+
- curated: `tosijs-ui/icons`, `tosijs-ui/code-editor`, `tosijs-ui/live-example`,
|
|
14
|
+
`tosijs-ui/doc-browser`, `tosijs-ui/diff`, `tosijs-ui/theme`
|
|
15
|
+
- per-component wildcard: `import { tosiRating } from 'tosijs-ui/rating'`
|
|
16
|
+
(resolves `dist/rating.js`, registering just that element)
|
|
17
|
+
|
|
18
|
+
The full barrel `import 'tosijs-ui'` is unchanged and still registers every
|
|
19
|
+
`<tosi-*>` element.
|
|
20
|
+
|
|
21
|
+
### Changed
|
|
22
|
+
|
|
23
|
+
- **`menu`, `tooltip`, and `float` now inject their stylesheet and register their
|
|
24
|
+
global listeners on first use, not at import.** Importing these modules (or the
|
|
25
|
+
barrel) no longer has import-time side effects. This also fixes a latent bug
|
|
26
|
+
where the menu `keydown` handler attached to `document.body` at import time,
|
|
27
|
+
which may not exist yet.
|
|
28
|
+
|
|
29
|
+
### Possible edge case
|
|
30
|
+
|
|
31
|
+
If you imported `menu` / `tooltip` / `float` **purely for the side effect** — i.e.
|
|
32
|
+
you hand-author markup with the internal classes (`.tosi-menu`, `.tosi-tooltip`,
|
|
33
|
+
…) and relied on a bare `import` injecting the stylesheet, **without** ever
|
|
34
|
+
calling the API (`popMenu`, `popDropMenu`, `initTooltips`, `showTooltip`) or using
|
|
35
|
+
the `<tosi-*>` element — those styles now apply only once the API/element is first
|
|
36
|
+
used. Normal usage (the element creators, the `<tosi-*>` elements, and the
|
|
37
|
+
`popMenu`/`initTooltips` entry points) is unaffected.
|
|
@@ -1,11 +1,38 @@
|
|
|
1
|
+
/** A doc as it appears in the extracted corpus (docs.json). */
|
|
2
|
+
interface CorpusDoc {
|
|
3
|
+
title?: string;
|
|
4
|
+
filename: string;
|
|
5
|
+
text?: string;
|
|
6
|
+
description?: string;
|
|
7
|
+
hidden?: boolean;
|
|
8
|
+
}
|
|
9
|
+
interface LlmsEntry {
|
|
10
|
+
title: string;
|
|
11
|
+
description: string;
|
|
12
|
+
/** rendered-doc URL (corpus mode) or dist/*.js path (legacy scan) */
|
|
13
|
+
link: string;
|
|
14
|
+
}
|
|
1
15
|
export interface LlmsTxtMeta {
|
|
2
16
|
name?: string;
|
|
3
17
|
description?: string;
|
|
4
|
-
/** site origin, used for the Docs link */
|
|
18
|
+
/** site origin, used for the Docs link and to make page links absolute */
|
|
5
19
|
baseUrl?: string;
|
|
6
20
|
/** project links — `github` / `npm` (or any) become Source/npm links */
|
|
7
21
|
projectLinks?: Record<string, string | undefined>;
|
|
8
22
|
/** optional framing line(s) under the description */
|
|
9
23
|
tagline?: string;
|
|
10
24
|
}
|
|
11
|
-
|
|
25
|
+
/**
|
|
26
|
+
* Build entries from the extracted corpus — every doc that was actually
|
|
27
|
+
* extracted (`.md`, `.ts`/`.js`/`.css` doc comments, auto-created sections),
|
|
28
|
+
* linking to its rendered URL. This is what the build uses: it reflects the real
|
|
29
|
+
* docs and needs no `dist/` library output.
|
|
30
|
+
*/
|
|
31
|
+
export declare function entriesFromCorpus(corpus: CorpusDoc[], meta: LlmsTxtMeta): LlmsEntry[];
|
|
32
|
+
/**
|
|
33
|
+
* Write an `llms.txt` index. Pass the extracted `corpus` (the build does) to
|
|
34
|
+
* index every doc by its rendered URL; omit it to fall back to the legacy
|
|
35
|
+
* `src/*.ts`-with-`dist/*.js` scan.
|
|
36
|
+
*/
|
|
37
|
+
export declare function generateLlmsTxt(outputPath: string, meta?: LlmsTxtMeta, corpus?: CorpusDoc[]): void;
|
|
38
|
+
export {};
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import * as fs from 'fs';
|
|
2
2
|
import * as path from 'path';
|
|
3
|
+
import { buildSlugMap, pathForSlug } from '../routing';
|
|
3
4
|
const SRC = 'src';
|
|
4
5
|
const DIST = 'dist';
|
|
5
6
|
function extractTitle(text) {
|
|
@@ -33,19 +34,37 @@ function extractDescription(text) {
|
|
|
33
34
|
}
|
|
34
35
|
return '';
|
|
35
36
|
}
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
37
|
+
/**
|
|
38
|
+
* Build entries from the extracted corpus — every doc that was actually
|
|
39
|
+
* extracted (`.md`, `.ts`/`.js`/`.css` doc comments, auto-created sections),
|
|
40
|
+
* linking to its rendered URL. This is what the build uses: it reflects the real
|
|
41
|
+
* docs and needs no `dist/` library output.
|
|
42
|
+
*/
|
|
43
|
+
export function entriesFromCorpus(corpus, meta) {
|
|
44
|
+
const slugMap = buildSlugMap(corpus);
|
|
45
|
+
const base = (meta.baseUrl ?? '').replace(/\/$/, '');
|
|
46
|
+
return corpus
|
|
47
|
+
.filter((doc) => doc.title && !doc.hidden)
|
|
48
|
+
.map((doc) => ({
|
|
49
|
+
title: doc.title,
|
|
50
|
+
description: doc.description?.trim() || extractDescription(doc.text ?? ''),
|
|
51
|
+
link: base + pathForSlug(slugMap[doc.filename]),
|
|
52
|
+
}))
|
|
53
|
+
.sort((a, b) => a.title.localeCompare(b.title));
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Legacy fallback when no corpus is passed (e.g. running this file directly):
|
|
57
|
+
* scan `src/*.ts` for doc-comment blocks that have a built `dist/*.js` sibling.
|
|
58
|
+
*/
|
|
59
|
+
function entriesFromSrcScan() {
|
|
60
|
+
const entries = [];
|
|
61
|
+
let files = [];
|
|
41
62
|
try {
|
|
42
|
-
|
|
63
|
+
files = fs.readdirSync(SRC).filter((f) => f.endsWith('.ts'));
|
|
43
64
|
}
|
|
44
65
|
catch {
|
|
45
|
-
|
|
66
|
+
return entries;
|
|
46
67
|
}
|
|
47
|
-
const docs = [];
|
|
48
|
-
const files = fs.readdirSync(SRC).filter((f) => f.endsWith('.ts'));
|
|
49
68
|
for (const file of files) {
|
|
50
69
|
const content = fs.readFileSync(path.join(SRC, file), 'utf8');
|
|
51
70
|
const match = content.match(/\/\*#([\s\S]+?)\*\//);
|
|
@@ -53,21 +72,36 @@ export function generateLlmsTxt(outputPath, meta = {}) {
|
|
|
53
72
|
continue;
|
|
54
73
|
const markdown = match[1].trim();
|
|
55
74
|
const title = extractTitle(markdown);
|
|
56
|
-
const description = extractDescription(markdown);
|
|
57
75
|
if (!title)
|
|
58
76
|
continue;
|
|
59
77
|
const distFile = file.replace(/\.ts$/, '.js');
|
|
60
|
-
|
|
61
|
-
if (!fs.existsSync(distPath))
|
|
78
|
+
if (!fs.existsSync(path.join(DIST, distFile)))
|
|
62
79
|
continue;
|
|
63
|
-
|
|
80
|
+
entries.push({
|
|
64
81
|
title,
|
|
65
|
-
description,
|
|
66
|
-
|
|
67
|
-
distFile: `dist/${distFile}`,
|
|
82
|
+
description: extractDescription(markdown),
|
|
83
|
+
link: `dist/${distFile}`,
|
|
68
84
|
});
|
|
69
85
|
}
|
|
70
|
-
|
|
86
|
+
return entries.sort((a, b) => a.title.localeCompare(b.title));
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Write an `llms.txt` index. Pass the extracted `corpus` (the build does) to
|
|
90
|
+
* index every doc by its rendered URL; omit it to fall back to the legacy
|
|
91
|
+
* `src/*.ts`-with-`dist/*.js` scan.
|
|
92
|
+
*/
|
|
93
|
+
export function generateLlmsTxt(outputPath, meta = {}, corpus) {
|
|
94
|
+
// package.json supplies fallback name/version/description, but a doc site
|
|
95
|
+
// isn't guaranteed to have one (or it may be unreadable) — degrade gracefully
|
|
96
|
+
// rather than aborting the whole build.
|
|
97
|
+
let pkg = {};
|
|
98
|
+
try {
|
|
99
|
+
pkg = JSON.parse(fs.readFileSync('package.json', 'utf8'));
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
console.warn('llms.txt: no readable package.json — using config values only');
|
|
103
|
+
}
|
|
104
|
+
const entries = corpus ? entriesFromCorpus(corpus, meta) : entriesFromSrcScan();
|
|
71
105
|
const name = meta.name ?? pkg.name ?? '';
|
|
72
106
|
const description = meta.description ?? pkg.description ?? '';
|
|
73
107
|
const links = [];
|
|
@@ -89,24 +123,22 @@ export function generateLlmsTxt(outputPath, meta = {}) {
|
|
|
89
123
|
'',
|
|
90
124
|
'## Documentation',
|
|
91
125
|
'',
|
|
92
|
-
'
|
|
93
|
-
'embedded in its distributed JS file as inline comments. Read the',
|
|
94
|
-
'relevant dist/*.js file for complete docs, usage examples, and API.',
|
|
126
|
+
'Full documentation, with live code examples, is at the links below.',
|
|
95
127
|
'',
|
|
96
|
-
'##
|
|
128
|
+
'## Pages',
|
|
97
129
|
'',
|
|
98
130
|
];
|
|
99
|
-
for (const
|
|
100
|
-
lines.push(`- ${
|
|
101
|
-
if (
|
|
102
|
-
lines.push(` ${
|
|
131
|
+
for (const entry of entries) {
|
|
132
|
+
lines.push(`- ${entry.link} — ${entry.title}`);
|
|
133
|
+
if (entry.description) {
|
|
134
|
+
lines.push(` ${entry.description}`);
|
|
103
135
|
}
|
|
104
136
|
}
|
|
105
137
|
lines.push('');
|
|
106
138
|
fs.writeFileSync(outputPath, lines.join('\n'), 'utf8');
|
|
107
|
-
console.log(`llms.txt generated (${
|
|
139
|
+
console.log(`llms.txt generated (${entries.length} entries)`);
|
|
108
140
|
}
|
|
109
|
-
// Allow running directly
|
|
141
|
+
// Allow running directly (no corpus — uses the legacy src/*.ts scan)
|
|
110
142
|
if (import.meta.main) {
|
|
111
143
|
generateLlmsTxt('dist/llms.txt');
|
|
112
144
|
}
|
|
@@ -19,6 +19,7 @@ import { extractDocs } from './docs';
|
|
|
19
19
|
import { ensureSections } from './sections';
|
|
20
20
|
import { generateLlmsTxt } from './make-llms-txt';
|
|
21
21
|
import { generateSite } from './generate-site';
|
|
22
|
+
import { findOutputDirOverlap } from './output-guard';
|
|
22
23
|
// Module specifiers contain regex metacharacters (`/`, `.`, `@`, …), so escape
|
|
23
24
|
// before interpolating into the require-shim detector below.
|
|
24
25
|
const escapeRegExp = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
@@ -39,10 +40,20 @@ export async function buildSite(config) {
|
|
|
39
40
|
console.time('prebuild');
|
|
40
41
|
// Project-specific codegen (version stamp, icon data, …) before anything else.
|
|
41
42
|
await config.prebuild?.();
|
|
43
|
+
// Guard before the destructive `rm -rf`: if a docPath overlaps the output dir,
|
|
44
|
+
// wiping the output would delete the source docs we're about to extract.
|
|
45
|
+
const docPaths = config.docPaths ?? ['src', 'README.md'];
|
|
46
|
+
const overlap = findOutputDirOverlap(docPaths, config.outputDir ?? 'docs', PROJECT_ROOT);
|
|
47
|
+
if (overlap) {
|
|
48
|
+
throw new Error(`doc-site build: docPath "${overlap}" overlaps outputDir "${config.outputDir ?? 'docs'}". ` +
|
|
49
|
+
'buildSite() runs `rm -rf <outputDir>` before extracting docs, so this would ' +
|
|
50
|
+
'delete your source docs first (producing an empty site with no error). Move the ' +
|
|
51
|
+
"source docs out of the output dir, or set a different outputDir (e.g. outputDir: 'site').");
|
|
52
|
+
}
|
|
42
53
|
await $ `rm -rf ${PUBLIC}`.text();
|
|
43
54
|
await $ `mkdir ${PUBLIC}`.text();
|
|
44
55
|
const extract = () => extractDocs({
|
|
45
|
-
paths:
|
|
56
|
+
paths: docPaths,
|
|
46
57
|
// Skip the build's own output dir by path (not by the name 'docs', so a
|
|
47
58
|
// source dir like src/docs is still scanned).
|
|
48
59
|
ignore: ['node_modules', 'dist', 'build', PUBLIC],
|
|
@@ -139,8 +150,11 @@ export async function buildSite(config) {
|
|
|
139
150
|
console.log(`${scriptName}: ${(bundleFile.byteLength / 1024).toFixed(1)}kb (${(bundleGzip.length / 1024).toFixed(1)}kb gzip)`);
|
|
140
151
|
}
|
|
141
152
|
if (config.llmsTxt !== false) {
|
|
153
|
+
// Drive llms.txt from the extracted corpus (every doc, by rendered URL) — not
|
|
154
|
+
// a re-scan of src/*.ts — so it works regardless of doc source (.md, etc.)
|
|
155
|
+
// and whether the project emits a dist/ library.
|
|
156
|
+
const corpus = JSON.parse(await Bun.file(DOCS_JSON).text());
|
|
142
157
|
if (typeof config.llmsTxt === 'function') {
|
|
143
|
-
const corpus = JSON.parse(await Bun.file(DOCS_JSON).text());
|
|
144
158
|
await Bun.write('llms.txt', config.llmsTxt(corpus));
|
|
145
159
|
}
|
|
146
160
|
else {
|
|
@@ -149,7 +163,7 @@ export async function buildSite(config) {
|
|
|
149
163
|
description: config.description,
|
|
150
164
|
baseUrl: config.baseUrl,
|
|
151
165
|
projectLinks: config.projectLinks,
|
|
152
|
-
});
|
|
166
|
+
}, corpus);
|
|
153
167
|
}
|
|
154
168
|
// Also place it at the served web root so {baseUrl}/llms.txt resolves (the
|
|
155
169
|
// root copy stays for the npm package's `files`).
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Return the first `docPaths` entry that overlaps `outputDir` (the same
|
|
3
|
+
* directory, or either nested inside the other), or null if none overlap.
|
|
4
|
+
*
|
|
5
|
+
* `buildSite()` runs `rm -rf <outputDir>` before it extracts docs, so an overlap
|
|
6
|
+
* means the source docs get deleted *before* they're read — silently producing
|
|
7
|
+
* an empty site with no error. Callers throw an actionable message instead.
|
|
8
|
+
*/
|
|
9
|
+
export declare function findOutputDirOverlap(docPaths: string[], outputDir: string, root?: string): string | null;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import * as path from 'path';
|
|
2
|
+
/**
|
|
3
|
+
* Return the first `docPaths` entry that overlaps `outputDir` (the same
|
|
4
|
+
* directory, or either nested inside the other), or null if none overlap.
|
|
5
|
+
*
|
|
6
|
+
* `buildSite()` runs `rm -rf <outputDir>` before it extracts docs, so an overlap
|
|
7
|
+
* means the source docs get deleted *before* they're read — silently producing
|
|
8
|
+
* an empty site with no error. Callers throw an actionable message instead.
|
|
9
|
+
*/
|
|
10
|
+
export function findOutputDirOverlap(docPaths, outputDir, root = '.') {
|
|
11
|
+
// Compare as directory prefixes (trailing separator) so '/docs' doesn't
|
|
12
|
+
// falsely match '/docs-site', while genuine nesting ('/docs' vs '/docs/api')
|
|
13
|
+
// and exact matches still do.
|
|
14
|
+
const withSep = (p) => (p.endsWith(path.sep) ? p : p + path.sep);
|
|
15
|
+
const out = withSep(path.resolve(root, outputDir));
|
|
16
|
+
for (const docPath of docPaths) {
|
|
17
|
+
const resolved = withSep(path.resolve(root, docPath));
|
|
18
|
+
if (resolved.startsWith(out) || out.startsWith(resolved))
|
|
19
|
+
return docPath;
|
|
20
|
+
}
|
|
21
|
+
return null;
|
|
22
|
+
}
|
package/dist/float.js
CHANGED
|
@@ -131,6 +131,7 @@ export class TosiFloat extends WebComponent {
|
|
|
131
131
|
};
|
|
132
132
|
connectedCallback() {
|
|
133
133
|
super.connectedCallback();
|
|
134
|
+
ensureFloatListeners();
|
|
134
135
|
TosiFloat.floats.add(this);
|
|
135
136
|
const PASSIVE = { passive: true };
|
|
136
137
|
this.addEventListener('touchstart', this.reposition, PASSIVE);
|
|
@@ -147,27 +148,37 @@ export const XinFloat = TosiFloat;
|
|
|
147
148
|
export const tosiFloat = TosiFloat.elementCreator();
|
|
148
149
|
/** @deprecated Use tosiFloat instead */
|
|
149
150
|
export const xinFloat = tosiFloat;
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
}
|
|
158
|
-
});
|
|
159
|
-
}, { passive: true });
|
|
160
|
-
document.addEventListener('scroll', (event) => {
|
|
161
|
-
if (event.target instanceof HTMLElement &&
|
|
162
|
-
event.target.closest(TosiFloat.tagName)) {
|
|
151
|
+
// Register the global reposition/dismiss handlers on first float connecting,
|
|
152
|
+
// not at import (keeps the module side-effect-free for tree-shaking). They
|
|
153
|
+
// iterate TosiFloat.floats, so they're no-ops until a float exists — registered
|
|
154
|
+
// once, no teardown needed.
|
|
155
|
+
let floatListenersRegistered = false;
|
|
156
|
+
function ensureFloatListeners() {
|
|
157
|
+
if (floatListenersRegistered)
|
|
163
158
|
return;
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
float.
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
float.remove
|
|
159
|
+
floatListenersRegistered = true;
|
|
160
|
+
window.addEventListener('resize', () => {
|
|
161
|
+
Array.from(TosiFloat.floats).forEach((float) => {
|
|
162
|
+
if (float.remainOnResize === 'hide') {
|
|
163
|
+
float.hidden = true;
|
|
164
|
+
}
|
|
165
|
+
else if (float.remainOnResize === 'remove') {
|
|
166
|
+
float.remove();
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
}, { passive: true });
|
|
170
|
+
document.addEventListener('scroll', (event) => {
|
|
171
|
+
if (event.target instanceof HTMLElement &&
|
|
172
|
+
event.target.closest(TosiFloat.tagName)) {
|
|
173
|
+
return;
|
|
171
174
|
}
|
|
172
|
-
|
|
173
|
-
|
|
175
|
+
Array.from(TosiFloat.floats).forEach((float) => {
|
|
176
|
+
if (float.remainOnScroll === 'hide') {
|
|
177
|
+
float.hidden = true;
|
|
178
|
+
}
|
|
179
|
+
else if (float.remainOnScroll === 'remove') {
|
|
180
|
+
float.remove();
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
}, { passive: true, capture: true });
|
|
184
|
+
}
|