@weft-js/cli 0.0.0-stage → 0.1.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/LICENSE +16 -0
- package/README.md +74 -2
- package/bin/weft.mjs +8 -0
- package/docs/async.md +40 -0
- package/docs/browser.md +39 -0
- package/docs/config.md +93 -0
- package/docs/deploy.md +83 -0
- package/docs/design.md +97 -0
- package/docs/fonts.md +70 -0
- package/docs/icons.md +102 -0
- package/docs/lists.md +23 -0
- package/docs/meta.md +82 -0
- package/docs/plugins.md +143 -0
- package/docs/props.md +77 -0
- package/docs/rules.md +32 -0
- package/docs/rust.md +223 -0
- package/docs/start-short.md +8 -0
- package/docs/start.md +422 -0
- package/docs/state.md +96 -0
- package/docs/style.md +59 -0
- package/docs/surface.md +111 -0
- package/docs/testing.md +71 -0
- package/docs/ui-state.md +92 -0
- package/docs/verify.md +48 -0
- package/package.json +39 -4
- package/src/add-rust.mjs +123 -0
- package/src/add.mjs +358 -0
- package/src/agents-md.mjs +134 -0
- package/src/alias.mjs +59 -0
- package/src/audit.mjs +366 -0
- package/src/browser/cli.mjs +34 -0
- package/src/browser/client.mjs +132 -0
- package/src/browser/commands.mjs +143 -0
- package/src/catalog.mjs +496 -0
- package/src/check.mjs +330 -0
- package/src/cli.mjs +364 -0
- package/src/config.d.ts +1 -0
- package/src/config.mjs +5 -0
- package/src/contributions.mjs +69 -0
- package/src/db-layer.mjs +33 -0
- package/src/deploy/cdn.mjs +64 -0
- package/src/deploy/files.mjs +187 -0
- package/src/deploy/fit.mjs +62 -0
- package/src/deploy/hosts.mjs +56 -0
- package/src/deploy/index.mjs +273 -0
- package/src/deploy/needs.mjs +92 -0
- package/src/design-checks.mjs +439 -0
- package/src/diag.mjs +74 -0
- package/src/docs.mjs +132 -0
- package/src/doctor.mjs +30 -0
- package/src/files.mjs +47 -0
- package/src/fix.mjs +40 -0
- package/src/gate.mjs +276 -0
- package/src/index.mjs +9 -0
- package/src/infer/engine.cjs +525 -0
- package/src/infer/facts.cjs +252 -0
- package/src/infer/hosts.cjs +114 -0
- package/src/infer/lsp.mjs +131 -0
- package/src/infer/session.cjs +105 -0
- package/src/infer/shadow.cjs +46 -0
- package/src/infer/worker.cjs +97 -0
- package/src/jsx-scan.mjs +86 -0
- package/src/load-config.mjs +303 -0
- package/src/map.mjs +110 -0
- package/src/mcp.mjs +195 -0
- package/src/meta-table.mjs +28 -0
- package/src/mutate.mjs +140 -0
- package/src/plugin-checks.mjs +89 -0
- package/src/plugins.mjs +172 -0
- package/src/project.mjs +219 -0
- package/src/props-checks.mjs +46 -0
- package/src/props-listener.mjs +38 -0
- package/src/react-checks.mjs +36 -0
- package/src/share-checks.mjs +74 -0
- package/src/site.mjs +19 -0
- package/src/slots.mjs +96 -0
- package/src/start.mjs +296 -0
- package/src/state-checks.mjs +213 -0
- package/src/style.mjs +320 -0
- package/src/surface-checks.mjs +218 -0
- package/src/tailwind.mjs +22 -0
- package/src/tasks.mjs +99 -0
- package/src/testdiff.mjs +157 -0
- package/src/tests.mjs +572 -0
- package/src/text-checks.mjs +172 -0
- package/src/tokens.mjs +24 -0
- package/src/trust.mjs +282 -0
- package/src/weft.mjs +66 -0
- package/src/workspace.mjs +105 -0
- package/ts-plugin/index.js +137 -0
- package/ts-plugin/package.json +1 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 ravonus
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
|
|
6
|
+
documentation files (the "Software"), to deal in the Software without restriction, including without limitation the
|
|
7
|
+
rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit
|
|
8
|
+
persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
9
|
+
|
|
10
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the
|
|
11
|
+
Software.
|
|
12
|
+
|
|
13
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
|
|
14
|
+
WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
|
15
|
+
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
|
16
|
+
OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,75 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @weft-js/cli
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
> **Early release (0.1.1).** APIs will change between minor versions.
|
|
4
|
+
|
|
5
|
+
The `weft` command. It runs an app (`dev`, `build`, `start`), checks it (`check`, `tsc`, `fix`), serves its docs to people
|
|
6
|
+
and agents (`docs`, `agents-md`, `mcp`) and manages plugins (`add`, `plugins`). An app made with `npm create @weft-js`
|
|
7
|
+
already has it.
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install -D @weft-js/cli
|
|
11
|
+
npx weft check
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
Each problem prints as `file:line:col CODE severity why`, with a `fix:` line under it, and the run ends with a count of
|
|
15
|
+
files, errors and warnings.
|
|
16
|
+
|
|
17
|
+
## Commands
|
|
18
|
+
|
|
19
|
+
`npx weft help` prints this list.
|
|
20
|
+
|
|
21
|
+
| Command | What it does |
|
|
22
|
+
|---|---|
|
|
23
|
+
| `check [files…]` | Compiles every component in check mode and reports diagnostics with fixes. `--verbose` says which compiler ran |
|
|
24
|
+
| `tsc` | Type-checks the whole project with inferred props, for CI |
|
|
25
|
+
| `fix [files…]` | Applies the safe autofixes |
|
|
26
|
+
| `doctor` | Which compiler this install uses, whether Rust is around (it's optional), and what live data does on the target |
|
|
27
|
+
| `dev [--port <n>]` | The dev server, for an app with `src/routes` |
|
|
28
|
+
| `build [--runtime node\|bun\|deno] [--preset <p>]` | The production build, into `.output/` |
|
|
29
|
+
| `start [--runtime <r>]` | Runs the build |
|
|
30
|
+
| `db generate [name]`, `db live`, `db listen` | A migration for schema changes; one with change triggers; the change feed as its own process |
|
|
31
|
+
| `routes [--meta]` | The route table; `--meta` adds each route's metadata and H-diagnostics |
|
|
32
|
+
| `test --gate` | The test gate on the diff: fail-before, mutants on changed lines and no weakened tests |
|
|
33
|
+
| `browser <cmd>` | Drives the running app: open, snapshot, act, assert and more (`weft browser help`) |
|
|
34
|
+
| `docs [topic\|code]` | Prints a docs page or a diagnostic (`weft docs props`, `weft docs W014`) |
|
|
35
|
+
| `agents-md [--check]` | Writes AGENTS.md from the installed weft; `--check` exits 1 when it's stale |
|
|
36
|
+
| `map [--stdout]` | Writes `.weft/map.md`: what to call in this app |
|
|
37
|
+
| `tasks`, `run <task>` | Lists commands and project tasks; runs a task from `weft.config.ts` |
|
|
38
|
+
| `add <feature>` | Installs a plugin and wires it into the config; also `tailwind`, `auth`, `db` and `rust` |
|
|
39
|
+
| `plugins [list\|explain\|sync\|audit\|approve]` | Plugins, their slots and permissions |
|
|
40
|
+
| `style find\|explain\|matrix` | Style groups |
|
|
41
|
+
| `tokens` | warp's resolved tokens as DTCG, in `.weft/tokens/` |
|
|
42
|
+
| `lsp` | A language server for editors, with inferred props |
|
|
43
|
+
| `mcp` | These commands as MCP tools, on stdio |
|
|
44
|
+
|
|
45
|
+
Every command takes `--root <dir>` (the working directory by default) and `--json`. JSON goes to stdout and text for
|
|
46
|
+
people to stderr. Exit codes: 0 ok, 1 errors found (or a stale AGENTS.md under `--check`), 2 a usage or internal error.
|
|
47
|
+
|
|
48
|
+
## Docs
|
|
49
|
+
|
|
50
|
+
- `weft docs config`: `weft.config.ts` and every key in it.
|
|
51
|
+
- `weft docs diagnostics`: every code, and which ones the installed weft reports.
|
|
52
|
+
- `weft docs verify`: `weft browser` and the test gate.
|
|
53
|
+
- `weft docs plugins`: writing a plugin.
|
|
54
|
+
|
|
55
|
+
The source of each page is in [docs/](docs). Adding a diagnostic is in [CONTRIBUTING.md](../../CONTRIBUTING.md).
|
|
56
|
+
|
|
57
|
+
## A diagnostic
|
|
58
|
+
|
|
59
|
+
`check`, `tasks` and the MCP tools report the same shape:
|
|
60
|
+
|
|
61
|
+
```json
|
|
62
|
+
{ "code": "W014", "severity": "error", "owner": "core", "file": "src/App.tsx",
|
|
63
|
+
"span": [21, 44, 21, 117], "offset": [867, 940],
|
|
64
|
+
"why": "`.map(p => ({ ...p, … }))` gives every item a new identity on each update…",
|
|
65
|
+
"fix": { "text": "Keep items stable … or replace only the item that changed." },
|
|
66
|
+
"project": true,
|
|
67
|
+
"related": [{ "file": "src/components/ProductList.tsx", "span": [29, 10, 29, 10], "why": "rendered here as a keyed list" }],
|
|
68
|
+
"docs": "weft docs W014" }
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
- `span` is 1-based `[line, col, endLine, endCol]`, with columns in UTF-16 units. `offset` is UTF-16 indices into the
|
|
72
|
+
file's text.
|
|
73
|
+
- `fix.edits` (`{ file, span, offset, text }`) is there when there's a mechanical change, and `fix.safe` says whether
|
|
74
|
+
`weft fix` applies it without asking.
|
|
75
|
+
- `project: true` marks a result that needed more than one file to find.
|
package/bin/weft.mjs
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { main } from '../src/cli.mjs'
|
|
3
|
+
|
|
4
|
+
// Not `exitCode`: an app's module can leave a pool open, and a finished command must still end.
|
|
5
|
+
const code = await main(process.argv.slice(2))
|
|
6
|
+
// A pipe holds about 64 KB: exiting first would cut off long --json output.
|
|
7
|
+
await Promise.all([process.stdout, process.stderr].map(out => new Promise(done => { out.write('', done) })))
|
|
8
|
+
process.exit(code)
|
package/docs/async.md
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# Async data, Boundary and SSR
|
|
2
|
+
|
|
3
|
+
```tsx
|
|
4
|
+
import { resource, Boundary } from '@weft-js/core'
|
|
5
|
+
|
|
6
|
+
type User = { name: string }
|
|
7
|
+
|
|
8
|
+
function Name({ id }: { id: string }) {
|
|
9
|
+
// `id` is read before the first `await`, so a new id fetches again and aborts the old request.
|
|
10
|
+
const user = resource(signal => fetch(`/api/users/${id}`, { signal }).then(r => r.json() as Promise<User>))
|
|
11
|
+
// `pending` is also true on a refetch, while the old value is still here: dim it rather than replace it (X033).
|
|
12
|
+
return <h1 aria-busy={user.pending ? 'true' : 'false'}>{user.value?.name}</h1>
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// The resource is made inside the Boundary (in Name), so the Boundary waits for it.
|
|
16
|
+
export function Profile({ id }: { id: string }) {
|
|
17
|
+
return (
|
|
18
|
+
<Boundary pending={<p>Loading…</p>} error={(e, retry) => <button onClick={retry}>Retry</button>}>
|
|
19
|
+
<Name id={id} />
|
|
20
|
+
</Boundary>
|
|
21
|
+
)
|
|
22
|
+
}
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
- **`resource(fetcher)`** is async `derived`: `.value`, `.pending`, `.error`, `.refresh()`. It re-fetches when what it
|
|
26
|
+
read before the first `await` changes, and aborts the superseded fetch through `signal`. `derived(async …)` is
|
|
27
|
+
**W012**.
|
|
28
|
+
- **`<Boundary pending error>`** covers every resource created while its children are built, and every error they
|
|
29
|
+
throw. A resource made above the `<Boundary>` in the same component isn't below it, and the fallback never shows (X037). The fallback shows after 150 ms and
|
|
30
|
+
stays at least 400 ms; refetches keep the current content (no flash). `error(e, retry)` gets failed resources and
|
|
31
|
+
children that throw during render.
|
|
32
|
+
- **`@weft-js/core/ssr`**: `renderToString(() => <App />)` renders what it can now; `await renderToStringAsync(() => <App />)`
|
|
33
|
+
waits for resources first and puts their values in the page. Pass `{ hydratable: true }` when the client hydrates.
|
|
34
|
+
`renderToStream(() => <App />, { hydratable: true })` sends `shell` at once, then one chunk per `<Boundary>` as its
|
|
35
|
+
resources resolve (`chunks`, an async iterable), and `data()` for `hydrate` when they're done. `@weft-js/start`
|
|
36
|
+
streams this way for you.
|
|
37
|
+
- **`hydrate(() => <App />, el)`** from `@weft-js/core/dom` takes over server HTML without rewriting it. Use it instead of
|
|
38
|
+
`render` when the HTML came from `@weft-js/core/ssr`.
|
|
39
|
+
- Import only `renderToString`, `renderToStringAsync` and `renderToStream` from `@weft-js/core/ssr`; its other exports are
|
|
40
|
+
what compiled server code calls.
|
package/docs/browser.md
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# Browser values (`@weft-js/core/browser`)
|
|
2
|
+
|
|
3
|
+
```tsx
|
|
4
|
+
import { state } from '@weft-js/core'
|
|
5
|
+
import { online, visible, lifecycle, media, on, interval } from '@weft-js/core/browser'
|
|
6
|
+
|
|
7
|
+
export function Toolbar() {
|
|
8
|
+
const isOnline = online() // Derived<boolean>; server: true
|
|
9
|
+
const shown = visible() // Derived<boolean>; server: true
|
|
10
|
+
const page = lifecycle() // 'active' | 'passive' | 'hidden' | 'frozen' | 'terminated'
|
|
11
|
+
const dark = media('(prefers-color-scheme: dark)', { ssr: false }) // server renders `ssr`
|
|
12
|
+
const open = state(true)
|
|
13
|
+
const now = state(Date.now())
|
|
14
|
+
on(window, 'keydown', e => { if (e.key === 'Escape') open.value = false }) // e: KeyboardEvent
|
|
15
|
+
interval('1s', () => { now.value = Date.now() }) // also '500ms', '5m', '1h', or a number of ms
|
|
16
|
+
return (
|
|
17
|
+
<p hidden={!open.value}>
|
|
18
|
+
{isOnline.value ? 'online' : 'offline'}, {dark.value ? 'dark' : 'light'}, {shown.value ? page.value : 'hidden'},
|
|
19
|
+
{new Date(now.value).toLocaleTimeString()}
|
|
20
|
+
</p>
|
|
21
|
+
)
|
|
22
|
+
}
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
- Call them in setup (a component body, a `defineState` setup, or `createRoot`). Their owner removes them; created
|
|
26
|
+
anywhere else (module scope, a timer, a handler, after `await`) they throw in dev and are **W051**.
|
|
27
|
+
- Values subscribe on first read and share one listener per event, however many components read them.
|
|
28
|
+
- On the server a value is its `ssr` value (`online`/`visible`: `true`, `lifecycle`: `'active'`). `media` has no
|
|
29
|
+
default: pass `ssr` in server-rendered components (**W054**). Hydration renders the server value, then the real one.
|
|
30
|
+
- Not `window.addEventListener` / `setInterval` in components (**W050**, with an edit to `on` / `interval`), and not
|
|
31
|
+
`watch(() => live.value, v => { s.value = v })` (**W056**): read `live.value` where you need it.
|
|
32
|
+
- `tabs.channel('cart', schema)` is a validated `BroadcastChannel` between tabs: `.post(msg)`, `.on(fn)`, `.value`.
|
|
33
|
+
`state(init, { persist: 'local', sync: 'tabs' })` in a `defineState` keeps a box the same in every tab (S008 without
|
|
34
|
+
`persist`, or with `session`).
|
|
35
|
+
|
|
36
|
+
In tests (`@weft-js/core/test`): `await net.offline()`, `net.online()`, `page.hide()`, `page.show()`, `page.blur()`,
|
|
37
|
+
`page.focus()`, `page.freeze()`, `page.resume()`, `page.leave({ persisted })`, `page.restore()`,
|
|
38
|
+
`page.media(query, true)`, `tabs.post(name, msg)`, `tabs.posted(name)`. Each test starts online, visible and focused with every query false. After each test
|
|
39
|
+
@weft-js/core/test fails if anything is still open (**T012**); `leaks()` lists it.
|
package/docs/config.md
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# weft.config
|
|
2
|
+
|
|
3
|
+
`weft.config.ts` is the default. `weft.config.json` and `weft.config.yaml` take the same keys, and `weft check` writes
|
|
4
|
+
`.weft/config.schema.json` for editor completion. In JSON and YAML a plugin is a package name or `[name, options]`, like
|
|
5
|
+
`["@weft-js/warp", { "fields": "classic" }]`; anything that needs a function belongs in `weft.config.ts`. Keep one file:
|
|
6
|
+
two is P010. `weft add <plugin>` installs a plugin and adds it to whichever format you use; `weft add tailwind` sets up Tailwind, `weft add db mysql --layer native` adds a database, and `weft add auth` adds Better Auth to it.
|
|
7
|
+
|
|
8
|
+
```ts
|
|
9
|
+
import { defineConfig } from '@weft-js/core/config'
|
|
10
|
+
import { dialScript, warp } from '@weft-js/warp/plugin'
|
|
11
|
+
|
|
12
|
+
export default defineConfig({
|
|
13
|
+
scope: { write: ['src/**', 'test/**'], forbid: ['.env*'] },
|
|
14
|
+
tasks: {
|
|
15
|
+
seed: { run: 'node scripts/seed.mjs', description: 'Load demo products into the dev db', sideEffects: ['db'], env: ['dev'] },
|
|
16
|
+
},
|
|
17
|
+
environments: { dev: { url: 'http://localhost:5173' } },
|
|
18
|
+
site: { // page metadata defaults: static tags in the server's first response (weft docs meta)
|
|
19
|
+
name: 'Acme',
|
|
20
|
+
url: 'https://acme.dev',
|
|
21
|
+
title: '%s · Acme',
|
|
22
|
+
description: 'Tools for builders.',
|
|
23
|
+
locale: 'en_US',
|
|
24
|
+
image: { url: '/og.png', width: 1200, height: 630 }, // default 'auto': a generated card (weft docs meta); false: none
|
|
25
|
+
twitter: { card: 'summary_large_image', site: '@acme' },
|
|
26
|
+
inlineCss: false, // default true: each page carries what its first paint needs; false keeps plain links
|
|
27
|
+
criticalCss: false, // default true: one hashed sheet plus a first-paint part; false splits by chunk and inlines up to 48 KB
|
|
28
|
+
criticalKeep: ['theme-x'], // words of selectors a first paint keeps, for classes a head script sets
|
|
29
|
+
head: dialScript, // raw HTML in every page's head, before the stylesheets: dialScript from @weft-js/warp/plugin restores `setDial(…, { persist: true })` before the first paint
|
|
30
|
+
},
|
|
31
|
+
plugins: [warp()], // plugin factories (@weft-js/core/plugin); a plain `{ name, provides }` entry is data only
|
|
32
|
+
slots: {}, // names the winner when two plugins provide one slot
|
|
33
|
+
})
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
| Key | What it holds, and what `weft check` reports |
|
|
37
|
+
|---|---|
|
|
38
|
+
| `scope` | `write`, `read` and `forbid` globs. `forbid` is never compiled or written. `write` will bound `weft fix` (P005, not reported yet) |
|
|
39
|
+
| `tasks` | Project commands for `weft run` and `weft tasks`. P003 duplicate name or command, P004 missing `description` or `sideEffects` |
|
|
40
|
+
| `environments` | Named places the app runs, with a `url` |
|
|
41
|
+
| `test` | `include` (default `**/*.{test,spec}.*`), `lanes`, and `gate: { mutationBudgetSec, maxMutants }` for `weft test --gate` |
|
|
42
|
+
| `check` | `include` and `exclude` globs; `design: false` turns off the D-codes, `routes: false` the route pass (H001–H007, H011–H016, which run the app's loaders once), `props: false` or `'manual'` the prop inference (`weft docs props`) |
|
|
43
|
+
| `plugins` | P116 duplicate name; P100 two entries provide one `one` slot; P115 bad options; P117 impure `contribute()`; P111 code outside the plugin's namespace; P112 AGENTS section over 1 KB; P113 token-namespace or catalog collision; and the codes plugins report themselves (e.g. warp's D010) |
|
|
44
|
+
| `slots` | Which plugin wins a slot two provide. P008 unknown slot id, P009 winner not in `plugins` or not providing the slot |
|
|
45
|
+
| `site` | Page metadata defaults and CSS delivery. P007 `url` not absolute http(s), `title` without `%s`, `locale` not `en_US` style; H001 no `url`; H003 no `description`; H004 a title over 60 or a description over 160 (warnings) |
|
|
46
|
+
| `site.card` | The generated share card's colours and fonts. P007 a colour that isn't `#rrggbb`, a font family that isn't a name. `weft docs meta` has the rest |
|
|
47
|
+
| `site.headers` | Response headers by route pattern |
|
|
48
|
+
| `runtime` | `node`, `bun` or `deno`: what `weft build` targets (`weft docs start`) |
|
|
49
|
+
| `hydration` | `'lazy'` (default): islands load when needed; `'partial'`: every island loads with the page; `'full'`: every route hydrates whole |
|
|
50
|
+
| `rust` | `unions` and `services` for Rust bodies (`weft docs rust`) |
|
|
51
|
+
| `live` | `targets` for a live query's `verify` (`weft docs start`) |
|
|
52
|
+
| `trust` | `'enforce'` (default) or `'warn'`: see P109 and P110 under Plugins |
|
|
53
|
+
| anything else | P007: unknown key or wrong type, with the path |
|
|
54
|
+
|
|
55
|
+
`package.json` scripts may only alias `weft …` (P002). Project commands live in `tasks`.
|
|
56
|
+
|
|
57
|
+
## Plugins
|
|
58
|
+
|
|
59
|
+
A plugin is a factory made with `definePlugin` from `@weft-js/core/plugin`. Its pure `contribute(options, env)` returns what it
|
|
60
|
+
adds: slot providers, `tokens` (`--<namespace>-*`), one AGENTS.md section (≤ 1 KB), catalog entries and diagnostic
|
|
61
|
+
codes in its namespace. Components stay normal imports.
|
|
62
|
+
|
|
63
|
+
- `weft plugins --json` lists plugins (sorted by name), the slots they provide and bind, permissions and codes.
|
|
64
|
+
- `weft plugins explain <slot> --json` shows config → plugin → module → `.weft/slots/<id>.ts`.
|
|
65
|
+
- `weft plugins sync` (and `weft check`) writes `.weft/slots/<id>.ts`, `.weft/slots.d.ts` and `.weft/tokens/<ns>.css`.
|
|
66
|
+
Import a slot as `weft:slot/<id>`; its provider loads lazily through `load()`. Unbound: `load()` throws P101.
|
|
67
|
+
- Output is the same for any order of `plugins: []`; array order never decides a binding (P100).
|
|
68
|
+
|
|
69
|
+
### Permissions and the lock
|
|
70
|
+
|
|
71
|
+
A plugin declares `permissions` (`fs: { read, write }`, `net`, `env`, `spawn`, `native`). `.weft/plugins.lock.json`
|
|
72
|
+
records what you approved, plus a hash of each installed plugin's files. Commit it.
|
|
73
|
+
|
|
74
|
+
- **P109** stops `weft check`, `weft dev` and `weft build` when a plugin asks for a permission the lock doesn't give it,
|
|
75
|
+
or the audit finds it using one it didn't declare.
|
|
76
|
+
- **P110** stops them when an installed plugin's code isn't what the lock recorded, or the lock can't be read.
|
|
77
|
+
- `weft plugins audit --json` shows every plugin against the lock: permissions added and removed, undeclared use with
|
|
78
|
+
file and line, and the dependencies it didn't read. It exits 1 if anything is blocked.
|
|
79
|
+
- `weft plugins approve <name…>` records those plugins, and `--all` records every plugin and drops ones you removed.
|
|
80
|
+
With neither it shows what's waiting and writes nothing. If `.gitignore` hides the lock, it fixes that line. In a
|
|
81
|
+
monorepo, check the ignore file at the repository root yourself.
|
|
82
|
+
- `weft add` never approves. It tells you what the plugin asks for, and the next `weft check` stops until you do.
|
|
83
|
+
- A plugin that asks for nothing and passes the audit needs no approval, and `approve` pins it if you want that.
|
|
84
|
+
- `npm create @weft-js` approves the plugins you pick. It can't hash code before the install, so run
|
|
85
|
+
`weft plugins approve --all` once to pin them.
|
|
86
|
+
- `trust: 'warn'` in the config turns P109 and P110 into warnings, for a project that vets dependencies some other way.
|
|
87
|
+
|
|
88
|
+
This shows you changes; it isn't a sandbox. `contribute()` runs when the config loads, before any of this. The audit
|
|
89
|
+
reads the source of the plugin's own package from its entry and its build and server modules, so it won't see code
|
|
90
|
+
that hides itself or code in dependencies (your package manager's lockfile pins those). A workspace package has no
|
|
91
|
+
fixed code to hash, so it's audited on every check instead. A plugin defined in the config file is your own code:
|
|
92
|
+
only its permissions are compared. A plugin is matched to its package by name, so one that claims another's name is
|
|
93
|
+
hashed as that package.
|
package/docs/deploy.md
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# Deploy: from `weft build` to a live URL
|
|
2
|
+
|
|
3
|
+
weft reads what the app uses and says which hosts can run it, before a build and before an account exists. Then it
|
|
4
|
+
writes the host's files and a workflow, and `weft deploy` runs the host's own CLI and reports the URL.
|
|
5
|
+
|
|
6
|
+
```bash
|
|
7
|
+
weft deploy hosts # which hosts fit, why not, what each costs, and the suggestion
|
|
8
|
+
weft deploy init # the suggested host's files, a GitHub workflow, and `deploy` in weft.config.ts
|
|
9
|
+
weft deploy # a dry run: the build and the exact commands --yes would run
|
|
10
|
+
weft deploy --yes # a preview deploy; add --prod for production
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
`weft deploy` without `--yes` runs nothing. An agent runs the dry run on its own, and asks before `--yes`: a deploy
|
|
14
|
+
publishes, and a host may bill for it. Nothing in weft creates a site, an app, a volume or a DNS record; `init` prints the
|
|
15
|
+
one command per host that does, for you to run.
|
|
16
|
+
|
|
17
|
+
## Which host
|
|
18
|
+
|
|
19
|
+
| The app uses | Netlify / Vercel | Cloudflare | Fly / Render |
|
|
20
|
+
|---|---|---|---|
|
|
21
|
+
| pages, queries, mutations, forms, `stream()` | ✓ | ✓ | ✓ |
|
|
22
|
+
| `channel`, `sharedState`, `realtime({ share })` | ✗ E001 | ✓ (Durable Objects) | ✓ |
|
|
23
|
+
| `.live({ required })`, `remote()` sidecars | ✗ E002 | ✗ E002 | ✓ |
|
|
24
|
+
| a SQLite file, media uploads | ✗ E003 | ✗ E003 | with a volume or disk (billed) |
|
|
25
|
+
| `changes` without an external listener | degraded (R014) | degraded | ✓ |
|
|
26
|
+
|
|
27
|
+
The suggestion: Netlify when nothing needs a server that stays up (a CDN with functions is the cheapest and fastest
|
|
28
|
+
host), Cloudflare when channels are all that need one, Fly when data must last on disk, else Fly or Render.
|
|
29
|
+
|
|
30
|
+
A function host blocked by a channel has a third way, the split: pages on the function host, realtime on a small server.
|
|
31
|
+
It needs a second origin and a cookie both can read, and is designed (research 46 §10), not built.
|
|
32
|
+
|
|
33
|
+
## `deploy` in weft.config.ts
|
|
34
|
+
|
|
35
|
+
```ts
|
|
36
|
+
deploy: {
|
|
37
|
+
target: 'fly', // netlify | vercel | cloudflare | fly | render
|
|
38
|
+
fly: { app: 'shop', region: 'iad', volume: { name: 'data', sizeGb: 1 } },
|
|
39
|
+
ci: 'github', // the workflow; false for none
|
|
40
|
+
branch: 'main',
|
|
41
|
+
cdn: { proxy: 'cloudflare', images: { provider: 'cloudinary', cloud: 'shop' } },
|
|
42
|
+
}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Names, regions and sizes only. Every token comes from the environment, and one written into the config, a host file or a
|
|
46
|
+
workflow is **E008**. With `deploy.target` set, `weft build` builds for it, and `weft check` reports E001–E003 for it.
|
|
47
|
+
|
|
48
|
+
A volume or disk mounts at `/app/.data`: SQLite's default `file:.data/app.db` is already there, and uploads go to
|
|
49
|
+
`.data/media` (`WEFT_MEDIA_DIR`).
|
|
50
|
+
|
|
51
|
+
## What each target writes and runs
|
|
52
|
+
|
|
53
|
+
| Target | Files | `weft deploy --yes` runs | Reads |
|
|
54
|
+
|---|---|---|---|
|
|
55
|
+
| netlify | `netlify.toml` | `weft build --preset netlify`, `netlify deploy --dir dist --functions .netlify/functions-internal --json` | `NETLIFY_AUTH_TOKEN`, `NETLIFY_SITE_ID` |
|
|
56
|
+
| vercel | `vercel.json` | `weft build --preset vercel`, `vercel deploy --prebuilt` | `VERCEL_TOKEN`, `VERCEL_ORG_ID`, `VERCEL_PROJECT_ID` |
|
|
57
|
+
| cloudflare | `wrangler.toml` | `weft build --preset cloudflare_durable`, `wrangler deploy` in `.output/` | `CLOUDFLARE_API_TOKEN`, `CLOUDFLARE_ACCOUNT_ID` |
|
|
58
|
+
| fly | `fly.toml`, `Dockerfile`, `.dockerignore` | `flyctl deploy --remote-only` (Fly builds the image) | `FLY_API_TOKEN` |
|
|
59
|
+
| render | `render.yaml`, `Dockerfile`, `.dockerignore` | the deploy hook (Render builds the pushed commit) | `RENDER_DEPLOY_HOOK_URL` |
|
|
60
|
+
|
|
61
|
+
Every target also gets `.github/workflows/weft-deploy.yml`: on push to `branch`, install, `weft check`, then `weft deploy
|
|
62
|
+
--yes --prod`; on a pull request, a preview where the host has them. Add the variables as repository secrets (`gh secret
|
|
63
|
+
set NAME`). Netlify, Vercel and Cloudflare deploy what was built, so build where the host runs: the workflow builds on
|
|
64
|
+
Linux, and a native addon built on a Mac is **E004**.
|
|
65
|
+
|
|
66
|
+
Each written file starts with `# weft deploy init <target>, sha <hash>`. Change the config and `weft check` reports the
|
|
67
|
+
file as stale (**E006**); `init` rewrites it. A file you edited keeps its old hash, and `init` leaves it alone unless
|
|
68
|
+
`--force`.
|
|
69
|
+
|
|
70
|
+
## CDN and media
|
|
71
|
+
|
|
72
|
+
- **Cloudflare in front of a server host** (`cdn.proxy: 'cloudflare'`): proxy the domain to the Fly or Render origin.
|
|
73
|
+
Hashed assets are already `immutable`, and pages are `private` unless a route caches, so the proxy can't serve one
|
|
74
|
+
user's page to another. E010: `site.url` is still the host's own domain. E011: a proxy in front of a CDN host.
|
|
75
|
+
- **Cloudinary for uploads** (`cdn.images`): checked (E012 without `@weft-js/plugin-media`, E013 without a cloud or with
|
|
76
|
+
two), not yet wired: uploads still go to the media server's disk (E014). `?media` imports never need it: the build
|
|
77
|
+
already makes sized AVIF and WebP files that any CDN serves.
|
|
78
|
+
|
|
79
|
+
## Codes
|
|
80
|
+
|
|
81
|
+
E001 sockets on a function host, E002 a held process or a sidecar, E003 data on a disk that doesn't last, E004 a native
|
|
82
|
+
addon deployed from another OS, E005 no target (with the suggestion), E006 a stale host file, E007 an unset variable,
|
|
83
|
+
E008 a written credential, E009 the memory bus on many instances, E010–E014 CDN and media. `weft docs E003` explains one.
|
package/docs/design.md
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# Design: tokens, primitives, D-codes
|
|
2
|
+
|
|
3
|
+
An app with warp has one design language: `@weft-js/warp`'s primitives and components, on warp's tokens. Name what a thing
|
|
4
|
+
is, and the look follows the theme, the dials and the density.
|
|
5
|
+
|
|
6
|
+
```tsx
|
|
7
|
+
import { Button, Card, Heading, Input, Row, Stack, Text } from '@weft-js/warp'
|
|
8
|
+
|
|
9
|
+
export function Billing() {
|
|
10
|
+
return (
|
|
11
|
+
<Stack as="section" gap="section">
|
|
12
|
+
<Heading level={1} role="title">Billing</Heading>
|
|
13
|
+
<Card title="Card on file">
|
|
14
|
+
<Stack gap="related">
|
|
15
|
+
<Text subtle>Visa ending 4242</Text>
|
|
16
|
+
<Input label="Billing email" type="email" />
|
|
17
|
+
<Row gap="tight" justify="end">
|
|
18
|
+
<Button emphasis="ghost">Cancel</Button>
|
|
19
|
+
<Button>Save</Button>
|
|
20
|
+
</Row>
|
|
21
|
+
</Stack>
|
|
22
|
+
</Card>
|
|
23
|
+
</Stack>
|
|
24
|
+
)
|
|
25
|
+
}
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Vocabulary
|
|
29
|
+
|
|
30
|
+
| Thing | Use |
|
|
31
|
+
|---|---|
|
|
32
|
+
| Vertical flow | `Stack gap align divided` |
|
|
33
|
+
| Horizontal cluster | `Row gap justify align wrap reflow` (`reflow` stacks it below a width step) |
|
|
34
|
+
| Columns | `Grid min cols gap` |
|
|
35
|
+
| Side + main | `Split side width at gap` |
|
|
36
|
+
| Measured column | `Center max gutter intrinsic` |
|
|
37
|
+
| Full-screen, one centred child | `Cover`, with `data-part="center"` on the child |
|
|
38
|
+
| Media box | `Frame ratio` |
|
|
39
|
+
| Push to the far end | `Spacer` |
|
|
40
|
+
| Sunken region | `Inset pad` |
|
|
41
|
+
| Raised box | `Card` (warp's surface: `<Surface>` is weft's canvas) |
|
|
42
|
+
| Heading | `Heading level role`: `level` is the outline and picks the look (`title` for h1, `heading` for h2 and h3, `label` below); `role` overrides it |
|
|
43
|
+
| Text | `Text role tone subtle faint inline` |
|
|
44
|
+
| Link | `Link href subtle external`; a bare `<a>` still gets warp's colour |
|
|
45
|
+
|
|
46
|
+
`gap`: `none hair tight related group section page`, a relationship and not a size: `related` inside a thing, `group`
|
|
47
|
+
between things, `section` between sections. `as` picks the element: `div section article main header footer nav aside
|
|
48
|
+
form ul ol`. There are no breakpoint props, since boxes answer to their container. `@weft-js/core/ds` exports these names as
|
|
49
|
+
constants and types.
|
|
50
|
+
|
|
51
|
+
`style={{ … }}` and `class` still work; the D-codes below tell you when they leave the system. For a named look of your
|
|
52
|
+
own, use a style group (`weft docs style`), not a class list.
|
|
53
|
+
|
|
54
|
+
## D-codes
|
|
55
|
+
|
|
56
|
+
`weft check` reports them. `weft fix` applies the safe autofixes. A fix that changes an element is a suggested edit in
|
|
57
|
+
`fix.edits`; a raw colour has none, since a token follows the theme and the raw value didn't.
|
|
58
|
+
|
|
59
|
+
Where the app has warp's tokens (`@weft-js/warp` in package.json; headless `@weft-js/warp-core` ships none):
|
|
60
|
+
|
|
61
|
+
| Code | Catches | Fix |
|
|
62
|
+
|---|---|---|
|
|
63
|
+
| D001 | Raw colour, palette class | A token or `tone` |
|
|
64
|
+
| D002 | Spacing off the scale, or a step by hand | Autofix when exactly a step |
|
|
65
|
+
| D003 | Raw type, radius, shadow | `Heading`, `Text`, radius and elevation tokens; autofix for an exact radius |
|
|
66
|
+
| D004 | Layout spelled out on an element | `Stack`, `Row` (suggested edit) |
|
|
67
|
+
| D040 | The style gate (`data-weft="styled"`) on main content or an h1 | A secondary part, or no marker |
|
|
68
|
+
| D005 | A browser-painted control: `<button>`, `<select>`, `<textarea>`, an input type such as `date`, `number` or `range`, or a `title` tooltip | The warp component (suggested edit for a button) |
|
|
69
|
+
| D006 | One heading level at two roles across the app, or a bare `<hN>` beside `<Heading>` | Match the level's main role (suggested edit at each site) |
|
|
70
|
+
| D007 | A bare `<a href>` | `<Link>`, or `Shell`'s `nav` (suggested edit) |
|
|
71
|
+
|
|
72
|
+
In every app:
|
|
73
|
+
|
|
74
|
+
| Code | Catches | Fix |
|
|
75
|
+
|---|---|---|
|
|
76
|
+
| D011 | A literal colour pair below 4.5:1 | Tokens, or better colours |
|
|
77
|
+
| D030 | A form control with no label | `<label>`, `aria-label`, warp's `label` |
|
|
78
|
+
| D031 | A button or link with no name | Text, `aria-label`, warp's `label` |
|
|
79
|
+
| D032 | `<img>` with no `alt` | Describe it; `alt=""` for decoration |
|
|
80
|
+
| D033 | `onClick` on a non-interactive element | `<button type="button">` (suggested edit) |
|
|
81
|
+
| D034 | A control inside a control | One control |
|
|
82
|
+
| D035 | A heading level that jumps | The next level |
|
|
83
|
+
| D036 | Focus ring removed | Remove the override, or add a `focus-visible:` style |
|
|
84
|
+
| D037 | Positive `tabindex` | `0` (autofix) |
|
|
85
|
+
| D038 | Two solid Buttons side by side | One primary action per region |
|
|
86
|
+
| D039 | `<html>` with no `lang` | Set it |
|
|
87
|
+
|
|
88
|
+
D010 is warp's contrast gate on the seed and tokens. D020–D025 are style groups.
|
|
89
|
+
|
|
90
|
+
`// weft-allow D001: <reason>` on the line above keeps one site; the reason is required. `check: { design: false }` in
|
|
91
|
+
the config turns these off.
|
|
92
|
+
|
|
93
|
+
## Tokens
|
|
94
|
+
|
|
95
|
+
The theme is `warp({ seed, dials, radius, fonts, tokens })`: every value goes through the contrast check. `weft tokens`
|
|
96
|
+
writes the resolved tokens to `.weft/tokens/` as DTCG 2025.10, one file per theme × expression plus a resolver, for
|
|
97
|
+
Figma, Style Dictionary or Terrazzo.
|
package/docs/fonts.md
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# Fonts
|
|
2
|
+
|
|
3
|
+
`@weft-js/text` is the one place an app's fonts are declared. `weft add text` installs it; then:
|
|
4
|
+
|
|
5
|
+
```ts
|
|
6
|
+
import { defineConfig } from '@weft-js/core/config'
|
|
7
|
+
import { text } from '@weft-js/text'
|
|
8
|
+
|
|
9
|
+
export default defineConfig({
|
|
10
|
+
plugins: [
|
|
11
|
+
text({
|
|
12
|
+
fonts: {
|
|
13
|
+
body: { family: 'Inter', src: 'assets/fonts/inter.woff2', fallback: 'Arial', axes: { wght: [100, 900] } },
|
|
14
|
+
display: { family: 'Fraunces', src: 'assets/fonts/fraunces.woff2', fallback: 'Georgia', weight: 700 },
|
|
15
|
+
},
|
|
16
|
+
}),
|
|
17
|
+
],
|
|
18
|
+
})
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
`src` is relative to the project root (woff2, woff, ttf or otf). `fallback` is a system face: Arial, Helvetica, Times New
|
|
22
|
+
Roman, Georgia, Courier New, Verdana. The keys are roles of your choosing; `body` and `display` are preloaded unless a
|
|
23
|
+
face sets `preload: false`, others only with `preload: true`. Other options: `display` (`swap`, `optional`, `fallback`),
|
|
24
|
+
`weight`, `style`, `axes`, and beside `fonts`, `subsets` (default `['latin']`) and `trim` (default `true`).
|
|
25
|
+
|
|
26
|
+
## What the build does
|
|
27
|
+
|
|
28
|
+
- Subsets each face per `unicode-range` to hashed woff2 files.
|
|
29
|
+
- Writes the `@font-face` rules, and a `"<Family> Fallback"` face matched to the font's metrics, so the swap doesn't shift
|
|
30
|
+
layout.
|
|
31
|
+
- Adds the preload links to the page's head, before the first byte.
|
|
32
|
+
- Trims each face, in a build only. Font bytes are what hold back the largest paint on a text hero, so nothing here needs
|
|
33
|
+
turning on, and neither cut changes how the page looks or which characters it can show:
|
|
34
|
+
- **Axes.** A custom axis (`SOFT`, `WONK`, `GRAD`…) is pinned to the one value your `font-variation-settings` give it, or
|
|
35
|
+
to the font's default if the source never sets it. An axis set to two values stays. A value weft can't read from the
|
|
36
|
+
source (`var()`, a template, an expression) keeps every axis. `wght`, `wdth`, `opsz`, `slnt` and `ital` are never
|
|
37
|
+
pinned: CSS drives them. A pinned value applies to every use of the face, so a face used both with and without the
|
|
38
|
+
setting needs the axis declared.
|
|
39
|
+
- **Characters.** `latin` becomes two files. The core is preloaded: ASCII, common punctuation, and every other latin
|
|
40
|
+
character in your source. The rest is fetched through `unicode-range` only by a page that shows one of those
|
|
41
|
+
characters, so text from a database still gets the face, after a fallback-matched swap.
|
|
42
|
+
|
|
43
|
+
Keep one axis by naming it in `axes`, such as `axes: { SOFT: [0, 100] }`, for values set at runtime or by a dependency's CSS
|
|
44
|
+
(only your own source is read). `trim: false` turns all of it off. `weft dev` always serves whole faces, so an axis you are
|
|
45
|
+
editing responds.
|
|
46
|
+
|
|
47
|
+
**There is nothing to import.** `@weft-js/core/vite` loads the generated CSS in the client entry (Start) or the page (an HTML app).
|
|
48
|
+
`import 'weft:fonts'` still works and changes nothing. `import { preloads } from 'weft:fonts/preloads'` gives the preload
|
|
49
|
+
list to a custom server.
|
|
50
|
+
|
|
51
|
+
## Using a face
|
|
52
|
+
|
|
53
|
+
The build declares faces; it doesn't apply them. Name one in your CSS, with its fallback second:
|
|
54
|
+
|
|
55
|
+
```css
|
|
56
|
+
body { font-family: "Inter", "Inter Fallback", sans-serif; }
|
|
57
|
+
h1 { font-family: "Fraunces", "Fraunces Fallback", Georgia, serif; }
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Diagnostics
|
|
61
|
+
|
|
62
|
+
| Code | Catches |
|
|
63
|
+
|---|---|
|
|
64
|
+
| X020 | A hand-written `@font-face` with no `"<Family> Fallback"` face, so the swap shifts layout: declare it in `text({ fonts })` |
|
|
65
|
+
| X021 | `font-display: block` or `auto`, which hide text while the font loads |
|
|
66
|
+
| X022 | A hand-written `@font-face` with no `unicode-range`, or the font build over budget: 100 KB of preloads or 1.5 KB of font CSS (brotli). Preload fewer faces or use fewer weights |
|
|
67
|
+
| X023 | A preload no `@font-face` uses |
|
|
68
|
+
|
|
69
|
+
The plugin also provides `text.measure`, line breaking for canvas text and exact row heights. It loads only when something
|
|
70
|
+
measures; `measure: 'off'` removes it. See `weft docs surface`.
|
package/docs/icons.md
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# Icons
|
|
2
|
+
|
|
3
|
+
`@weft-js/warp-icons` is weft's own set: 651 original icons in warp's three expression tiers. Outline at quiet and balanced, pixel art on a 16×16 grid at vivid. Each has two forms, hollow (the default) and solid, drawn apart in every tier and shaded with dither.
|
|
4
|
+
|
|
5
|
+
`weft add icons` installs it and adds `icons()` to the config. warp already depends on it.
|
|
6
|
+
|
|
7
|
+
## Using one
|
|
8
|
+
|
|
9
|
+
Three forms, and all three build to the same bytes:
|
|
10
|
+
|
|
11
|
+
```tsx
|
|
12
|
+
import { CheckIcon } from '@weft-js/warp-icons'
|
|
13
|
+
<CheckIcon />
|
|
14
|
+
|
|
15
|
+
import Icons from '@weft-js/warp-icons' // or: import * as Icons
|
|
16
|
+
<Icons.Check />
|
|
17
|
+
|
|
18
|
+
import { Icon } from '@weft-js/warp' // or: '@weft-js/warp-icons/icon'
|
|
19
|
+
<Icon name="check" />
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Props on any of them:
|
|
23
|
+
|
|
24
|
+
```tsx
|
|
25
|
+
<CheckIcon /> // decorative: aria-hidden, focusable="false"
|
|
26
|
+
<CheckIcon label="Saved" /> // role="img" with that name
|
|
27
|
+
<CheckIcon size={24} /> // otherwise warp's icon sizes apply
|
|
28
|
+
<CheckIcon tier="vivid" /> // this tier whatever the page's expression is
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
An icon you don't use costs nothing. One adds about 164 B of brotli with all tiers in. `sideEffects` lists only CSS, so a named import ships that icon.
|
|
32
|
+
|
|
33
|
+
## Names and variants
|
|
34
|
+
|
|
35
|
+
One scheme, generated from the art with its types:
|
|
36
|
+
|
|
37
|
+
| Export | Pack | `<Icon>` | What it is |
|
|
38
|
+
|---|---|---|---|
|
|
39
|
+
| `BellIcon` | `Icons.Bell` | `name="bell"` | hollow, the default |
|
|
40
|
+
| `BellIconHollow` | `Icons.BellHollow` | | the same, by name |
|
|
41
|
+
| `BellIconSolid` | `Icons.BellSolid` | `solid` | the solid form |
|
|
42
|
+
| `BellIconAction` | `Icons.BellAction` | `action` | has a built-in animation |
|
|
43
|
+
|
|
44
|
+
The tier is a prop, never a name. An open mark, like an arrow, has a notched badge as its solid, with the mark cut out. Pick it on the sheet: `npm run sheet` in the package writes `contact-sheet.html`, with tier, variant, size, theme and search. `dither-sheet.html` compares the treatments.
|
|
45
|
+
|
|
46
|
+
## A wrong name
|
|
47
|
+
|
|
48
|
+
`<Icon name>` is typed, so a bad name is a type error. Untyped code gets the same from the compiler and from `weft check`:
|
|
49
|
+
|
|
50
|
+
```
|
|
51
|
+
I001 no icon 'chekc', did you mean 'check'?
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
It fails the build, and so does a variant an icon doesn't have.
|
|
55
|
+
|
|
56
|
+
## Names that aren't literal
|
|
57
|
+
|
|
58
|
+
The build follows a name through variables, props, conditionals, maps and component boundaries, and ships what it finds. A name it can't follow, such as one from an API, is legal. `weft check` reports it as I002 with the file and the cost (I003 if the pack itself is passed around). That use then gets one of two things:
|
|
59
|
+
|
|
60
|
+
- **The pack, as its own chunk.** The default. About 111 KB of brotli for all 651 with every tier in, loaded only by a page that reaches it. A page stays about 1.6 KB bigger than one with no dynamic name.
|
|
61
|
+
- **Single icons from a URL.** `icons({ source: 'https://cdn.example.com/icons/' })`. Nothing of the pack is built; each icon is fetched when first drawn.
|
|
62
|
+
|
|
63
|
+
`weft icons publish <dir>` writes every icon and variant as static `.svg` files, with a `manifest.json`, for that URL. It writes files and uploads nothing. Action icons aren't published.
|
|
64
|
+
|
|
65
|
+
A remote icon renders an empty `<svg data-icon data-loading>` of the right size until it arrives, then swaps with no layout shift, and carries `data-failed` if the fetch fails. It stays empty on the server, so SSR needs no network. Elements are allowlisted before they are inserted. A Content-Security-Policy needs that origin in `connect-src`.
|
|
66
|
+
|
|
67
|
+
## Actions
|
|
68
|
+
|
|
69
|
+
Five icons have a built-in animation: `trash` (the lid lifts), `bell` (it rings), `check` (it draws on), `refresh` (it spins), `copy` (the sheet stacks).
|
|
70
|
+
|
|
71
|
+
```tsx
|
|
72
|
+
<Icon name="trash" action /> // hover, focus and press, on the icon or its parent
|
|
73
|
+
<Icon name="refresh" action="on" /> // now; refresh keeps spinning
|
|
74
|
+
<TrashIconAction action="press" /> // 'hover' | 'focus' | 'press' | 'on'
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
It's CSS alone, and each one's stylesheet ships only if that icon is used (about 230 B of CSS and 50 B of JS, brotli). Outlines ease; pixels move in whole cells, in frames. warp's motion dial and `prefers-reduced-motion` turn it off. An action on another icon is I001.
|
|
78
|
+
|
|
79
|
+
## Tiers
|
|
80
|
+
|
|
81
|
+
With no options each icon carries its tiers, and CSS shows the one for the nearest `data-expression`. Switching needs no script, doesn't flash and doesn't change the box. A nested `data-expression` is honoured. The build keeps only the tiers your config and `data-expression` attributes name.
|
|
82
|
+
|
|
83
|
+
To compile one tier in, with no switch, name it in the config:
|
|
84
|
+
|
|
85
|
+
<!-- example: skip @weft-js/warp-icons/plugin has no type declarations yet (TS7016 under strict) -->
|
|
86
|
+
```ts
|
|
87
|
+
import { icons } from '@weft-js/warp-icons/plugin'
|
|
88
|
+
|
|
89
|
+
export default { plugins: [icons({ tier: 'vivid' })] }
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
`tier` is `quiet`, `balanced` or `vivid`. warp's own icons follow it too. After that a page's `data-expression` no longer changes icons: run `weft plugins sync` if you change it.
|
|
93
|
+
|
|
94
|
+
## Pixel sizes
|
|
95
|
+
|
|
96
|
+
The pixel tier is exact at 16, 32 and 48. At 20 and 24 a 1× screen rounds cells unevenly; a 2× screen draws 24 evenly. Prefer 16 or 32 at vivid.
|
|
97
|
+
|
|
98
|
+
## Styling
|
|
99
|
+
|
|
100
|
+
A solid cuts its marks with `--warp-icon-knock`, which defaults to `Canvas`. On a coloured surface, like a solid button, set it to that surface's colour.
|
|
101
|
+
|
|
102
|
+
Icons are `currentColor`. The weft stroke, the one that says what the icon means, is `[data-w]`, so `svg [data-w] { stroke: var(--warp-color-accent-text) }` colours it. Its weight is `--warp-icon-weft`.
|