@texuli/core 0.1.0-next.2 → 0.2.0
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 +139 -129
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,144 +1,154 @@
|
|
|
1
1
|
# @texuli/core
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
## What exists today
|
|
11
|
-
|
|
12
|
-
Every texel in the table below is implemented, tested and documented in this
|
|
13
|
-
package. The public API is exactly what the root barrel `src/index.ts`
|
|
14
|
-
exports; each texel is also a per-area subpath export in `package.json`
|
|
15
|
-
(`@texuli/core/<area>`), so consumers can import narrowly and tree-shake the
|
|
16
|
-
rest.
|
|
17
|
-
|
|
18
|
-
| Texel | Headline exports | Doc |
|
|
19
|
-
| --- | --- | --- |
|
|
20
|
-
| Announcer | `createAnnouncer`, `createLiveRegion`, `getAnnouncer`, `appendRegion` | [core-announcer](docs/core-announcer.md) |
|
|
21
|
-
| Controllable state | `createControllableState` | [core-controllable](docs/core-controllable.md) |
|
|
22
|
-
| Direction and locale | `directionOf`, `inlineSideToPhysical`, `createCollator`, `compareLocale`, `resolveMessages` | [core-dir-locale](docs/core-dir-locale.md) |
|
|
23
|
-
| Dismiss | `bindDismissLayer`, `isPointerOutside`, `isFocusOutside`, `createDismissOutsideEvent`, `createOpenGestureGuard` | [core-dismiss](docs/core-dismiss.md) |
|
|
24
|
-
| Editor model | `loadEditorDocument`, `normalizeDocument`, `validateEditorDocument`, `caret`, `clampSelection`, `inlineToTree`; transforms (`insertText`, `splitBlock`, `deleteBackward`, `toggleMark`, `setLink`, `replaceSelection`, `applyInputRules`…), `editorHistoryReducer`, `createEditorKeymap`, `fromMarkdown`/`toMarkdown` (subpaths `@texuli/core/editor-model`, `editor-transforms`, `editor-history`, `editor-keymap`, `editor-markdown`; not in the root barrel) | [core-editor](docs/core-editor.md) |
|
|
25
|
-
| Environment | `isBrowser`, `getOwnerDocument`, `getOwnerWindow`, `listen`, `matchesMedia`, `requestFrame` | [core-env](docs/core-env.md) |
|
|
26
|
-
| Focus scope | `createFocusScope` | [core-focus-scope](docs/core-focus-scope.md) |
|
|
27
|
-
| Form bridge | `createFormBridge` | [core-form-bridge](docs/core-form-bridge.md) |
|
|
28
|
-
| Ids | `createIdScope`, `createIdFamily`, `joinIds`, `ariaLabelledBy`, `ariaDescribedBy`, `ariaControls`, `ensureElementId`, `sanitizeIdPart` | [core-ids](docs/core-ids.md) |
|
|
29
|
-
| Inert | `createInertRegistry`, `getInertRegistry`, `computeInertTargets` | [core-inert](docs/core-inert.md) |
|
|
30
|
-
| Keyboard | `parseShortcut`, `matchesShortcut`, `formatShortcut`, `createShortcutMap`, `getNavigationIntent`, `normalizeKey`, `detectPlatform` | [core-keyboard](docs/core-keyboard.md) |
|
|
31
|
-
| Layer stack | `createLayerStack`, `getLayerStack`, `decideEscape`, `bindLayerStackDocument`, `layerReceivesEscape` | [core-layer-stack](docs/core-layer-stack.md) |
|
|
32
|
-
| Lazy chunks | `lazyChunk`, `hasLazyChunk`, `clearLazyChunk(s)` (subpath `@texuli/core/lazy`; not in the root barrel) | — |
|
|
33
|
-
| Overlay engine | `createOverlay` (`OVERLAY_MODE_ATTR`, `OVERLAY_Z_BASE`); the reduced surface is the `@texuli/core/overlay-core` subpath | [core-overlay-core](docs/core-overlay-core.md), [overlay-engine](docs/overlay-engine.md) |
|
|
34
|
-
| Portal | `createPortal` | [core-portal](docs/core-portal.md) |
|
|
35
|
-
| Positioning (DOM) | `positionFloating`, `applyPosition`, `autoUpdate`, `observeMove`, `cssAnchorDeclarations`, `supportsCssAnchorPositioning` | [core-position-dom](docs/core-position-dom.md) |
|
|
36
|
-
| Positioning (math) | `computePosition`, `place`, `shiftIntoBoundary`, `sizeToAvailable`, `listPlacements`, `resolvePlacementForDirection`, `rect` | [core-position-math](docs/core-position-math.md) |
|
|
37
|
-
| Presence | `createPresence`, `prefersReducedMotion`, `hasPendingMotion`, `parseCssTimeList` | [core-presence](docs/core-presence.md) |
|
|
38
|
-
| Roving focus + typeahead | `createRovingFocus`, `createTypeahead`, `stepRovingIndex`, `setRovingTabIndex`, `indexOfSelected` | [core-roving](docs/core-roving.md) |
|
|
39
|
-
| Scroll lock | `lockScroll` | [core-scroll-lock](docs/core-scroll-lock.md) |
|
|
40
|
-
| Tabbable | `isFocusable`, `isTabbable`, `getFocusableElements`, `getTabbableElements` | [core-tabbable](docs/core-tabbable.md) |
|
|
41
|
-
| URL policy (links) | `safeHref`, `type HrefTrust` | [core-url-policy](docs/core-url-policy.md) |
|
|
42
|
-
| URL policy (media) | `safeImageSrc`, `safeImageSrcset`, `safeMediaSrc`, `MAX_DATA_IMAGE_BYTES` | [core-url-policy](docs/core-url-policy.md) |
|
|
43
|
-
|
|
44
|
-
The editor texels are not in the root barrel: the barrel measures 22.64 of
|
|
45
|
-
its 24 KiB budget. `pnpm size` (2026-10-26) measures `editor-history` at
|
|
46
|
-
773 B and `editor-keymap` at 2.49 KiB, both within the 4 KiB per-texel
|
|
47
|
-
default. `editor-markdown` (7.05 of 7.50 KiB), `editor-model` (7.83 of
|
|
48
|
-
8.50 KiB) and `editor-transforms` (10.36 of 11 KiB) are over that default
|
|
49
|
-
and carry their own budget rows; every entry is within budget.
|
|
50
|
-
|
|
51
|
-
The tables above name the headline exports per texel, not the full surface:
|
|
52
|
-
read `src/index.ts` (and the per-area `index.ts`) for every exported symbol
|
|
53
|
-
and type. The threat model behind the URL policy lives in
|
|
54
|
-
[docs/SECURITY.md](../../docs/SECURITY.md).
|
|
55
|
-
|
|
56
|
-
## Examples
|
|
3
|
+
The framework-free foundation of [Texuli](https://texuli.com), an MIT-licensed
|
|
4
|
+
Svelte 5 component library by Dailybot, Inc. (*Small pieces. Infinite
|
|
5
|
+
possibilities.*). `@texuli/core` holds the **texels**: small, tested
|
|
6
|
+
TypeScript units of behavior and policy (focus management, roving focus,
|
|
7
|
+
layering and Escape handling, positioning, ids, keyboard shortcuts, URL
|
|
8
|
+
safety) that every Texuli primitive and component is built from.
|
|
57
9
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
createIdScope,
|
|
62
|
-
ariaLabelledBy,
|
|
63
|
-
getLayerStack,
|
|
64
|
-
decideEscape,
|
|
65
|
-
getTabbableElements,
|
|
66
|
-
isTabbable,
|
|
67
|
-
} from '@texuli/core';
|
|
68
|
-
|
|
69
|
-
safeHref('https://example.com/a'); // 'https://example.com/a'
|
|
70
|
-
safeHref('http://example.com'); // undefined (untrusted is the default)
|
|
71
|
-
safeHref('/docs/button', 'developer'); // '/docs/button'
|
|
72
|
-
safeHref('javascript:alert(1)', 'developer'); // undefined in every tier
|
|
10
|
+
Use it directly when you build your own components in any framework, or
|
|
11
|
+
when you need Texuli's URL policy to sanitize links and media from agents
|
|
12
|
+
or users. It has no runtime dependencies and imports no UI framework.
|
|
73
13
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
14
|
+
> **Status: 0.x release.** Texuli is in development. The packages are
|
|
15
|
+
> published to npm as `0.2.0`, a pre-1.0 release (1.0.0 will be the first
|
|
16
|
+
> stable release). Under 0.x, a new minor (`0.3.0`) may contain breaking
|
|
17
|
+
> changes, so pin the exact version or use `^0.2.0`, which accepts only 0.2.x.
|
|
77
18
|
|
|
78
|
-
|
|
79
|
-
const layer = stack.register({ element: panelEl, label: 'dialog' });
|
|
80
|
-
decideEscape(stack, { key: 'Escape' }); // reaches only the topmost layer
|
|
81
|
-
layer.unregister();
|
|
19
|
+
## Install
|
|
82
20
|
|
|
83
|
-
|
|
84
|
-
|
|
21
|
+
```bash
|
|
22
|
+
npm install @texuli/core
|
|
85
23
|
```
|
|
86
24
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
- **`developer`** (for app-authored props): anything except `javascript:`,
|
|
92
|
-
`vbscript:`, `data:` and any value opening with two slashes or
|
|
93
|
-
backslashes in any mix (`//host`, `/\host`, `\/host`, `\\host`) or with
|
|
94
|
-
any leading backslash, which browsers resolve off-origin.
|
|
95
|
-
- Whitespace and ASCII control characters are stripped before the scheme is
|
|
96
|
-
read, so `java\tscript:` is caught.
|
|
97
|
-
|
|
98
|
-
Images and media use the element-specific policies: `safeImageSrc` allows
|
|
99
|
-
HTTPS and relative assets from either tier, plus (developer tier only) HTTP,
|
|
100
|
-
valid blob URLs, and PNG, JPEG, GIF, WebP or AVIF base64 data images up to
|
|
101
|
-
1 MiB decoded; SVG and non-image data URLs are always refused.
|
|
102
|
-
`safeImageSrcset` accepts a comma-separated `srcset` only when every candidate
|
|
103
|
-
is safe. `safeMediaSrc` restricts untrusted audio/video to blob URLs or exact
|
|
104
|
-
origins from an HTTPS `allowedOrigins` list. Full contract:
|
|
105
|
-
[core-url-policy](docs/core-url-policy.md).
|
|
106
|
-
|
|
107
|
-
## Layout
|
|
108
|
-
|
|
109
|
-
```text
|
|
110
|
-
packages/core/
|
|
111
|
-
├── package.json exports "." and one subpath per texel; script: typecheck
|
|
112
|
-
├── tsconfig.json extends ../../tsconfig.base.json
|
|
113
|
-
├── docs/ one contract doc per texel (core-*.md) + overlay-engine.md
|
|
114
|
-
├── src/index.ts the public barrel (only what is exported here is public)
|
|
115
|
-
├── src/<area>/ one directory per texel (announcer, roving, layer-stack, …)
|
|
116
|
-
└── tests/ 26 test files, one or more per texel, including hostile
|
|
117
|
-
inputs for every policy that guards a sink
|
|
25
|
+
```bash
|
|
26
|
+
pnpm add @texuli/core
|
|
27
|
+
# or
|
|
28
|
+
yarn add @texuli/core
|
|
118
29
|
```
|
|
119
30
|
|
|
120
|
-
|
|
31
|
+
No peer dependencies and no CSS. The package is ESM-only and ships
|
|
32
|
+
TypeScript declarations.
|
|
121
33
|
|
|
122
|
-
|
|
123
|
-
are fine.
|
|
124
|
-
- No runtime dependencies; no imports from any other workspace package (L0 is
|
|
125
|
-
the bottom layer).
|
|
126
|
-
- Policy functions fail closed and document their contract in JSDoc.
|
|
127
|
-
- Every export ships with tests in `tests/<name>.test.ts`, including hostile
|
|
128
|
-
inputs for anything that guards a sink.
|
|
129
|
-
- Ids never use `Math.random` at module scope; scopes are per-root.
|
|
34
|
+
## Quick start
|
|
130
35
|
|
|
131
|
-
|
|
36
|
+
```ts
|
|
37
|
+
import { ariaLabelledBy, createIdScope, safeHref } from '@texuli/core';
|
|
38
|
+
import { detectPlatform, matchesShortcut } from '@texuli/core/keyboard';
|
|
132
39
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
40
|
+
// URL policy: untrusted (agent or user) content is the default tier.
|
|
41
|
+
safeHref('https://example.com/report'); // 'https://example.com/report'
|
|
42
|
+
safeHref('http://example.com'); // undefined (https only)
|
|
43
|
+
safeHref('/docs/button', 'developer'); // '/docs/button'
|
|
44
|
+
safeHref('javascript:alert(1)', 'developer'); // undefined in every tier
|
|
45
|
+
|
|
46
|
+
// Ids that match on the server and the client.
|
|
47
|
+
const ids = createIdScope({ prefix: 'dialog' });
|
|
48
|
+
const titleId = ids.next('title'); // 'dialog-0-title'
|
|
49
|
+
ariaLabelledBy(titleId); // 'dialog-0-title'
|
|
50
|
+
|
|
51
|
+
// Keyboard shortcuts written as data; Mod is Cmd on Apple platforms, Ctrl elsewhere.
|
|
52
|
+
const platform = detectPlatform(navigator);
|
|
53
|
+
window.addEventListener('keydown', (event) => {
|
|
54
|
+
if (matchesShortcut(event, 'Mod+K', platform)) {
|
|
55
|
+
// open your command palette
|
|
56
|
+
}
|
|
57
|
+
});
|
|
138
58
|
```
|
|
139
59
|
|
|
140
|
-
|
|
141
|
-
`
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
60
|
+
Everything in the table below is available from the root entry
|
|
61
|
+
(`@texuli/core`) and from its own subpath (`@texuli/core/<subpath>`), except
|
|
62
|
+
the editor and lazy-chunk texels, which are subpath-only. Import subpaths to
|
|
63
|
+
keep bundles narrow.
|
|
64
|
+
|
|
65
|
+
## What's inside
|
|
66
|
+
|
|
67
|
+
| Texel | Subpath | Headline exports |
|
|
68
|
+
| --- | --- | --- |
|
|
69
|
+
| URL policy | `url-policy` | `safeHref`, `safeImageSrc`, `safeImageSrcset`, `safeMediaSrc` |
|
|
70
|
+
| Ids | `ids` | `createIdScope`, `createIdFamily`, `ariaLabelledBy`, `ariaDescribedBy`, `ensureElementId` |
|
|
71
|
+
| Keyboard | `keyboard` | `parseShortcut`, `matchesShortcut`, `formatShortcut`, `createShortcutMap`, `getNavigationIntent` |
|
|
72
|
+
| Roving focus and typeahead | `roving` | `createRovingFocus`, `createTypeahead`, `setRovingTabIndex` |
|
|
73
|
+
| Focus scope | `focus-scope` | `createFocusScope` (trap and restore focus) |
|
|
74
|
+
| Tabbable | `tabbable` | `isFocusable`, `isTabbable`, `getTabbableElements` |
|
|
75
|
+
| Layer stack | `layer-stack` | `getLayerStack`, `decideEscape` (Escape reaches only the topmost layer) |
|
|
76
|
+
| Dismiss | `dismiss` | `bindDismissLayer`, `isPointerOutside`, `isFocusOutside` |
|
|
77
|
+
| Inert | `inert` | `createInertRegistry`, `computeInertTargets` |
|
|
78
|
+
| Scroll lock | `scroll-lock` | `lockScroll` |
|
|
79
|
+
| Portal | `portal` | `createPortal` |
|
|
80
|
+
| Overlay engine | `overlay`, `overlay-core` | `createOverlay` (combines the texels above for dialogs, popovers and menus); `createOverlayCore` is the reduced surface |
|
|
81
|
+
| Positioning (math) | `position-math` | `computePosition`, `place`, `shiftIntoBoundary`, `resolvePlacementForDirection` |
|
|
82
|
+
| Positioning (DOM) | `position-dom` | `positionFloating`, `autoUpdate`, `cssAnchorDeclarations` |
|
|
83
|
+
| Presence | `presence` | `createPresence`, `prefersReducedMotion` (enter and exit animations) |
|
|
84
|
+
| Announcer | `announcer` | `createAnnouncer`, `createLiveRegion` (screen reader announcements) |
|
|
85
|
+
| Controllable state | `controllable` | `createControllableState` (controlled and uncontrolled values) |
|
|
86
|
+
| Form bridge | `form-bridge` | `createFormBridge` (custom controls in native forms) |
|
|
87
|
+
| Direction and locale | `dir-locale` | `directionOf`, `inlineSideToPhysical`, `createCollator`, `resolveMessages` |
|
|
88
|
+
| Environment | `env` | `isBrowser`, `getOwnerDocument`, `listen`, `matchesMedia`, `requestFrame` |
|
|
89
|
+
| Lazy chunks | `lazy` | `lazyChunk`, `hasLazyChunk` (subpath only) |
|
|
90
|
+
| Rich text editor model | `editor-model`, `editor-transforms`, `editor-history`, `editor-keymap`, `editor-markdown` | Document model, transforms, undo history, keymap, Markdown import and export (subpath only) |
|
|
91
|
+
|
|
92
|
+
The table names the headline exports; each subpath's TypeScript
|
|
93
|
+
declarations list the full surface.
|
|
94
|
+
|
|
95
|
+
## URL policy
|
|
96
|
+
|
|
97
|
+
Every Texuli component that renders a link or a media source passes it
|
|
98
|
+
through this policy, and you can use it the same way. Rejected values return
|
|
99
|
+
`undefined` (the policy fails closed).
|
|
100
|
+
|
|
101
|
+
- **`untrusted`** (the default, for agent or user content): only `https:`,
|
|
102
|
+
`mailto:` and `#fragment` links.
|
|
103
|
+
- **`developer`** (for app-authored values): anything except `javascript:`,
|
|
104
|
+
`vbscript:`, `data:`, and values that start with two slashes or
|
|
105
|
+
backslashes in any mix, or with any backslash, which browsers resolve
|
|
106
|
+
off-origin.
|
|
107
|
+
- Whitespace and ASCII control characters are stripped before the scheme is
|
|
108
|
+
read, so `java\tscript:` is caught.
|
|
109
|
+
- `safeImageSrc` refuses SVG and non-image `data:` URLs in every tier;
|
|
110
|
+
`safeMediaSrc` limits untrusted audio and video to blob URLs or exact
|
|
111
|
+
HTTPS origins you allow.
|
|
112
|
+
|
|
113
|
+
## Key guarantees
|
|
114
|
+
|
|
115
|
+
- **Framework-free.** No Svelte, React or other UI framework imports, and no
|
|
116
|
+
runtime dependencies. Standard DOM types only.
|
|
117
|
+
- **SSR-friendly ids.** Id scopes are per render root with no module-level
|
|
118
|
+
randomness, so the server and the client produce the same ids; `isBrowser`
|
|
119
|
+
and the owner-document helpers let code skip DOM work on the server.
|
|
120
|
+
- **Accessibility building blocks.** Roving focus, focus scopes, inert
|
|
121
|
+
backgrounds and layered Escape handling implement the behavior the
|
|
122
|
+
WAI-ARIA Authoring Practices patterns require.
|
|
123
|
+
- **Right-to-left aware.** Direction helpers map logical sides to physical
|
|
124
|
+
ones, and positioning resolves placements for the current direction.
|
|
125
|
+
|
|
126
|
+
## Documentation
|
|
127
|
+
|
|
128
|
+
- Website: [texuli.com](https://texuli.com) and the
|
|
129
|
+
[accessibility guide](https://texuli.com/guides/accessibility/)
|
|
130
|
+
- URL policy contract: [core-url-policy.md](https://github.com/DailybotHQ/texuli/blob/main/packages/core/docs/core-url-policy.md)
|
|
131
|
+
- Overlay engine: [overlay-engine.md](https://github.com/DailybotHQ/texuli/blob/main/packages/core/docs/overlay-engine.md)
|
|
132
|
+
- Roving focus: [core-roving.md](https://github.com/DailybotHQ/texuli/blob/main/packages/core/docs/core-roving.md)
|
|
133
|
+
- Keyboard shortcuts: [core-keyboard.md](https://github.com/DailybotHQ/texuli/blob/main/packages/core/docs/core-keyboard.md)
|
|
134
|
+
- Positioning: [core-position-math.md](https://github.com/DailybotHQ/texuli/blob/main/packages/core/docs/core-position-math.md)
|
|
135
|
+
and [core-position-dom.md](https://github.com/DailybotHQ/texuli/blob/main/packages/core/docs/core-position-dom.md)
|
|
136
|
+
- Rich text editor model: [core-editor.md](https://github.com/DailybotHQ/texuli/blob/main/packages/core/docs/core-editor.md)
|
|
137
|
+
- Every texel's contract: [packages/core/docs](https://github.com/DailybotHQ/texuli/tree/main/packages/core/docs)
|
|
138
|
+
- Security model: [SECURITY.md](https://github.com/DailybotHQ/texuli/blob/main/docs/SECURITY.md)
|
|
139
|
+
|
|
140
|
+
## Related packages
|
|
141
|
+
|
|
142
|
+
- [`@texuli/primitives`](https://www.npmjs.com/package/@texuli/primitives): headless Svelte 5 primitives built on these texels.
|
|
143
|
+
- [`texuli`](https://www.npmjs.com/package/texuli): styled, ready-to-use components.
|
|
144
|
+
- [`@texuli/tokens`](https://www.npmjs.com/package/@texuli/tokens): the theme engine and design tokens.
|
|
145
|
+
|
|
146
|
+
## Contributing
|
|
147
|
+
|
|
148
|
+
Source, issues and contribution guidelines live at
|
|
149
|
+
[github.com/DailybotHQ/texuli](https://github.com/DailybotHQ/texuli). Report
|
|
150
|
+
bugs at [github.com/DailybotHQ/texuli/issues](https://github.com/DailybotHQ/texuli/issues).
|
|
151
|
+
|
|
152
|
+
## License
|
|
153
|
+
|
|
154
|
+
MIT © Dailybot, Inc.
|