@colletdev/docs 0.2.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/README.md +60 -0
- package/angular.md +254 -0
- package/cli.mjs +300 -0
- package/components.md +2268 -0
- package/core.md +684 -0
- package/index.md +53 -0
- package/package.json +40 -0
- package/react.md +290 -0
- package/svelte.md +234 -0
- package/vue.md +195 -0
package/core.md
ADDED
|
@@ -0,0 +1,684 @@
|
|
|
1
|
+
# Collet Core — Framework-Agnostic Reference
|
|
2
|
+
|
|
3
|
+
Shared architecture, initialization, theming, CSS, SSR, and conventions
|
|
4
|
+
that apply to all Collet framework wrappers and vanilla Custom Element usage.
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Architecture
|
|
9
|
+
|
|
10
|
+
Framework wrappers (`@colletdev/react`, `@colletdev/vue`, `@colletdev/svelte`,
|
|
11
|
+
`@colletdev/angular`) are thin adapters that render the underlying `<cx-*>`
|
|
12
|
+
Custom Element. They handle:
|
|
13
|
+
|
|
14
|
+
- **Attribute serialization** -- objects/arrays go through `JSON.stringify`
|
|
15
|
+
- **Event bridging** -- framework event props attach listeners for `cx-{event}` CustomEvents
|
|
16
|
+
- **Slot projection** -- named slots via `<div slot="name">` (display strategy varies by framework)
|
|
17
|
+
- **Typed imperative handles** -- expose typed methods like `.open()`, `.close()`, `.focus()`
|
|
18
|
+
|
|
19
|
+
### Package structure
|
|
20
|
+
|
|
21
|
+
```
|
|
22
|
+
packages/
|
|
23
|
+
core/ <- Custom Elements, runtime, WASM, CSS
|
|
24
|
+
src/
|
|
25
|
+
elements/ <- Per-component Custom Element definitions
|
|
26
|
+
runtime.js <- Shadow DOM, adopted stylesheets, __cx namespace
|
|
27
|
+
styles.js <- Inlined CSS strings (tokens + utilities)
|
|
28
|
+
index.js <- init(), element registration
|
|
29
|
+
server.js <- createRenderer() for SSR/DSD
|
|
30
|
+
markdown.js <- renderMarkdown(), renderMarkdownSync()
|
|
31
|
+
dist/
|
|
32
|
+
tokens.css <- Document-level CSS vars, themes, fonts
|
|
33
|
+
tokens-shadow.css <- Shadow DOM adopted stylesheet (keyframes, motion)
|
|
34
|
+
cx-utilities.css <- Tailwind utility classes for shadow DOM
|
|
35
|
+
syntax.css <- Code viewer syntax highlighting theme
|
|
36
|
+
react/
|
|
37
|
+
generated/ <- Auto-generated React wrappers (DO NOT EDIT)
|
|
38
|
+
index.ts <- Re-exports all components + hooks
|
|
39
|
+
vue/
|
|
40
|
+
src/ <- Auto-generated Vue 3.3+ wrappers (Composition API)
|
|
41
|
+
dist/ <- Compiled .js + .d.ts files
|
|
42
|
+
svelte/
|
|
43
|
+
src/ <- Auto-generated Svelte 5 runes-first wrappers
|
|
44
|
+
angular/
|
|
45
|
+
src/ <- Auto-generated Angular 16+ standalone components
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### Regeneration
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
bash scripts/build-packages.sh
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
This runs: `gen_tokens` + `gen_shadow_tokens` -> `wasm-pack` -> Tailwind extract -> `inline-css.mjs` -> `generate-elements.mjs` -> `generate-react.mjs` -> `generate-vue.mjs` -> `generate-svelte.mjs` -> `generate-angular.mjs` -> `generate-skill-docs.mjs`
|
|
55
|
+
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
## CSS Architecture (Build-Time Split)
|
|
59
|
+
|
|
60
|
+
The build produces **two** token CSS files instead of one:
|
|
61
|
+
|
|
62
|
+
| File | Scope | Contents |
|
|
63
|
+
|------|-------|----------|
|
|
64
|
+
| `tokens.css` | Document `<head>` | CSS vars, fonts, themes, floating rules, view transitions |
|
|
65
|
+
| `tokens-shadow.css` | Shadow DOM adopted stylesheet | Keyframes, component motion, slider/scrollbar/texture/prose |
|
|
66
|
+
|
|
67
|
+
`init()` injects `tokens.css` into `<head>` (CSS variables inherit into Shadow DOM).
|
|
68
|
+
The shadow subset is adopted directly -- no runtime CSS parsing.
|
|
69
|
+
|
|
70
|
+
```
|
|
71
|
+
Document <head>
|
|
72
|
+
tokens.css <- CSS vars (--color-*, --spacing-*, --duration-*, etc.)
|
|
73
|
+
Fonts, themes, floating panel rules, view transitions
|
|
74
|
+
|
|
75
|
+
Shadow DOM (per component)
|
|
76
|
+
cx-utilities.css <- Tailwind utility classes (adopted stylesheet)
|
|
77
|
+
tokens-shadow.css <- Keyframes + component motion (adopted stylesheet)
|
|
78
|
+
Component HTML <- Semantic HTML with Tailwind classes
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
**Previous approach (removed):** `stripTokensForShadowDom()` was a runtime
|
|
82
|
+
regex/brace-balanced parser that tried to filter a single monolithic CSS blob.
|
|
83
|
+
It broke across three patch versions. The build-time split eliminates this
|
|
84
|
+
entire class of regressions.
|
|
85
|
+
|
|
86
|
+
---
|
|
87
|
+
|
|
88
|
+
## Initialization & Tree-Shaking
|
|
89
|
+
|
|
90
|
+
The `init()` function from `@colletdev/core` registers Custom Elements and loads
|
|
91
|
+
behavior JS. It supports selective component registration so bundlers (Vite,
|
|
92
|
+
Webpack, Rollup) can tree-shake unused behavior modules via dynamic `import()`.
|
|
93
|
+
|
|
94
|
+
### Usage
|
|
95
|
+
|
|
96
|
+
```js
|
|
97
|
+
import { init } from '@colletdev/core';
|
|
98
|
+
|
|
99
|
+
// Zero-config -- all 48 components (recommended for prototyping)
|
|
100
|
+
await init();
|
|
101
|
+
|
|
102
|
+
// Selective -- only 3 components downloaded (recommended for production)
|
|
103
|
+
await init({ components: ['button', 'text-input', 'select'] });
|
|
104
|
+
|
|
105
|
+
// Lazy -- WASM loads in background, no first-paint blocking
|
|
106
|
+
await init({ lazy: true });
|
|
107
|
+
|
|
108
|
+
// Lazy + selective -- fastest possible first paint
|
|
109
|
+
await init({ lazy: true, components: ['button', 'card'] });
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
### Options
|
|
113
|
+
|
|
114
|
+
| Option | Type | Default | Description |
|
|
115
|
+
|--------|------|---------|-------------|
|
|
116
|
+
| `components` | `string[]` | all | Component names to register. Only these components' behavior JS files are downloaded. |
|
|
117
|
+
| `lazy` | `boolean` | `false` | When `true`, WASM loads in the background after first paint instead of blocking. |
|
|
118
|
+
|
|
119
|
+
### How it works
|
|
120
|
+
|
|
121
|
+
**Lazy by design:** The heavy assets (WASM binary ~893 KB, CSS strings ~173 KB,
|
|
122
|
+
WASM glue ~21 KB) are NOT loaded at module level. They're fetched via dynamic
|
|
123
|
+
`import()` inside `init()`, which means `import { init } from '@colletdev/core'`
|
|
124
|
+
costs only ~48 KB. Bundlers (Vite, Webpack, Rollup) automatically code-split
|
|
125
|
+
the WASM and CSS into separate async chunks.
|
|
126
|
+
|
|
127
|
+
Each component's behavior JS (`static/_behaviors/*.js`) is also loaded via
|
|
128
|
+
dynamic `import()`. When `components` is provided, only the listed components'
|
|
129
|
+
chunks are fetched -- the rest never leave the server.
|
|
130
|
+
|
|
131
|
+
### Bundle impact
|
|
132
|
+
|
|
133
|
+
| Asset | Raw | Gzip | Brotli | When loaded |
|
|
134
|
+
|-------|-----|------|--------|-------------|
|
|
135
|
+
| Entry point (index.js + runtime.js) | 48 KB | ~10 KB | ~8 KB | `import { init }` |
|
|
136
|
+
| CSS strings (styles.js) | 173 KB | 33 KB | ~28 KB | `init()` call |
|
|
137
|
+
| WASM glue (wasm_api.js) | 21 KB | ~5 KB | ~4 KB | `init()` call |
|
|
138
|
+
| WASM binary (wasm_api_bg.wasm) | 893 KB | 318 KB | 232 KB | `init()` call |
|
|
139
|
+
| Per-component behavior JS | 1-20 KB each | varies | varies | Component registration |
|
|
140
|
+
|
|
141
|
+
**Total first-paint cost with brotli:** ~8 KB (just the import).
|
|
142
|
+
**Total after init():** ~272 KB brotli (all assets loaded).
|
|
143
|
+
**WASM is cached:** After first visit, the browser serves it from disk cache.
|
|
144
|
+
|
|
145
|
+
**Recommendation:** Use `components` in production builds. Use bare `init()`
|
|
146
|
+
during prototyping when the component set is still changing.
|
|
147
|
+
|
|
148
|
+
---
|
|
149
|
+
|
|
150
|
+
## Custom Theming with Collet Tokens
|
|
151
|
+
|
|
152
|
+
Collet components use CSS custom properties for all visual values. Override them
|
|
153
|
+
with a custom `tokens.css` generated by [Collet Tokens](https://github.com/Danrozen87/collet-tokens):
|
|
154
|
+
|
|
155
|
+
```bash
|
|
156
|
+
# Install the token compiler
|
|
157
|
+
cargo install collet-tokens-cli
|
|
158
|
+
|
|
159
|
+
# Create a token file with your brand
|
|
160
|
+
collet-tokens init
|
|
161
|
+
# Edit tokens.yaml with your colors, fonts, spacing
|
|
162
|
+
|
|
163
|
+
# Generate CSS
|
|
164
|
+
collet-tokens build --input tokens.yaml --outdir public/
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
Then load your `tokens.css` after Collet's default:
|
|
168
|
+
|
|
169
|
+
```html
|
|
170
|
+
<!-- Your custom tokens override Collet defaults via CSS cascade -->
|
|
171
|
+
<link rel="stylesheet" href="/@colletdev/tokens.css"> <!-- Collet defaults -->
|
|
172
|
+
<link rel="stylesheet" href="/public/tokens.css"> <!-- Your brand overrides -->
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
Or in JS:
|
|
176
|
+
|
|
177
|
+
```js
|
|
178
|
+
import '@colletdev/core/dist/tokens.css'; // Collet defaults
|
|
179
|
+
import './public/tokens.css'; // Your brand overrides
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
Every Collet component instantly reflects your brand. No code changes needed.
|
|
183
|
+
|
|
184
|
+
**White-labeling:** Load different `tokens.css` per tenant for multi-brand apps.
|
|
185
|
+
See [Theme Registry docs](https://github.com/Danrozen87/collet-tokens/blob/main/docs/theme-registry.md).
|
|
186
|
+
|
|
187
|
+
---
|
|
188
|
+
|
|
189
|
+
## Server Rendering & Declarative Shadow DOM
|
|
190
|
+
|
|
191
|
+
Collet can pre-render components to HTML at build time or on the server.
|
|
192
|
+
The browser paints them **instantly** -- no JavaScript needed for first render.
|
|
193
|
+
When the Custom Element class registers, the element "upgrades" and becomes
|
|
194
|
+
interactive. No hydration step. This is resumability via native browser APIs.
|
|
195
|
+
|
|
196
|
+
### Server Renderer (Node.js)
|
|
197
|
+
|
|
198
|
+
```js
|
|
199
|
+
import { createRenderer } from '@colletdev/core/server';
|
|
200
|
+
|
|
201
|
+
const cx = await createRenderer();
|
|
202
|
+
|
|
203
|
+
// Full DSD element -- browser renders this without JS
|
|
204
|
+
const html = cx.renderDSD('button', {
|
|
205
|
+
label: 'Click me',
|
|
206
|
+
variant: 'filled',
|
|
207
|
+
id: 'btn-1',
|
|
208
|
+
});
|
|
209
|
+
// -> '<cx-button variant="filled" ...>
|
|
210
|
+
// <template shadowrootmode="open">
|
|
211
|
+
// <link rel="stylesheet" href="/@colletdev/cx-utilities.css">
|
|
212
|
+
// <link rel="stylesheet" href="/@colletdev/tokens-shadow.css">
|
|
213
|
+
// <button class="inline-flex items-center ...">Click me</button>
|
|
214
|
+
// </template>
|
|
215
|
+
// </cx-button>'
|
|
216
|
+
|
|
217
|
+
// Just the inner HTML (for custom injection)
|
|
218
|
+
const inner = cx.renderToString('button', { label: 'Click', id: 'btn-2' });
|
|
219
|
+
// -> { html: '<button class="...">Click</button>', sprites: '', a11y: {...} }
|
|
220
|
+
|
|
221
|
+
// Just the <template> fragment (for existing host elements)
|
|
222
|
+
const frag = cx.renderDSDFragment('badge', { label: 'New', id: 'b-1' });
|
|
223
|
+
// -> '<template shadowrootmode="open">...</template>'
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
### Renderer Options
|
|
227
|
+
|
|
228
|
+
```js
|
|
229
|
+
const cx = await createRenderer({
|
|
230
|
+
stylesUrl: '/assets/cx-utilities.css', // URL for CSS <link> in DSD templates
|
|
231
|
+
motionUrl: '/assets/tokens-shadow.css', // URL for motion CSS <link>
|
|
232
|
+
inlineStyles: true, // Inline CSS instead of <link> (SSG)
|
|
233
|
+
});
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
### Vite Setup
|
|
237
|
+
|
|
238
|
+
The Vite plugin handles WASM MIME types, binary copying, preload hints, and
|
|
239
|
+
dependency pre-bundling automatically:
|
|
240
|
+
|
|
241
|
+
```js
|
|
242
|
+
import { colletPlugin } from '@colletdev/core/vite-plugin';
|
|
243
|
+
|
|
244
|
+
export default defineConfig({
|
|
245
|
+
plugins: [colletPlugin({
|
|
246
|
+
prerender: true, // Pre-render <cx-*> in index.html (optional)
|
|
247
|
+
preload: true, // Inject WASM preload hint (default)
|
|
248
|
+
})],
|
|
249
|
+
});
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
The plugin automatically:
|
|
253
|
+
- Copies `wasm_api_bg.wasm` to `public/` during dev
|
|
254
|
+
- Serves `.wasm` files with correct MIME type (`application/wasm`)
|
|
255
|
+
- Emits the WASM binary in production builds
|
|
256
|
+
- Configures `optimizeDeps` to pre-bundle `@colletdev/core` (prevents full-page reloads during dev)
|
|
257
|
+
- Excludes the WASM glue module from pre-bundling (must stay as native ESM)
|
|
258
|
+
|
|
259
|
+
**Without the plugin**, manually configure:
|
|
260
|
+
|
|
261
|
+
```js
|
|
262
|
+
export default defineConfig({
|
|
263
|
+
optimizeDeps: {
|
|
264
|
+
include: ['@colletdev/core'],
|
|
265
|
+
exclude: ['@colletdev/core/wasm/wasm_api.js'],
|
|
266
|
+
},
|
|
267
|
+
});
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
**For large apps**, pass an explicit component list to `init()` for faster startup
|
|
271
|
+
(avoids O(n) DOM scan):
|
|
272
|
+
|
|
273
|
+
```js
|
|
274
|
+
await init({ components: ['button', 'dialog', 'text-input', 'select'] });
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
### Framework SSR Integration
|
|
278
|
+
|
|
279
|
+
| Framework | Approach |
|
|
280
|
+
|-----------|----------|
|
|
281
|
+
| **Next.js (App Router)** | Use `createRenderer()` in a Server Component or API route |
|
|
282
|
+
| **Nuxt** | Use `createRenderer()` in a server plugin or composable |
|
|
283
|
+
| **SvelteKit** | Use `createRenderer()` in a server `load` function |
|
|
284
|
+
| **Angular Universal** | Use `createRenderer()` in a transfer state resolver |
|
|
285
|
+
| **Remix** | Use `createRenderer()` in the loader, inject DSD in the template |
|
|
286
|
+
| **Astro** | Use `createRenderer()` in `.astro` components (SSG or SSR) |
|
|
287
|
+
| **Plain HTML** | Use `colletPlugin({ prerender: true })` in Vite |
|
|
288
|
+
| **SPA (no SSR)** | Use `init({ lazy: true })` -- DSD not applicable |
|
|
289
|
+
|
|
290
|
+
### How DSD Upgrade Works
|
|
291
|
+
|
|
292
|
+
1. Server/build produces `<cx-button><template shadowrootmode="open">...</template></cx-button>`
|
|
293
|
+
2. Browser creates the shadow root immediately from the DSD template (spec behavior)
|
|
294
|
+
3. Component is **visible and styled** -- zero JavaScript needed
|
|
295
|
+
4. When `init()` runs, `customElements.define()` triggers element upgrade
|
|
296
|
+
5. The existing shadow root is reused (not recreated) -- `this.shadowRoot` already exists
|
|
297
|
+
6. Event handlers attach, adopted stylesheets replace `<link>` tags -- zero visual flash
|
|
298
|
+
|
|
299
|
+
---
|
|
300
|
+
|
|
301
|
+
## Event Convention
|
|
302
|
+
|
|
303
|
+
All Collet components emit events with the `cx-` prefix as `CustomEvent` instances.
|
|
304
|
+
|
|
305
|
+
### Naming
|
|
306
|
+
|
|
307
|
+
```
|
|
308
|
+
cx-{action}
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
Examples: `cx-close`, `cx-navigate`, `cx-action`, `cx-input`, `cx-change`,
|
|
312
|
+
`cx-sort`, `cx-select`, `cx-page`, `cx-expand`, `cx-dismiss`, `cx-stream-end`
|
|
313
|
+
|
|
314
|
+
### Listening (Vanilla JS)
|
|
315
|
+
|
|
316
|
+
```js
|
|
317
|
+
const el = document.querySelector('cx-dialog');
|
|
318
|
+
|
|
319
|
+
el.addEventListener('cx-close', (e) => {
|
|
320
|
+
console.log('Dialog closed', e.detail);
|
|
321
|
+
});
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
### Detail Types
|
|
325
|
+
|
|
326
|
+
Every event carries a typed `detail` payload:
|
|
327
|
+
|
|
328
|
+
| Event | Detail Type | Fields |
|
|
329
|
+
|-------|-------------|--------|
|
|
330
|
+
| `cx-close` | `CloseDetail` | `{}` |
|
|
331
|
+
| `cx-navigate` | `NavigateDetail` | `{ label, href }` |
|
|
332
|
+
| `cx-action` | `MenuActionDetail` | `{ id, label }` |
|
|
333
|
+
| `cx-input` | `InputDetail` | `{ value }` |
|
|
334
|
+
| `cx-change` | varies | Component-specific |
|
|
335
|
+
| `cx-sort` | `SortDetail` | `{ column, direction }` |
|
|
336
|
+
| `cx-select` | `SelectDetail` | `{ value, label }` |
|
|
337
|
+
| `cx-page` | `PageDetail` | `{ page }` |
|
|
338
|
+
| `cx-expand` | `TableExpandDetail` | `{ rowId, expanded }` |
|
|
339
|
+
| `cx-dismiss` | `DismissDetail` | `{}` |
|
|
340
|
+
| `cx-focus` | `FocusDetail` | `{}` |
|
|
341
|
+
| `cx-blur` | `FocusDetail` | `{}` |
|
|
342
|
+
| `cx-keydown` | `KeyboardDetail` | `{ key, code, ... }` |
|
|
343
|
+
| `cx-click` | `ClickDetail` | `{}` |
|
|
344
|
+
| `cx-stream-end` | `{}` | Streaming complete |
|
|
345
|
+
|
|
346
|
+
### Framework wrapper mapping
|
|
347
|
+
|
|
348
|
+
Framework wrappers map these to idiomatic event props:
|
|
349
|
+
|
|
350
|
+
- **React:** `onClose`, `onNavigate`, `onAction`, `onInput`, `onChange`, etc.
|
|
351
|
+
- **Vue:** `@close`, `@navigate`, `@action`, `@input`, `@change`, etc.
|
|
352
|
+
- **Svelte:** `onclose`, `onnavigate`, `onaction`, `oninput`, `onchange`, etc.
|
|
353
|
+
- **Angular:** `(cxClose)`, `(cxNavigate)`, `(cxAction)`, `(cxInput)`, `(cxChange)`, etc.
|
|
354
|
+
|
|
355
|
+
---
|
|
356
|
+
|
|
357
|
+
## Slot Convention
|
|
358
|
+
|
|
359
|
+
Collet uses the web standard `<slot>` mechanism via Shadow DOM.
|
|
360
|
+
|
|
361
|
+
### Default slot
|
|
362
|
+
|
|
363
|
+
The default slot receives children:
|
|
364
|
+
|
|
365
|
+
```html
|
|
366
|
+
<cx-card>
|
|
367
|
+
<p>This goes into the default slot</p>
|
|
368
|
+
</cx-card>
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
### Named slots
|
|
372
|
+
|
|
373
|
+
Named slots target specific content areas:
|
|
374
|
+
|
|
375
|
+
```html
|
|
376
|
+
<cx-card>
|
|
377
|
+
<div slot="header"><h3>Card Title</h3></div>
|
|
378
|
+
<p>Default slot content</p>
|
|
379
|
+
<div slot="footer"><button>Save</button></div>
|
|
380
|
+
</cx-card>
|
|
381
|
+
|
|
382
|
+
<cx-dialog title="Confirm">
|
|
383
|
+
<p>Are you sure?</p>
|
|
384
|
+
<div slot="footer"><button>OK</button></div>
|
|
385
|
+
</cx-dialog>
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
### Common named slots
|
|
389
|
+
|
|
390
|
+
| Slot | Used by | Purpose |
|
|
391
|
+
|------|---------|---------|
|
|
392
|
+
| `header` | Card, Dialog, Drawer, Sidebar | Top section content |
|
|
393
|
+
| `footer` | Card, Dialog, Drawer, Sidebar | Bottom section content |
|
|
394
|
+
| `actions` | Alert | Action buttons area |
|
|
395
|
+
| `trigger` | Tooltip, Popover | Element that triggers the floating panel |
|
|
396
|
+
|
|
397
|
+
### Framework wrapper slot projection
|
|
398
|
+
|
|
399
|
+
Each framework wrapper maps slot children to the correct `slot` attribute:
|
|
400
|
+
|
|
401
|
+
- **Vanilla JS / @colletdev/core:** Use `slot="name"` attribute directly
|
|
402
|
+
- **React:** Pass as named props (e.g., `header={<h3>Title</h3>}`), wrapper adds `<div slot="name" style={{display:'contents'}}>`
|
|
403
|
+
- **Vue:** Use `<template v-slot:header>` or `<template #header>`
|
|
404
|
+
- **Svelte:** Use `{#snippet header()}` (Svelte 5) or `<div slot="header">`
|
|
405
|
+
- **Angular:** Use `<ng-container slot="header">` or `ngProjectAs`
|
|
406
|
+
|
|
407
|
+
---
|
|
408
|
+
|
|
409
|
+
## Form Integration
|
|
410
|
+
|
|
411
|
+
Form-associated Collet components (`TextInput`, `Select`, `Checkbox`, `Switch`,
|
|
412
|
+
`RadioGroup`, `DatePicker`, `Slider`) participate in native `<form>` submission
|
|
413
|
+
through the `ElementInternals` API.
|
|
414
|
+
|
|
415
|
+
### How it works
|
|
416
|
+
|
|
417
|
+
```html
|
|
418
|
+
<form id="signup">
|
|
419
|
+
<cx-text-input name="email" label="Email" required></cx-text-input>
|
|
420
|
+
<cx-select name="role" label="Role" items='[...]'></cx-select>
|
|
421
|
+
<cx-checkbox name="terms" label="Accept terms" required></cx-checkbox>
|
|
422
|
+
<button type="submit">Submit</button>
|
|
423
|
+
</form>
|
|
424
|
+
|
|
425
|
+
<script>
|
|
426
|
+
document.getElementById('signup').addEventListener('submit', (e) => {
|
|
427
|
+
e.preventDefault();
|
|
428
|
+
const data = new FormData(e.target);
|
|
429
|
+
console.log(data.get('email'), data.get('role'), data.get('terms'));
|
|
430
|
+
});
|
|
431
|
+
</script>
|
|
432
|
+
```
|
|
433
|
+
|
|
434
|
+
Each form component:
|
|
435
|
+
- Implements `static formAssociated = true`
|
|
436
|
+
- Uses `ElementInternals.setFormValue()` to sync the current value
|
|
437
|
+
- Participates in `formdata`, `reset`, and `submit` events
|
|
438
|
+
- Reports validity via `ElementInternals.setValidity()`
|
|
439
|
+
|
|
440
|
+
---
|
|
441
|
+
|
|
442
|
+
## DOM Collision Handling
|
|
443
|
+
|
|
444
|
+
Some HTML attributes (`title`, `width`, `loading`, `name`, `value`) collide
|
|
445
|
+
with Custom Element attribute names. Collet handles this by routing colliding
|
|
446
|
+
attributes through explicit DOM operations rather than relying on the
|
|
447
|
+
framework's default attribute/property sync.
|
|
448
|
+
|
|
449
|
+
### Affected attributes
|
|
450
|
+
|
|
451
|
+
| Attribute | Components | Issue |
|
|
452
|
+
|-----------|-----------|-------|
|
|
453
|
+
| `title` | Dialog, Drawer, Card | Browser shows native tooltip |
|
|
454
|
+
| `name` | TextInput, Select, RadioGroup | React 19 property-first sets wrong target |
|
|
455
|
+
| `value` | TextInput, ChatInput, Slider | React 19 property-first sets wrong target |
|
|
456
|
+
| `loading` | Table | Conflicts with native `loading` attribute |
|
|
457
|
+
|
|
458
|
+
### How collisions are resolved
|
|
459
|
+
|
|
460
|
+
Framework wrappers use effect-based routing: the colliding prop is applied via
|
|
461
|
+
`setAttribute()` in a post-render effect rather than as a JSX attribute. This
|
|
462
|
+
ensures the value reaches the Custom Element's `attributeChangedCallback`
|
|
463
|
+
correctly regardless of framework property-setting behavior.
|
|
464
|
+
|
|
465
|
+
---
|
|
466
|
+
|
|
467
|
+
## Floating Components in Shadow DOM
|
|
468
|
+
|
|
469
|
+
Components with floating panels (Select, Autocomplete, Menu, Popover,
|
|
470
|
+
DatePicker, SpeedDial, SplitButton, ProfileMenu) manage their own visibility
|
|
471
|
+
inside Shadow DOM.
|
|
472
|
+
|
|
473
|
+
**Key architectural detail:** The SSR gallery uses `[data-floating]` CSS rules
|
|
474
|
+
from `tokens.css` to control floating panel visibility. These rules are
|
|
475
|
+
**stripped** from the adopted stylesheet in Shadow DOM because they would make
|
|
476
|
+
panels permanently invisible inside Custom Elements.
|
|
477
|
+
|
|
478
|
+
Instead, each Custom Element's `#open()` / `#close()` methods control visibility
|
|
479
|
+
directly via JS:
|
|
480
|
+
- `classList.remove('hidden')` / `classList.add('hidden')`
|
|
481
|
+
- `style.display = 'block'` / `style.display = 'none'`
|
|
482
|
+
- `style.pointerEvents = 'auto'` / `style.pointerEvents = ''`
|
|
483
|
+
- `style.opacity = '1'` / `style.opacity = ''`
|
|
484
|
+
|
|
485
|
+
### Fixed positioning (escape scroll container clipping)
|
|
486
|
+
|
|
487
|
+
All floating panels use `position: fixed` with viewport coordinates calculated
|
|
488
|
+
from `trigger.getBoundingClientRect()`. This ensures panels escape `overflow:
|
|
489
|
+
auto/scroll/hidden` on ancestor elements (e.g. when a Select or Popover is
|
|
490
|
+
inside `<cx-scrollbar>` or any scroll container).
|
|
491
|
+
|
|
492
|
+
The base class `CxElement` provides two shared utilities:
|
|
493
|
+
|
|
494
|
+
```js
|
|
495
|
+
// In #open() — positions panel with fixed coordinates + above/below flip logic
|
|
496
|
+
this._positionFloatingFixed(trigger, panel, { matchWidth: true, gap: 4 });
|
|
497
|
+
|
|
498
|
+
// In #close() — resets all inline position styles
|
|
499
|
+
this._resetFloatingFixed(panel);
|
|
500
|
+
```
|
|
501
|
+
|
|
502
|
+
Tooltip uses a different approach: the `tooltip.js` behavior module creates a
|
|
503
|
+
single shared tooltip element on `document.body` (fully outside all scroll
|
|
504
|
+
containers). The `<cx-tooltip>` CE connects its shadow DOM wrapper to this
|
|
505
|
+
shared floating tooltip via `__cx._behaviors.tooltip.init(wrapper)`.
|
|
506
|
+
|
|
507
|
+
**When writing new Custom Elements with floating panels:** Never use `position:
|
|
508
|
+
absolute` -- it clips inside scroll containers. Always use
|
|
509
|
+
`_positionFloatingFixed()` or portal to `document.body`.
|
|
510
|
+
|
|
511
|
+
### Shadow DOM containment rule (CRITICAL)
|
|
512
|
+
|
|
513
|
+
`Node.contains()` does NOT cross shadow DOM boundaries. When checking if focus
|
|
514
|
+
or a click target is "inside" the component, you MUST check BOTH trees:
|
|
515
|
+
|
|
516
|
+
```js
|
|
517
|
+
// WRONG -- misses elements inside shadow root
|
|
518
|
+
if (!this.contains(active)) { this.#close(); }
|
|
519
|
+
|
|
520
|
+
// CORRECT -- checks light DOM AND shadow DOM
|
|
521
|
+
if (!this.contains(active) && !this._shadow.contains(active) && active !== this) {
|
|
522
|
+
this.#close();
|
|
523
|
+
}
|
|
524
|
+
```
|
|
525
|
+
|
|
526
|
+
This applies to:
|
|
527
|
+
- **focusout handlers** -- `activeElement` after focus leaves
|
|
528
|
+
- **outside click handlers** -- `e.target` from document mousedown
|
|
529
|
+
- **relatedTarget checks** -- `e.relatedTarget` in focusin/focusout
|
|
530
|
+
|
|
531
|
+
Every Custom Element with a close-on-blur or close-on-outside-click pattern
|
|
532
|
+
must use the dual check. Without it, the focusout fires and closes the panel
|
|
533
|
+
before the click event can register on the shadow DOM option.
|
|
534
|
+
|
|
535
|
+
---
|
|
536
|
+
|
|
537
|
+
## Markdown Rendering
|
|
538
|
+
|
|
539
|
+
Collet provides GFM markdown rendering via WASM with compile-time XSS safety.
|
|
540
|
+
No runtime HTML sanitizer -- raw HTML in source is escaped by the Rust type system.
|
|
541
|
+
|
|
542
|
+
### Static Rendering (Vanilla JS)
|
|
543
|
+
|
|
544
|
+
```js
|
|
545
|
+
import { renderMarkdown, renderMarkdownSync } from '@colletdev/core/markdown';
|
|
546
|
+
|
|
547
|
+
// Async -- waits for WASM if needed
|
|
548
|
+
const html = await renderMarkdown('**Hello** world');
|
|
549
|
+
// -> '<div class="cx-prose"><p><strong>Hello</strong> world</p>\n</div>'
|
|
550
|
+
|
|
551
|
+
// Sync -- returns '' if WASM not loaded yet
|
|
552
|
+
const html2 = renderMarkdownSync('# Heading');
|
|
553
|
+
```
|
|
554
|
+
|
|
555
|
+
### Streaming Rendering (SSE/WebSocket)
|
|
556
|
+
|
|
557
|
+
For AI chat interfaces with token-by-token streaming, use the `MessagePart`
|
|
558
|
+
Custom Element directly:
|
|
559
|
+
|
|
560
|
+
```js
|
|
561
|
+
const part = document.querySelector('cx-message-part');
|
|
562
|
+
part.startStream();
|
|
563
|
+
|
|
564
|
+
const source = new EventSource('/api/chat');
|
|
565
|
+
source.onmessage = (e) => part.appendTokens(e.data);
|
|
566
|
+
source.addEventListener('done', () => {
|
|
567
|
+
source.close();
|
|
568
|
+
part.endStream(); // final WASM sanitization pass
|
|
569
|
+
});
|
|
570
|
+
```
|
|
571
|
+
|
|
572
|
+
### Markdown API Reference
|
|
573
|
+
|
|
574
|
+
| Export | Type | Description |
|
|
575
|
+
|--------|------|-------------|
|
|
576
|
+
| `renderMarkdown(input)` | `async (string) -> string` | One-shot async rendering. Waits for WASM if needed. |
|
|
577
|
+
| `renderMarkdownSync(input)` | `(string) -> string` | Synchronous rendering. Returns `''` if WASM not loaded. |
|
|
578
|
+
|
|
579
|
+
### Security Model
|
|
580
|
+
|
|
581
|
+
XSS safety is achieved through **compile-time structural guarantees**, not runtime sanitization:
|
|
582
|
+
|
|
583
|
+
1. `pulldown-cmark` parses markdown into typed Rust events (not strings)
|
|
584
|
+
2. `Event::Html` and `Event::InlineHtml` (raw HTML) are converted to `Event::Text` (escaped)
|
|
585
|
+
3. Dangerous URL schemes (`javascript:`, `data:`, `vbscript:`) are stripped from links
|
|
586
|
+
4. All text content is HTML-escaped by pulldown-cmark's renderer
|
|
587
|
+
|
|
588
|
+
This removed the `ammonia` + `html5ever` dependency chain (22 crates, ~400KB WASM),
|
|
589
|
+
cutting the binary by ~49% gzipped.
|
|
590
|
+
|
|
591
|
+
### Streaming Safety
|
|
592
|
+
|
|
593
|
+
During streaming, `streaming-markdown` (3KB vendored) renders directly to DOM
|
|
594
|
+
for instant visual feedback. When `endStream()` is called, the accumulated text
|
|
595
|
+
is re-rendered through the WASM pipeline for defense-in-depth XSS sanitization.
|
|
596
|
+
|
|
597
|
+
### Code Viewer (Mac Terminal Style)
|
|
598
|
+
|
|
599
|
+
`MessagePart` with `kind="code_block"` renders a macOS-style terminal with
|
|
600
|
+
traffic light dots, title bar, copy button, and syntax highlighting for 200+
|
|
601
|
+
languages.
|
|
602
|
+
|
|
603
|
+
Syntax highlighting uses `syntect` (SSR-side, feature-gated). The CSS theme
|
|
604
|
+
is in `packages/core/dist/syntax.css` -- include it alongside `tokens.css`
|
|
605
|
+
and `cx-utilities.css`.
|
|
606
|
+
|
|
607
|
+
### Component Preview (Sandboxed)
|
|
608
|
+
|
|
609
|
+
`MessagePart` with `kind="preview"` renders a sandboxed iframe with framework
|
|
610
|
+
tabs. The iframe uses `sandbox="allow-scripts"` for isolation. Tab switching
|
|
611
|
+
is handled by the `message-part` behavior module.
|
|
612
|
+
|
|
613
|
+
---
|
|
614
|
+
|
|
615
|
+
## Behavior Module Architecture
|
|
616
|
+
|
|
617
|
+
Component interactivity uses a three-layer runtime:
|
|
618
|
+
|
|
619
|
+
1. **Core Loader** (`static/loader.js`, ~200 lines) -- event delegation, WASM loading,
|
|
620
|
+
state R/W, floating positioning, behavior routing via `__cx` namespace
|
|
621
|
+
2. **Behavior Modules** (`static/_behaviors/*.js`, loaded via `<script defer>`) --
|
|
622
|
+
per-component DOM orchestration (focus, ARIA, classList, scrollIntoView)
|
|
623
|
+
3. **WASM Handlers** (`crates/handlers/src/*.rs`, lazy-loaded) -- pure business
|
|
624
|
+
logic (filtering, sorting, state transitions)
|
|
625
|
+
|
|
626
|
+
Rules:
|
|
627
|
+
- Core loader must stay under ~200 lines (core infrastructure only)
|
|
628
|
+
- Behavior modules register via `__cx.behavior('name', handler)`
|
|
629
|
+
- WASM handlers are pure: `(state) -> new_state`, no DOM access
|
|
630
|
+
- Behavior modules include JS fallbacks for WASM functions
|
|
631
|
+
- Each Custom Element loads only its needed behavior `<script>` tag
|
|
632
|
+
|
|
633
|
+
---
|
|
634
|
+
|
|
635
|
+
## WASM Dispatcher
|
|
636
|
+
|
|
637
|
+
Collet uses a single WASM entry point instead of per-component exports:
|
|
638
|
+
|
|
639
|
+
```js
|
|
640
|
+
// Internal -- called by Custom Element definitions
|
|
641
|
+
import { cx_render } from '@colletdev/core/wasm';
|
|
642
|
+
|
|
643
|
+
const html = cx_render('button', { label: 'Click', variant: 'filled' });
|
|
644
|
+
```
|
|
645
|
+
|
|
646
|
+
The dispatcher pattern (`cx_render(component, config)`) plus a separate
|
|
647
|
+
`cx_render_markdown(input)` export. This gives 2 WASM exports instead
|
|
648
|
+
of 45, yielding cleaner binaries and faster WASM instantiation.
|
|
649
|
+
|
|
650
|
+
---
|
|
651
|
+
|
|
652
|
+
## TypeScript Types
|
|
653
|
+
|
|
654
|
+
Shared types are exported from `@colletdev/core`:
|
|
655
|
+
|
|
656
|
+
```ts
|
|
657
|
+
import type {
|
|
658
|
+
CloseDetail,
|
|
659
|
+
NavigateDetail,
|
|
660
|
+
InputDetail,
|
|
661
|
+
SelectDetail,
|
|
662
|
+
MenuActionDetail,
|
|
663
|
+
SidebarGroup,
|
|
664
|
+
SelectOption,
|
|
665
|
+
OptionGroup,
|
|
666
|
+
TableColumn,
|
|
667
|
+
TableRow,
|
|
668
|
+
TabItem,
|
|
669
|
+
MenuEntry,
|
|
670
|
+
CarouselSlide,
|
|
671
|
+
StepperStep,
|
|
672
|
+
RadioOption,
|
|
673
|
+
ToggleGroupItem,
|
|
674
|
+
AccordionItem,
|
|
675
|
+
BreadcrumbItem,
|
|
676
|
+
SpeedDialAction,
|
|
677
|
+
SplitMenuEntry,
|
|
678
|
+
MessageGroupPart,
|
|
679
|
+
} from '@colletdev/core';
|
|
680
|
+
```
|
|
681
|
+
|
|
682
|
+
Complex props (`items`, `groups`, `entries`, `slides`, etc.) accept either
|
|
683
|
+
a typed object/array or a pre-serialized JSON string. When an object is passed,
|
|
684
|
+
the wrapper calls `JSON.stringify` automatically.
|