@weft-js/cli 0.1.1 → 0.1.3
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 +534 -54
- package/bin/weft.mjs +8 -1
- package/docs/async.md +16 -6
- package/docs/browser.md +22 -8
- package/docs/config.md +39 -9
- package/docs/core.md +30 -0
- package/docs/deploy.md +33 -8
- package/docs/design.md +12 -3
- package/docs/entrypoints.md +79 -0
- package/docs/fonts.md +12 -2
- package/docs/icons.md +13 -7
- package/docs/lists.md +3 -1
- package/docs/meta.md +13 -3
- package/docs/plugins.md +32 -9
- package/docs/props.md +13 -2
- package/docs/reactivity.md +26 -0
- package/docs/rules.md +19 -12
- package/docs/rust.md +47 -17
- package/docs/start-short.md +3 -2
- package/docs/start.md +226 -52
- package/docs/state.md +33 -7
- package/docs/style.md +31 -8
- package/docs/surface.md +61 -15
- package/docs/testing.md +15 -5
- package/docs/theme.md +182 -0
- package/docs/ui-state.md +19 -6
- package/docs/verify.md +13 -0
- package/package.json +13 -4
- package/src/add/config.mjs +93 -0
- package/src/add/errors.mjs +2 -0
- package/src/add/setup.mjs +88 -0
- package/src/add-rust.mjs +82 -27
- package/src/add.mjs +209 -181
- package/src/agents-md.mjs +144 -43
- package/src/alias.mjs +16 -5
- package/src/audit/page.mjs +313 -0
- package/src/audit/report.mjs +69 -0
- package/src/audit/utils.mjs +5 -0
- package/src/audit.mjs +159 -206
- package/src/browser/cli.mjs +24 -5
- package/src/browser/client.mjs +189 -25
- package/src/browser/commands.mjs +166 -51
- package/src/catalog/audit.mjs +129 -0
- package/src/catalog/auth.mjs +40 -0
- package/src/catalog/compiler.mjs +337 -0
- package/src/catalog/contracts.mjs +18 -0
- package/src/catalog/database.mjs +52 -0
- package/src/catalog/deploy.mjs +117 -0
- package/src/catalog/design.mjs +159 -0
- package/src/catalog/forms.mjs +25 -0
- package/src/catalog/hydration.mjs +29 -0
- package/src/catalog/icons.mjs +24 -0
- package/src/catalog/media.mjs +163 -0
- package/src/catalog/metadata.mjs +116 -0
- package/src/catalog/plugins.mjs +228 -0
- package/src/catalog/realtime.mjs +195 -0
- package/src/catalog/rust.mjs +75 -0
- package/src/catalog/server-functions.mjs +92 -0
- package/src/catalog/state.mjs +39 -0
- package/src/catalog/style-groups.mjs +54 -0
- package/src/catalog/tests.mjs +127 -0
- package/src/catalog/text.mjs +200 -0
- package/src/catalog/ui-state.mjs +42 -0
- package/src/catalog.mjs +67 -460
- package/src/check/runtime.mjs +280 -0
- package/src/check.mjs +198 -208
- package/src/cli/authoring.mjs +213 -0
- package/src/cli/project.mjs +302 -0
- package/src/cli.mjs +222 -250
- package/src/config.mjs +1 -1
- package/src/contributions.mjs +75 -25
- package/src/db-layer.mjs +13 -5
- package/src/deploy/cdn.mjs +94 -16
- package/src/deploy/files.mjs +100 -21
- package/src/deploy/fit.mjs +127 -23
- package/src/deploy/hosts.mjs +90 -20
- package/src/deploy/index/cli.mjs +20 -0
- package/src/deploy/index/environment.mjs +69 -0
- package/src/deploy/index/steps.mjs +87 -0
- package/src/deploy/index.mjs +228 -114
- package/src/deploy/needs.mjs +88 -31
- package/src/design-checks/accessible-names.mjs +62 -0
- package/src/design-checks/contrast.mjs +43 -0
- package/src/design-checks/csp.mjs +60 -0
- package/src/design-checks/files.mjs +59 -0
- package/src/design-checks/heading-links.mjs +61 -0
- package/src/design-checks/interaction.mjs +65 -0
- package/src/design-checks/jsx.mjs +67 -0
- package/src/design-checks/layout-values.mjs +68 -0
- package/src/design-checks/native-controls.mjs +48 -0
- package/src/design-checks/outline.mjs +39 -0
- package/src/design-checks/reporter.mjs +37 -0
- package/src/design-checks/rules.mjs +274 -0
- package/src/design-checks/values.mjs +262 -0
- package/src/design-checks.mjs +10 -400
- package/src/diag.mjs +32 -8
- package/src/docs.mjs +138 -43
- package/src/doctor.mjs +20 -8
- package/src/files.mjs +18 -5
- package/src/fix.mjs +10 -2
- package/src/gate/after.mjs +40 -0
- package/src/gate/before.mjs +54 -0
- package/src/gate/classify.mjs +72 -0
- package/src/gate/mutation.mjs +209 -0
- package/src/gate/resolve-cases.mjs +22 -0
- package/src/gate/result.mjs +63 -0
- package/src/gate/run.mjs +75 -0
- package/src/gate.mjs +104 -202
- package/src/infer/box-props.cjs +140 -0
- package/src/infer/core-boxes.cjs +46 -0
- package/src/infer/derived-writes.cjs +95 -0
- package/src/infer/engine/checks.cjs +193 -0
- package/src/infer/engine/flow.cjs +293 -0
- package/src/infer/engine/report.cjs +158 -0
- package/src/infer/engine/utils.cjs +15 -0
- package/src/infer/engine.cjs +108 -368
- package/src/infer/facts/sites.cjs +114 -0
- package/src/infer/facts.cjs +210 -117
- package/src/infer/hosts.cjs +69 -22
- package/src/infer/lsp.mjs +149 -40
- package/src/infer/session.cjs +73 -17
- package/src/infer/shadow.cjs +13 -5
- package/src/infer/worker.cjs +57 -9
- package/src/jsx-scan.mjs +63 -21
- package/src/load-config/schema.mjs +281 -0
- package/src/load-config.mjs +137 -138
- package/src/map.mjs +93 -26
- package/src/mcp/browser-tools.mjs +155 -0
- package/src/mcp.mjs +147 -69
- package/src/meta-table.mjs +43 -8
- package/src/mutate.mjs +120 -36
- package/src/plugin-checks.mjs +52 -23
- package/src/plugins.mjs +125 -39
- package/src/project.mjs +170 -79
- package/src/props-checks.mjs +24 -6
- package/src/props-listener.mjs +27 -9
- package/src/react-checks.mjs +35 -4
- package/src/server-state-checks.mjs +34 -0
- package/src/share-checks.mjs +121 -37
- package/src/site.mjs +33 -6
- package/src/slots.mjs +104 -19
- package/src/start/build.mjs +116 -0
- package/src/start/routes.mjs +38 -0
- package/src/start.mjs +193 -95
- package/src/state-checks/core-state.mjs +223 -0
- package/src/state-checks/modules.mjs +17 -0
- package/src/state-checks/scan.mjs +232 -0
- package/src/state-checks/server-graph.mjs +65 -0
- package/src/state-checks.mjs +109 -133
- package/src/style/find.mjs +121 -0
- package/src/style.mjs +133 -127
- package/src/surface-checks.mjs +212 -46
- package/src/tailwind.mjs +5 -4
- package/src/tasks.mjs +189 -27
- package/src/test-rules/context.mjs +109 -0
- package/src/test-rules/file-rules.mjs +279 -0
- package/src/test-rules/rule-helpers.mjs +244 -0
- package/src/test-rules/syntax.mjs +223 -0
- package/src/test-rules/test-rules.mjs +127 -0
- package/src/testdiff.mjs +94 -32
- package/src/tests.mjs +31 -483
- package/src/text-checks.mjs +229 -69
- package/src/tokens.mjs +12 -4
- package/src/trust/scan.mjs +95 -0
- package/src/trust.mjs +214 -102
- package/src/weft.mjs +41 -9
- package/src/workspace.mjs +96 -19
- package/ts-plugin/context.js +57 -0
- package/ts-plugin/index.js +4 -124
- package/ts-plugin/mapping.js +65 -0
- package/ts-plugin/proxy.js +144 -0
package/docs/state.md
CHANGED
|
@@ -3,14 +3,24 @@
|
|
|
3
3
|
Every reactive function in one place. The rules for using them are `weft docs rules`; props are `weft docs props`.
|
|
4
4
|
|
|
5
5
|
```tsx
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
derived,
|
|
8
|
+
flush,
|
|
9
|
+
onCleanup,
|
|
10
|
+
onMount,
|
|
11
|
+
state,
|
|
12
|
+
watch,
|
|
13
|
+
} from '@weft-js/core'
|
|
7
14
|
|
|
8
15
|
export function Counter() {
|
|
9
16
|
const count = state(0)
|
|
10
17
|
const label = derived(() => `Clicked ${count.value} times`)
|
|
11
18
|
let input: HTMLInputElement | undefined
|
|
12
19
|
|
|
13
|
-
watch(() => label.value, text => {
|
|
20
|
+
watch(() => label.value, text => {
|
|
21
|
+
document.title = text
|
|
22
|
+
return () => (document.title = 'App')
|
|
23
|
+
})
|
|
14
24
|
onMount(() => input?.focus())
|
|
15
25
|
onCleanup(() => console.log('Counter removed'))
|
|
16
26
|
|
|
@@ -21,8 +31,13 @@ export function Counter() {
|
|
|
21
31
|
}
|
|
22
32
|
return (
|
|
23
33
|
<p>
|
|
24
|
-
<button type="button" onClick={() => count.value++}>
|
|
25
|
-
|
|
34
|
+
<button type="button" onClick={() => count.value++}>
|
|
35
|
+
{label.value}
|
|
36
|
+
</button>
|
|
37
|
+
<input
|
|
38
|
+
ref={(n: HTMLInputElement) => (input = n)}
|
|
39
|
+
aria-label="Note"
|
|
40
|
+
/>
|
|
26
41
|
<button type="button" onClick={reset}>Reset</button>
|
|
27
42
|
</p>
|
|
28
43
|
)
|
|
@@ -66,9 +81,20 @@ import { defineState, derived, state } from '@weft-js/core'
|
|
|
66
81
|
import { z } from 'zod'
|
|
67
82
|
|
|
68
83
|
export const cart = defineState('cart', () => {
|
|
69
|
-
const lines = state<Record<string, number>>({}, {
|
|
70
|
-
|
|
71
|
-
|
|
84
|
+
const lines = state<Record<string, number>>({}, {
|
|
85
|
+
persist: {
|
|
86
|
+
in: 'local',
|
|
87
|
+
schema: z.record(z.string(), z.number()),
|
|
88
|
+
version: 1,
|
|
89
|
+
},
|
|
90
|
+
sync: 'tabs',
|
|
91
|
+
})
|
|
92
|
+
const count = derived(() =>
|
|
93
|
+
Object.values(lines.value).reduce((a, b) => a + b, 0),
|
|
94
|
+
)
|
|
95
|
+
const add = (id: string) => lines.set(d => {
|
|
96
|
+
d[id] = (d[id] ?? 0) + 1
|
|
97
|
+
})
|
|
72
98
|
return { lines, count, add }
|
|
73
99
|
}, { history: { limit: 100 } })
|
|
74
100
|
```
|
package/docs/style.md
CHANGED
|
@@ -9,20 +9,43 @@ import { group } from '@weft-js/core/style'
|
|
|
9
9
|
|
|
10
10
|
/** A summary card for one order in a list. */
|
|
11
11
|
export const orderCard = group('order-card', {
|
|
12
|
-
base: {
|
|
13
|
-
|
|
14
|
-
|
|
12
|
+
base: {
|
|
13
|
+
surface: 'raised',
|
|
14
|
+
pad: 'group',
|
|
15
|
+
radius: 'surface',
|
|
16
|
+
layout: 'stack',
|
|
17
|
+
gap: 'related',
|
|
18
|
+
},
|
|
19
|
+
axes: {
|
|
20
|
+
status: {
|
|
21
|
+
open: { tone: 'info' },
|
|
22
|
+
shipped: { tone: 'positive' },
|
|
23
|
+
late: { tone: 'critical' },
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
parts: {
|
|
27
|
+
title: { text: 'title' },
|
|
28
|
+
meta: { text: 'caption', ink: 'subtle' },
|
|
29
|
+
},
|
|
15
30
|
states: { selected: { ring: 'accent' } },
|
|
16
31
|
defaults: { status: 'open' },
|
|
17
32
|
})
|
|
18
33
|
|
|
19
|
-
type Order = {
|
|
34
|
+
type Order = {
|
|
35
|
+
name: string
|
|
36
|
+
date: string
|
|
37
|
+
status: 'open' | 'shipped' | 'late'
|
|
38
|
+
}
|
|
20
39
|
|
|
21
|
-
export function OrderCard(props: {
|
|
40
|
+
export function OrderCard(props: {
|
|
41
|
+
order: Order
|
|
42
|
+
selected: boolean
|
|
43
|
+
}) {
|
|
44
|
+
const { order, selected } = props
|
|
22
45
|
return (
|
|
23
|
-
<article {...orderCard({ status:
|
|
24
|
-
<h3 {...orderCard.title}>{
|
|
25
|
-
<p {...orderCard.meta}>{
|
|
46
|
+
<article {...orderCard({ status: order.status, selected })}>
|
|
47
|
+
<h3 {...orderCard.title}>{order.name}</h3>
|
|
48
|
+
<p {...orderCard.meta}>{order.date}</p>
|
|
26
49
|
</article>
|
|
27
50
|
)
|
|
28
51
|
}
|
package/docs/surface.md
CHANGED
|
@@ -11,19 +11,32 @@ import { z } from 'zod'
|
|
|
11
11
|
export const Bars = defineSurface({
|
|
12
12
|
name: 'bars',
|
|
13
13
|
kind: 'canvas2d', // or 'webgl2'
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
14
|
+
// Or defineSurface<{ data: number[] }, 'canvas2d'>({ ... })
|
|
15
|
+
props: z.object({ data: z.array(z.number()) }),
|
|
16
|
+
// Runs once; props.data is live, so never destructure it.
|
|
17
|
+
setup(s, props) {
|
|
18
|
+
// Repaints on box changes; the canvas is cleared first.
|
|
19
|
+
s.draw(() => {
|
|
20
|
+
const { width, height } = s.size.value // CSS px, live
|
|
21
|
+
// Live theme colours: .css, or .linear for shaders.
|
|
22
|
+
s.ctx.fillStyle = s.tokens.accent.css
|
|
23
|
+
props.data.forEach((v, i) => {
|
|
24
|
+
s.ctx.fillRect(i * 12, height - v, 10, v)
|
|
25
|
+
})
|
|
20
26
|
})
|
|
21
27
|
},
|
|
22
28
|
})
|
|
23
29
|
|
|
24
30
|
export function Page() {
|
|
25
31
|
const rows = state([3, 7, 5])
|
|
26
|
-
return
|
|
32
|
+
return (
|
|
33
|
+
<Surface
|
|
34
|
+
use={Bars}
|
|
35
|
+
data={rows.value}
|
|
36
|
+
aspect="16/9"
|
|
37
|
+
label="Sales by month"
|
|
38
|
+
/>
|
|
39
|
+
)
|
|
27
40
|
}
|
|
28
41
|
```
|
|
29
42
|
|
|
@@ -33,21 +46,43 @@ A control beside the surface needs only a box and a live prop. Nothing re-render
|
|
|
33
46
|
import { state } from '@weft-js/core'
|
|
34
47
|
import { defineSurface, Surface } from '@weft-js/core/surface'
|
|
35
48
|
|
|
36
|
-
|
|
49
|
+
// Name the kind too, so s.ctx is typed.
|
|
50
|
+
export const Stars = defineSurface<{ density: number }, 'canvas2d'>({
|
|
37
51
|
name: 'stars',
|
|
38
52
|
kind: 'canvas2d',
|
|
39
53
|
setup(s, props) {
|
|
40
|
-
|
|
41
|
-
|
|
54
|
+
// Every tick; reading props.density here keeps it live.
|
|
55
|
+
s.frame(({ seconds }) => {
|
|
56
|
+
for (let i = 0; i < props.density; i++) {
|
|
57
|
+
const { width, height } = s.size.value
|
|
58
|
+
const x = (i * 37 + seconds * 20) % width
|
|
59
|
+
const y = (i * 91) % height
|
|
60
|
+
s.ctx.fillRect(x, y, 1, 1)
|
|
61
|
+
}
|
|
42
62
|
})
|
|
43
63
|
},
|
|
44
64
|
})
|
|
45
65
|
|
|
46
66
|
export function Sky() {
|
|
47
67
|
const density = state(100)
|
|
68
|
+
const setDensity = (e: Event & {
|
|
69
|
+
currentTarget: HTMLInputElement
|
|
70
|
+
}) => (density.value = e.currentTarget.valueAsNumber)
|
|
48
71
|
return <>
|
|
49
|
-
<input
|
|
50
|
-
|
|
72
|
+
<input
|
|
73
|
+
type="range"
|
|
74
|
+
min="10"
|
|
75
|
+
max="500"
|
|
76
|
+
value={density.value}
|
|
77
|
+
onInput={setDensity}
|
|
78
|
+
aria-label="Density"
|
|
79
|
+
/>
|
|
80
|
+
<Surface
|
|
81
|
+
use={Stars}
|
|
82
|
+
density={density.value}
|
|
83
|
+
aspect="16/9"
|
|
84
|
+
decorative
|
|
85
|
+
/>
|
|
51
86
|
</>
|
|
52
87
|
}
|
|
53
88
|
```
|
|
@@ -78,7 +113,10 @@ Never `fillText` (X010). `s.text()` keeps a real element over the text, so selec
|
|
|
78
113
|
forced colours and print all work:
|
|
79
114
|
|
|
80
115
|
```tsx
|
|
81
|
-
const score = s.text(() => `Score ${props.score}`, {
|
|
116
|
+
const score = s.text(() => `Score ${props.score}`, {
|
|
117
|
+
x: 8, y: 8, width: 200,
|
|
118
|
+
font: '600 16px Inter', lineHeight: 22,
|
|
119
|
+
})
|
|
82
120
|
```
|
|
83
121
|
|
|
84
122
|
- Options may be a function, for live positions: `() => ({ x: s.size.value.width / 2, ... })`.
|
|
@@ -95,7 +133,13 @@ jsdom has no canvas, so `@weft-js/core/test` gives it a recording one:
|
|
|
95
133
|
|
|
96
134
|
<!-- example: skip imports the Page above from its own file -->
|
|
97
135
|
```tsx
|
|
98
|
-
import {
|
|
136
|
+
import {
|
|
137
|
+
canvasCalls,
|
|
138
|
+
clock,
|
|
139
|
+
fakeMeasure,
|
|
140
|
+
flush,
|
|
141
|
+
render,
|
|
142
|
+
} from '@weft-js/core/test'
|
|
99
143
|
import { expect, test } from 'vitest'
|
|
100
144
|
import { Page } from './Page' // the Bars page above
|
|
101
145
|
|
|
@@ -104,7 +148,9 @@ test('draws a bar per value', async () => {
|
|
|
104
148
|
render(Page, {}, { slots: { 'text.measure': fakeMeasure() } })
|
|
105
149
|
await flush()
|
|
106
150
|
await clock.advance(16) // a frame
|
|
107
|
-
|
|
151
|
+
const canvas = document.querySelector('canvas')!
|
|
152
|
+
const bars = canvasCalls(canvas).filter(c => c.name === 'fillRect')
|
|
153
|
+
expect(bars).toHaveLength(3)
|
|
108
154
|
})
|
|
109
155
|
```
|
|
110
156
|
|
package/docs/testing.md
CHANGED
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
A new app has no test runner. Add one:
|
|
4
4
|
|
|
5
5
|
```bash
|
|
6
|
-
npm install -D vitest jsdom @testing-library/dom
|
|
6
|
+
npm install -D vitest jsdom @testing-library/dom \
|
|
7
|
+
@testing-library/user-event
|
|
7
8
|
```
|
|
8
9
|
|
|
9
10
|
<!-- example: skip @weft-js/core/vite has no type declarations yet (TS7016 under strict) -->
|
|
@@ -12,7 +13,10 @@ npm install -D vitest jsdom @testing-library/dom @testing-library/user-event
|
|
|
12
13
|
import weft from '@weft-js/core/vite'
|
|
13
14
|
import { defineConfig } from 'vitest/config'
|
|
14
15
|
|
|
15
|
-
export default defineConfig({
|
|
16
|
+
export default defineConfig({
|
|
17
|
+
plugins: [weft()],
|
|
18
|
+
test: { environment: 'jsdom' },
|
|
19
|
+
})
|
|
16
20
|
```
|
|
17
21
|
|
|
18
22
|
Then `npx vitest run`. A test:
|
|
@@ -24,14 +28,20 @@ import { expect, test } from 'vitest'
|
|
|
24
28
|
|
|
25
29
|
function Cart() {
|
|
26
30
|
const count = state(0)
|
|
27
|
-
return
|
|
31
|
+
return (
|
|
32
|
+
<button type="button" onClick={() => count.value++}>
|
|
33
|
+
Cart ({count.value})
|
|
34
|
+
</button>
|
|
35
|
+
)
|
|
28
36
|
}
|
|
29
37
|
|
|
30
38
|
test('adding to the cart', async () => {
|
|
31
39
|
render(Cart)
|
|
32
40
|
await user.click(screen.getByRole('button', { name: 'Cart (0)' }))
|
|
33
|
-
expect(screen.getByRole('button', { name: 'Cart (1)' }))
|
|
34
|
-
|
|
41
|
+
expect(screen.getByRole('button', { name: 'Cart (1)' }))
|
|
42
|
+
.toBeTruthy()
|
|
43
|
+
const click = () => user.click(screen.getByRole('button'))
|
|
44
|
+
await expect(click).toTouchAtMost(1, { runs: 2 })
|
|
35
45
|
})
|
|
36
46
|
```
|
|
37
47
|
|
package/docs/theme.md
ADDED
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
# Theme: changing warp's look
|
|
2
|
+
|
|
3
|
+
Three levels, in order. Stop at the first that gets you there; each one keeps the contrast check.
|
|
4
|
+
|
|
5
|
+
1. `warp({ seed, dials, fields, radius, fonts, tokens })` in the weft config. Checked by D010 at the stops `dials` leave.
|
|
6
|
+
For your own names, per-element looks and named themes: `colors`, `system`, `parts`, `themes` (below).
|
|
7
|
+
2. `src/theme.css`, unlayered: `[data-scope]` and `[data-part]` rules beat warp with no `!important`.
|
|
8
|
+
3. Your own component, with warp's tokens.
|
|
9
|
+
|
|
10
|
+
The Theme studio on the docs site (`/components/theme-studio`) edits level 1 with live components and contrast
|
|
11
|
+
ratios, and copies the result as `weft.config.ts`.
|
|
12
|
+
|
|
13
|
+
## warp() options
|
|
14
|
+
|
|
15
|
+
| Option | Does |
|
|
16
|
+
|---|---|
|
|
17
|
+
| `seed.accent` | The hue family: `#rrggbb`, a name from `colors`, or `{ hue, chroma }`. Each theme solves its own lightness, and `weft build` prints where it landed |
|
|
18
|
+
| `seed.neutral` | The tint of pages, surfaces and text, `#rrggbb` or `{ hue, chroma }` |
|
|
19
|
+
| `dials` | Default `expression` (quiet balanced vivid), `density`, `flair`, `theme`. With `theme: 'light'`, dark isn't checked |
|
|
20
|
+
| `fields` | `floating` (the default: the label floats in a notch) or `classic` (the label above the box) |
|
|
21
|
+
| `radius` | `sharp` (2px), `soft`, `round`. Below 2px, set the radius tokens |
|
|
22
|
+
| `fonts` | `body`, `display`, `mono`: a family or a whole stack |
|
|
23
|
+
| `tokens` | Any `--warp-*`, set on every scope. Colours are `#rrggbb`, `oklch()`, `color-mix()` or `light-dark()` |
|
|
24
|
+
|
|
25
|
+
App-wide values go in `warp()`. A `--warp-color-*` set in CSS on `:root` skips the check and is undone inside any scope that
|
|
26
|
+
sets a dial, such as a Table's density.
|
|
27
|
+
|
|
28
|
+
## colors, system, parts, themes
|
|
29
|
+
|
|
30
|
+
| Option | Does |
|
|
31
|
+
|---|---|
|
|
32
|
+
| `colors` | `{ brand: '#e8590c' }`: `--warp-color-brand`, a tone (`tone="brand"`), and `bg-brand` via `.weft/tokens/warp.tailwind.css`. Lowercase names, `#rrggbb` |
|
|
33
|
+
| `system` | The scales: `radius`, `space`, `pad`, `control`, `font`, `shadow`, `duration`, `ease`, `border`, `icon`, `text.<role>` |
|
|
34
|
+
| `parts` | One element, at rest and per state (`hover focus press active open disabled`). Numbers are px (ms for `duration`); names are system steps or `colors` |
|
|
35
|
+
| `themes` | `{ ocean: { seed, colors, system, parts } }` under `[data-palette="ocean"]`. `dials.palette` sets the default; `setPalette()` switches |
|
|
36
|
+
|
|
37
|
+
Every part takes `bg text border borderWidth radius padding paddingX paddingY gap margin height minHeight width size
|
|
38
|
+
shadow opacity scale duration easing font fontSize weight leading tracking case`. A part's own keys add to those or
|
|
39
|
+
replace one of the same name with its own meaning (an underline's `width` is its thickness):
|
|
40
|
+
|
|
41
|
+
| Part | For | Own keys | States |
|
|
42
|
+
|---|---|---|---|
|
|
43
|
+
| `button` | Button and ButtonLink | `bg` `text` `border` `radius` | hover focus press active open disabled |
|
|
44
|
+
| `button.soft` | | `bg` `text` `border` | hover focus press active open disabled |
|
|
45
|
+
| `button.outline` | | `bg` `text` `border` | hover focus press active open disabled |
|
|
46
|
+
| `button.ghost` | | `bg` `text` `border` | hover focus press active open disabled |
|
|
47
|
+
| `button.frame` | | `bg` `text` `border` | hover focus press active open disabled |
|
|
48
|
+
| `button.link` | | `bg` `text` `border` | hover focus press active open disabled |
|
|
49
|
+
| `iconButton` | A button that holds only an icon (copy, close, menu): square by default | `radius` `icon` | hover focus press active open disabled |
|
|
50
|
+
| `copy` | CopyButton | `radius` | hover focus press active open disabled |
|
|
51
|
+
| `close` | The close button of a dialog, sheet, drawer, alert or side nav | — | hover focus press active open disabled |
|
|
52
|
+
| `link` | Link: the text link with the drawn underline | — | hover focus press active open disabled |
|
|
53
|
+
| `link.underline` | | `color` `width` `offset` | hover focus |
|
|
54
|
+
| `nav` | Nav: the row or column of links with the sliding bar | — | — |
|
|
55
|
+
| `nav.item` | | — | hover focus press active open disabled |
|
|
56
|
+
| `nav.underline` | | `color` `width` `duration` | — |
|
|
57
|
+
| `tabs` | Tabs | — | — |
|
|
58
|
+
| `tabs.tab` | | `radius` | hover focus press active open disabled |
|
|
59
|
+
| `tabs.underline` | | `color` `width` | — |
|
|
60
|
+
| `pill` | Pill and PillLink | `radius` | hover focus press active open disabled |
|
|
61
|
+
| `badge` | Badge | — | hover focus press active open disabled |
|
|
62
|
+
| `chip` | Chip | `radius` | hover focus press active open disabled |
|
|
63
|
+
| `card` | Card and Surface | — | hover focus press active open disabled |
|
|
64
|
+
| `field` | Text fields, selects and pickers | `bg` `radius` | hover focus press active open disabled |
|
|
65
|
+
| `field.label` | | `text` | hover focus |
|
|
66
|
+
| `sweep` | The sweep: the stroke that draws round a control on hover and focus, and the text while it runs | `stroke` `base` `text` `width` `duration` | — |
|
|
67
|
+
| `dialog` | Modal and Dialog | — | — |
|
|
68
|
+
| `sheet` | Sheet and Drawer | — | — |
|
|
69
|
+
| `popover` | Popover, Menu and Tooltip panels | — | — |
|
|
70
|
+
| `toast` | Toast | — | — |
|
|
71
|
+
|
|
72
|
+
A solid's `bg` and `text` move its tone channels, so hover, pull-out and sweep follow. `hover.bg` is the hover fill. Text
|
|
73
|
+
set on a fill set in the same part is checked like a seed: below 4.5:1 is D010. Each named theme's seed is fitted the
|
|
74
|
+
same way as the main one.
|
|
75
|
+
|
|
76
|
+
```ts
|
|
77
|
+
warp({
|
|
78
|
+
colors: { brand: '#e8590c', ink: '#1b1b1f' },
|
|
79
|
+
parts: {
|
|
80
|
+
button: {
|
|
81
|
+
bg: 'brand',
|
|
82
|
+
text: 'ink',
|
|
83
|
+
radius: 'pill',
|
|
84
|
+
hover: { bg: 'ink', text: 'white' },
|
|
85
|
+
},
|
|
86
|
+
sweep: { stroke: 'brand', width: 2 },
|
|
87
|
+
nav: {
|
|
88
|
+
item: { hover: { text: 'brand' } },
|
|
89
|
+
underline: { color: 'brand', width: 3 },
|
|
90
|
+
},
|
|
91
|
+
},
|
|
92
|
+
themes: {
|
|
93
|
+
ocean: {
|
|
94
|
+
seed: { accent: '#0b7285' },
|
|
95
|
+
colors: { brand: '#0b7285' },
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
})
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## Brutalist
|
|
102
|
+
|
|
103
|
+
```ts
|
|
104
|
+
warp({
|
|
105
|
+
dials: { expression: 'quiet', flair: 'none' },
|
|
106
|
+
fonts: {
|
|
107
|
+
display: 'ui-monospace, monospace',
|
|
108
|
+
body: 'ui-monospace, monospace',
|
|
109
|
+
},
|
|
110
|
+
tokens: {
|
|
111
|
+
'--warp-radius-mark': '0px',
|
|
112
|
+
'--warp-radius-control': '0px',
|
|
113
|
+
'--warp-radius-surface': '0px',
|
|
114
|
+
},
|
|
115
|
+
})
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
```css
|
|
119
|
+
/* src/theme.css */
|
|
120
|
+
[data-scope="button"] {
|
|
121
|
+
--warp-tone-edge: var(--warp-color-text);
|
|
122
|
+
border-width: 2px;
|
|
123
|
+
box-shadow: 4px 4px 0 var(--warp-color-text);
|
|
124
|
+
}
|
|
125
|
+
[data-scope="surface"] {
|
|
126
|
+
border: 2px solid var(--warp-color-text);
|
|
127
|
+
box-shadow: none;
|
|
128
|
+
}
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
Raw colours and shadows in `src/theme.css` or on a `[data-scope]` rule are D001 and D003 warnings, not errors.
|
|
132
|
+
|
|
133
|
+
## Warm editorial
|
|
134
|
+
|
|
135
|
+
```ts
|
|
136
|
+
warp({
|
|
137
|
+
seed: { accent: '#9a3412', neutral: { hue: 70, chroma: 0.03 } },
|
|
138
|
+
dials: { expression: 'vivid' },
|
|
139
|
+
fonts: { display: '"Iowan Old Style", Georgia, serif' },
|
|
140
|
+
})
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
## An exact brand
|
|
144
|
+
|
|
145
|
+
A seed gives the hue, not the colour. To pin it, set the fill and its hover for both themes. One alone never re-derives the other.
|
|
146
|
+
|
|
147
|
+
```ts
|
|
148
|
+
warp({
|
|
149
|
+
seed: { accent: '#c8102e' },
|
|
150
|
+
tokens: {
|
|
151
|
+
'--warp-color-accent-solid': 'light-dark(#c8102e, #e5484d)',
|
|
152
|
+
'--warp-color-accent-solid-hover': 'light-dark(#a50d26, #f2666a)',
|
|
153
|
+
},
|
|
154
|
+
})
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
## Button tone channels
|
|
158
|
+
|
|
159
|
+
A rule that sets a button's `background` leaves its hover layer on the accent. Set the channels and fill, ink, edge and hover
|
|
160
|
+
stay together, at every emphasis:
|
|
161
|
+
|
|
162
|
+
| Channel | Paints |
|
|
163
|
+
|---|---|
|
|
164
|
+
| `--warp-tone-fill` | The background |
|
|
165
|
+
| `--warp-tone-ink` | The text |
|
|
166
|
+
| `--warp-tone-edge` | The border |
|
|
167
|
+
| `--warp-tone-layer` | The hover layer that wipes across |
|
|
168
|
+
|
|
169
|
+
```css
|
|
170
|
+
[data-scope="button"][data-emphasis="solid"] {
|
|
171
|
+
--warp-tone-fill: #111111;
|
|
172
|
+
--warp-tone-ink: #ffffff;
|
|
173
|
+
--warp-tone-layer: #333333;
|
|
174
|
+
}
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
The channels are on Button only. Fields read `--warp-field-*`, tables `--warp-table-*`.
|
|
178
|
+
|
|
179
|
+
## D010
|
|
180
|
+
|
|
181
|
+
Each failing pair is named with its stop. Change that token or the seed, or limit the stops with `dials`. `weft build` stops on
|
|
182
|
+
it, because a refused config would ship warp's default look.
|
package/docs/ui-state.md
CHANGED
|
@@ -11,8 +11,13 @@ type Todo = { id: string; title: string }
|
|
|
11
11
|
|
|
12
12
|
export function TodoRow({ todo }: { todo: Todo }) {
|
|
13
13
|
const row = interaction({ share: () => `todo:${todo.id}` })
|
|
14
|
-
const seen = derived(() =>
|
|
15
|
-
|
|
14
|
+
const seen = derived(() =>
|
|
15
|
+
row.remote.value.hover ? 'someone is here' : '',
|
|
16
|
+
)
|
|
17
|
+
// Only your own hover triggers the request.
|
|
18
|
+
watch(() => row.hover.value, on => {
|
|
19
|
+
if (on) void fetch(`/api/todos/${todo.id}`)
|
|
20
|
+
})
|
|
16
21
|
return (
|
|
17
22
|
<li>
|
|
18
23
|
<Checkbox ui={row}>{todo.title}</Checkbox>
|
|
@@ -53,7 +58,10 @@ Sharing with other clients is off until you say who may, in `src/realtime.server
|
|
|
53
58
|
```ts
|
|
54
59
|
import { realtime } from '@weft-js/start'
|
|
55
60
|
|
|
56
|
-
export default realtime({
|
|
61
|
+
export default realtime({
|
|
62
|
+
share: ({ ctx, key }) =>
|
|
63
|
+
ctx.user !== null && key.startsWith('todo:'),
|
|
64
|
+
})
|
|
57
65
|
```
|
|
58
66
|
|
|
59
67
|
That rule runs once per key. If it reads a database, a 200-row list means 200 queries, so answer them together instead. You get up to 400 keys in one call and give back the ones allowed:
|
|
@@ -61,11 +69,16 @@ That rule runs once per key. If it reads a database, a 200-row list means 200 qu
|
|
|
61
69
|
```ts
|
|
62
70
|
import { realtime } from '@weft-js/start'
|
|
63
71
|
|
|
64
|
-
//
|
|
65
|
-
declare function visibleKeys(
|
|
72
|
+
// One query returns the keys this user may see.
|
|
73
|
+
declare function visibleKeys(
|
|
74
|
+
userId: string, keys: string[],
|
|
75
|
+
): Promise<string[]>
|
|
66
76
|
|
|
67
77
|
export default realtime({
|
|
68
|
-
share: {
|
|
78
|
+
share: {
|
|
79
|
+
keys: async ({ ctx, keys }) =>
|
|
80
|
+
ctx.user ? visibleKeys(ctx.user.id, keys) : [],
|
|
81
|
+
},
|
|
69
82
|
})
|
|
70
83
|
```
|
|
71
84
|
|
package/docs/verify.md
CHANGED
|
@@ -46,3 +46,16 @@ touched.
|
|
|
46
46
|
|
|
47
47
|
Tests added for code that didn't change have nothing to fail on: fail-before is skipped, and the mutants come from
|
|
48
48
|
the source files the tests import. `--refactor` promises no behavior change: then no test may change.
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
## Indexing intent
|
|
52
|
+
|
|
53
|
+
For a site intended for public search, declare
|
|
54
|
+
`build: { audit: { indexing: 'public' } }`. L024 then fails the build or audit if
|
|
55
|
+
robots metadata or an HTTP X-Robots-Tag header says noindex. `private` or the
|
|
56
|
+
backward-compatible `auto` leaves this a warning; neither removes a directive.
|
|
57
|
+
After deployment, run `weft audit --url https://your-site.example --indexing public`:
|
|
58
|
+
a host or proxy may add a header that a local build cannot see. Preserve intentional
|
|
59
|
+
preview/private exclusions. A noindex finding is an indexing policy issue, not
|
|
60
|
+
chat-agent traffic; use a clean profile before attributing other audit failures
|
|
61
|
+
to application or framework code.
|
package/package.json
CHANGED
|
@@ -1,8 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@weft-js/cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "The weft command: dev, build, check, the agent layer and the MCP server",
|
|
5
5
|
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/Ravonus/weft.git",
|
|
9
|
+
"directory": "packages/cli"
|
|
10
|
+
},
|
|
11
|
+
"homepage": "https://github.com/Ravonus/weft/tree/main/packages/cli#readme",
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/Ravonus/weft/issues"
|
|
14
|
+
},
|
|
6
15
|
"type": "module",
|
|
7
16
|
"bin": {
|
|
8
17
|
"weft": "./bin/weft.mjs"
|
|
@@ -15,14 +24,14 @@
|
|
|
15
24
|
"node": ">=26"
|
|
16
25
|
},
|
|
17
26
|
"dependencies": {
|
|
18
|
-
"@weft-js/create": "0.1.
|
|
27
|
+
"@weft-js/create": "0.1.3",
|
|
19
28
|
"axe-core": "4.14.0",
|
|
20
29
|
"typescript": "6.0.3",
|
|
21
30
|
"yaml": "2.9.1",
|
|
22
31
|
"zod": "4.6.7"
|
|
23
32
|
},
|
|
24
33
|
"peerDependencies": {
|
|
25
|
-
"@weft-js/core": "^0.1.
|
|
34
|
+
"@weft-js/core": "^0.1.3"
|
|
26
35
|
},
|
|
27
36
|
"publishConfig": {
|
|
28
37
|
"access": "public"
|
|
@@ -38,4 +47,4 @@
|
|
|
38
47
|
"README.md",
|
|
39
48
|
"LICENSE"
|
|
40
49
|
]
|
|
41
|
-
}
|
|
50
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { createRequire } from 'node:module'
|
|
2
|
+
import { join } from 'node:path'
|
|
3
|
+
import ts from 'typescript'
|
|
4
|
+
import { pluginFactory, pluginName } from '../load-config.mjs'
|
|
5
|
+
import { refused } from './errors.mjs'
|
|
6
|
+
|
|
7
|
+
/** The import to write: `<pkg>/plugin` when the package has one, and however it exports its factory. */
|
|
8
|
+
function pluginImport(root, pkg) {
|
|
9
|
+
const require = createRequire(join(root, 'package.json'))
|
|
10
|
+
const name = pluginName(pkg)
|
|
11
|
+
for (const spec of [`${pkg}/plugin`, pkg]) {
|
|
12
|
+
let path
|
|
13
|
+
try {
|
|
14
|
+
path = require.resolve(spec)
|
|
15
|
+
} catch {
|
|
16
|
+
continue
|
|
17
|
+
}
|
|
18
|
+
const factory = pluginFactory(createRequire(import.meta.url)(path), pkg)
|
|
19
|
+
if (!factory)
|
|
20
|
+
throw refused(`${spec} exports no plugin factory to call, so nothing was changed.`)
|
|
21
|
+
return {
|
|
22
|
+
name: factory.name,
|
|
23
|
+
line: `import ${factory.named ? `{ ${factory.name} }` : factory.name} from '${spec}'`,
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return { name, line: `import ${name} from '${pkg}'` } // not installed yet: only a dry run gets this far
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** The object literal `export default` gives, directly or through `defineConfig(…)`. */
|
|
30
|
+
function configObject(sf) {
|
|
31
|
+
for (const st of sf.statements) {
|
|
32
|
+
if (!ts.isExportAssignment(st)) continue
|
|
33
|
+
const value = ts.isCallExpression(st.expression) ? st.expression.arguments[0] : st.expression
|
|
34
|
+
if (value && ts.isObjectLiteralExpression(value)) return value
|
|
35
|
+
}
|
|
36
|
+
return null
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function addToTs(root, file, text, pkg) {
|
|
40
|
+
const sf = ts.createSourceFile(file, text, ts.ScriptTarget.Latest, true)
|
|
41
|
+
const object = configObject(sf)
|
|
42
|
+
if (!object)
|
|
43
|
+
throw refused(
|
|
44
|
+
`${file} doesn't export a plain object, so \`weft add\` can't edit it. Add \`${pkg}\` to \`plugins\` by hand.`,
|
|
45
|
+
)
|
|
46
|
+
const { name, line } = pluginImport(root, pkg)
|
|
47
|
+
const prop = object.properties.find((p) => p.name?.getText(sf) === 'plugins')
|
|
48
|
+
if (prop && !(ts.isPropertyAssignment(prop) && ts.isArrayLiteralExpression(prop.initializer)))
|
|
49
|
+
throw refused(
|
|
50
|
+
`\`plugins\` in ${file} isn't a plain array, so \`weft add\` can't edit it. Add \`${pkg}\` by hand.`,
|
|
51
|
+
)
|
|
52
|
+
const edits = []
|
|
53
|
+
const imports = sf.statements.filter(ts.isImportDeclaration)
|
|
54
|
+
const after = imports.at(-1)?.end ?? 0
|
|
55
|
+
edits.push([after, `${imports.length ? '\n' : ''}${line}${imports.length ? '' : '\n'}`])
|
|
56
|
+
if (prop) {
|
|
57
|
+
const list = prop.initializer
|
|
58
|
+
const last = list.elements.at(-1)
|
|
59
|
+
edits.push(last ? [last.end, `, ${name}()`] : [list.getStart(sf) + 1, `${name}()`])
|
|
60
|
+
} else {
|
|
61
|
+
const last = object.properties.at(-1)
|
|
62
|
+
edits.push(
|
|
63
|
+
last
|
|
64
|
+
? [
|
|
65
|
+
object.properties.end,
|
|
66
|
+
`${object.properties.hasTrailingComma ? '' : ','}\n plugins: [${name}()],`,
|
|
67
|
+
]
|
|
68
|
+
: [object.getStart(sf) + 1, `\n plugins: [${name}()],\n`],
|
|
69
|
+
)
|
|
70
|
+
}
|
|
71
|
+
return edits
|
|
72
|
+
.toSorted((a, b) => b[0] - a[0])
|
|
73
|
+
.reduce((t, [at, insert]) => t.slice(0, at) + insert + t.slice(at), text)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function addToData(file, text, pkg) {
|
|
77
|
+
if (file.endsWith('.json')) {
|
|
78
|
+
const data = JSON.parse(text)
|
|
79
|
+
data.plugins = [...(data.plugins ?? []), pkg]
|
|
80
|
+
return `${JSON.stringify(data, null, 2)}\n`
|
|
81
|
+
}
|
|
82
|
+
const { parseDocument } = createRequire(import.meta.url)('yaml')
|
|
83
|
+
const doc = parseDocument(text)
|
|
84
|
+
if (doc.has('plugins')) doc.addIn(['plugins'], pkg)
|
|
85
|
+
else doc.set('plugins', [pkg])
|
|
86
|
+
return doc.toString()
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export const listed = (config, pkg) => (config.plugins ?? []).some((p) => p.name === pkg)
|
|
90
|
+
|
|
91
|
+
/** The config names the package already (imported or listed), which a read can tell without loading it. */
|
|
92
|
+
export const names = (text, pkg) =>
|
|
93
|
+
new RegExp(`['"]${pkg.replace(/[.*+?^${}()|[\]\\/]/g, '\\$&')}(/[\\w-]+)?['"]`).test(text)
|