create-kerf-component 5.0.0-beta.7 → 5.0.0-beta.70
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 +75 -3
- package/catalog.js +937 -0
- package/component-composition.schema.json +399 -0
- package/component-metadata.schema.json +282 -0
- package/index.js +30 -3
- package/package.json +9 -2
- package/template/README.md +53 -10
- package/template/component-composition.json +144 -0
- package/template/kerf.components.json +71 -0
- package/template/package.json +15 -3
- package/template/src/counter.css +38 -0
- package/template/src/counter.tsx +18 -3
package/index.js
CHANGED
|
@@ -17,8 +17,18 @@
|
|
|
17
17
|
// kerf-component` alone just works. No third-party dependencies — the
|
|
18
18
|
// initializer is plain Node so `npm create` runs it with zero install latency.
|
|
19
19
|
|
|
20
|
-
import {
|
|
20
|
+
import {
|
|
21
|
+
copyFileSync,
|
|
22
|
+
cpSync,
|
|
23
|
+
existsSync,
|
|
24
|
+
mkdirSync,
|
|
25
|
+
readdirSync,
|
|
26
|
+
readFileSync,
|
|
27
|
+
renameSync,
|
|
28
|
+
writeFileSync,
|
|
29
|
+
} from 'node:fs';
|
|
21
30
|
import { basename, dirname, join, resolve } from 'node:path';
|
|
31
|
+
import process from 'node:process';
|
|
22
32
|
import { createInterface } from 'node:readline/promises';
|
|
23
33
|
import { fileURLToPath } from 'node:url';
|
|
24
34
|
|
|
@@ -41,7 +51,9 @@ const USAGE =
|
|
|
41
51
|
async function promptForTarget() {
|
|
42
52
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
43
53
|
try {
|
|
44
|
-
const answer = await rl.question(
|
|
54
|
+
const answer = await rl.question(
|
|
55
|
+
`Directory (\`.\` for current) [${DEFAULT_NAME}]: `,
|
|
56
|
+
);
|
|
45
57
|
return answer.trim() || DEFAULT_NAME;
|
|
46
58
|
} finally {
|
|
47
59
|
rl.close();
|
|
@@ -117,6 +129,19 @@ async function main(argv) {
|
|
|
117
129
|
// verbatim (`_gitignore` → `.gitignore`).
|
|
118
130
|
mkdirSync(targetDir, { recursive: true });
|
|
119
131
|
cpSync(TEMPLATE_DIR, targetDir, { recursive: true });
|
|
132
|
+
mkdirSync(join(targetDir, 'scripts'), { recursive: true });
|
|
133
|
+
copyFileSync(
|
|
134
|
+
join(dirname(fileURLToPath(import.meta.url)), 'catalog.js'),
|
|
135
|
+
join(targetDir, 'scripts', 'kerf-component-catalog.mjs'),
|
|
136
|
+
);
|
|
137
|
+
for (const schema of [
|
|
138
|
+
'component-metadata.schema.json',
|
|
139
|
+
'component-composition.schema.json',
|
|
140
|
+
])
|
|
141
|
+
copyFileSync(
|
|
142
|
+
join(dirname(fileURLToPath(import.meta.url)), schema),
|
|
143
|
+
join(targetDir, 'scripts', schema),
|
|
144
|
+
);
|
|
120
145
|
|
|
121
146
|
for (const file of walk(targetDir)) {
|
|
122
147
|
const text = readFileSync(file, 'utf8');
|
|
@@ -136,8 +161,10 @@ async function main(argv) {
|
|
|
136
161
|
cdHint +
|
|
137
162
|
' npm install\n' +
|
|
138
163
|
' npm run build # tsup → ESM + .d.ts (kerfjs stays external)\n' +
|
|
164
|
+
' npm run catalog:check # verify AI metadata and public exports\n' +
|
|
165
|
+
' npm run check:styles # each component styles only itself (kerf-ui-analyze)\n' +
|
|
139
166
|
' npm run typecheck\n\n' +
|
|
140
|
-
'Edit src/counter.tsx, then publish with `npm publish --access public`.\n',
|
|
167
|
+
'Edit src/counter.tsx, src/counter.css, and kerf.components.json, then publish with `npm publish --access public`.\n',
|
|
141
168
|
);
|
|
142
169
|
}
|
|
143
170
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-kerf-component",
|
|
3
|
-
"version": "5.0.0-beta.
|
|
3
|
+
"version": "5.0.0-beta.70",
|
|
4
4
|
"description": "Scaffold a publishable kerf component package that already follows kerf's hard packaging rules.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -26,15 +26,22 @@
|
|
|
26
26
|
"node": ">=18.0.0"
|
|
27
27
|
},
|
|
28
28
|
"bin": {
|
|
29
|
-
"create-kerf-component": "index.js"
|
|
29
|
+
"create-kerf-component": "index.js",
|
|
30
|
+
"kerf-component-catalog": "catalog.js"
|
|
30
31
|
},
|
|
31
32
|
"files": [
|
|
32
33
|
"index.js",
|
|
34
|
+
"catalog.js",
|
|
35
|
+
"component-metadata.schema.json",
|
|
36
|
+
"component-composition.schema.json",
|
|
33
37
|
"template",
|
|
34
38
|
"README.md",
|
|
35
39
|
"LICENSE"
|
|
36
40
|
],
|
|
37
41
|
"scripts": {
|
|
38
42
|
"test": "node --test tests/*.test.js"
|
|
43
|
+
},
|
|
44
|
+
"dependencies": {
|
|
45
|
+
"typescript": "^5.0.0 || ^6.0.0"
|
|
39
46
|
}
|
|
40
47
|
}
|
package/template/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
#
|
|
1
|
+
# **PKG_NAME**
|
|
2
2
|
|
|
3
3
|
A reusable [kerf](https://github.com/brianwestphal/kerf) component package,
|
|
4
4
|
scaffolded with `create-kerf-component`.
|
|
@@ -7,8 +7,10 @@ scaffolded with `create-kerf-component`.
|
|
|
7
7
|
|
|
8
8
|
```bash
|
|
9
9
|
npm install
|
|
10
|
-
npm run build
|
|
10
|
+
npm run build # tsup → dist/ (ESM + .d.ts); kerfjs stays external
|
|
11
11
|
npm run typecheck
|
|
12
|
+
npm run catalog:check # verify component-composition.json is current
|
|
13
|
+
npm run check:styles # each component styles only itself (kerf-ui-analyze)
|
|
12
14
|
```
|
|
13
15
|
|
|
14
16
|
## Use it
|
|
@@ -17,13 +19,14 @@ A kerf app that already has `jsxImportSource: "kerfjs"` configured can import an
|
|
|
17
19
|
render the component like any local function — there's no extra toolchain:
|
|
18
20
|
|
|
19
21
|
```tsx
|
|
20
|
-
import { mount } from
|
|
21
|
-
import { Counter, createCounter, wireCounter } from
|
|
22
|
+
import { mount } from "kerfjs";
|
|
23
|
+
import { Counter, createCounter, wireCounter } from "__PKG_NAME__";
|
|
24
|
+
import "__PKG_NAME__/counter.css";
|
|
22
25
|
|
|
23
|
-
const counter = createCounter(0);
|
|
24
|
-
const root = document.getElementById(
|
|
26
|
+
const counter = createCounter(0); // per-instance state (a factory)
|
|
27
|
+
const root = document.getElementById("app")!;
|
|
25
28
|
|
|
26
|
-
mount(root, () => <Counter store={counter} label="Clicks" />);
|
|
29
|
+
mount(root, () => <Counter store={counter} label="Clicks" size="compact" />);
|
|
27
30
|
|
|
28
31
|
const dispose = wireCounter(root, counter); // delegation disposer (call on teardown)
|
|
29
32
|
```
|
|
@@ -31,7 +34,7 @@ const dispose = wireCounter(root, counter); // delegation disposer (call on tear
|
|
|
31
34
|
## The rules this package follows
|
|
32
35
|
|
|
33
36
|
These are kerf's hard packaging rules (see the kerf docs,
|
|
34
|
-
|
|
37
|
+
_Building reusable component packages_). The scaffold encodes them so you don't
|
|
35
38
|
have to:
|
|
36
39
|
|
|
37
40
|
- **`kerfjs` is a `peerDependency` and is `external` in the build — never
|
|
@@ -43,9 +46,17 @@ have to:
|
|
|
43
46
|
- **No inline JSX event handlers.** Components are pure `(props) => SafeHtml`
|
|
44
47
|
string-builders; emit `data-action` hooks and let the host wire events with
|
|
45
48
|
`delegate()` (see `wireCounter`), which returns a disposer.
|
|
49
|
+
- **Each component owns its styles; configure, never override.** `counter.css`
|
|
50
|
+
styles only `.kerf-counter` and its own internals and writes only
|
|
51
|
+
`--_kerf-counter-*` private variables (context it provides is named after
|
|
52
|
+
Counter). Consumers configure it through props (`size`) and its public token
|
|
53
|
+
(`--kerf-counter-gap`); when it must adapt inside a parent, Counter does so
|
|
54
|
+
in its own stylesheet (`.kui-toolbar .kerf-counter`). A needed variation with
|
|
55
|
+
no prop is a gap to add here, not an override for consumers.
|
|
56
|
+
`npm run check:styles` enforces this with `kerf-ui-analyze`.
|
|
46
57
|
- **Build emits ESM + `.d.ts`; `tsconfig` sets `jsxImportSource: "kerfjs"`.**
|
|
47
58
|
- **Never import `kerfjs/dev`.** kerf's dev diagnostics install global hooks, so
|
|
48
|
-
installing them is the consuming
|
|
59
|
+
installing them is the consuming _app's_ call, not a library's — the app
|
|
49
60
|
writes `if (import.meta.env.DEV) await import('kerfjs/dev')` in its own entry.
|
|
50
61
|
To get the diagnostics while developing this package, add that line to your own
|
|
51
62
|
test harness or demo page instead (it belongs to the harness, not to `src/`).
|
|
@@ -56,4 +67,36 @@ have to:
|
|
|
56
67
|
npm publish --access public
|
|
57
68
|
```
|
|
58
69
|
|
|
59
|
-
`prepublishOnly` runs the build
|
|
70
|
+
`prepublishOnly` checks the catalog and runs the build; `files` ships `dist/`,
|
|
71
|
+
the metadata/catalog pair, the README, and the license.
|
|
72
|
+
|
|
73
|
+
## AI component metadata
|
|
74
|
+
|
|
75
|
+
`kerf.components.json` is the author-owned source for the component's purpose,
|
|
76
|
+
public exports, composition rules, geometry ownership, tokens, accessibility,
|
|
77
|
+
and source links. `boundaries.rootClass` explicitly names the public class that
|
|
78
|
+
owns runtime geometry (or is `null` when none does); `publicClasses` order has no
|
|
79
|
+
semantic meaning. Every public class is the component's rendered anatomy unless
|
|
80
|
+
`boundaries.placeableClasses` lists it: once an app's `.kerf-ui-profile.json`
|
|
81
|
+
declares this package's catalog under `catalogs`, `eslint-plugin-kerfjs`
|
|
82
|
+
reports an app that writes `kerf-counter` onto its own `<div>` (`KUI-L103`) and
|
|
83
|
+
tells it to render `Counter` instead. List a class there only when apps may legitimately
|
|
84
|
+
place it themselves (a layout utility, item geometry on another carrier), and
|
|
85
|
+
add `rootElement` when the component itself renders those classes on that tag.
|
|
86
|
+
Keep decisions explicit: the generator deliberately does not
|
|
87
|
+
derive semantic or geometry ownership from rendered appearance. It validates
|
|
88
|
+
both this source and the generated catalog against the schema copies beside the
|
|
89
|
+
checker, including rejection of unknown fields. Named exports must exist in
|
|
90
|
+
the TypeScript/TSX syntax tree; JSX text, nested scopes, comments, and literals
|
|
91
|
+
are not exports. The checker uses this package's installed TypeScript compiler,
|
|
92
|
+
so run `npm install` before the first local catalog command.
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
npm run catalog:generate # write component-composition.json
|
|
96
|
+
npm run catalog:check # no writes; fail when metadata, source, or output drift
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
The generated catalog ships with the package. AI tools join each entry to
|
|
100
|
+
Kerf's catalog by its `package:id` key, search this package first for
|
|
101
|
+
application-specific concepts, and retain both packages' identities when
|
|
102
|
+
following cross-catalog composition references.
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://raw.githubusercontent.com/brianwestphal/kerf/main/ui/ai/component-composition-extension.schema.json",
|
|
3
|
+
"schemaVersion": 1,
|
|
4
|
+
"package": "__PKG_NAME__",
|
|
5
|
+
"compatibility": {
|
|
6
|
+
"componentCatalog": "not-applicable",
|
|
7
|
+
"identity": "package:id"
|
|
8
|
+
},
|
|
9
|
+
"entries": [
|
|
10
|
+
{
|
|
11
|
+
"key": "__PKG_NAME__:counter",
|
|
12
|
+
"package": "__PKG_NAME__",
|
|
13
|
+
"id": "counter",
|
|
14
|
+
"name": "Counter",
|
|
15
|
+
"kind": "component",
|
|
16
|
+
"purpose": "Render a controlled counter with delegated increment and decrement actions.",
|
|
17
|
+
"publicExports": [
|
|
18
|
+
{
|
|
19
|
+
"name": "Counter",
|
|
20
|
+
"subpath": "."
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
"name": "CounterProps",
|
|
24
|
+
"subpath": "."
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
"name": "CounterState",
|
|
28
|
+
"subpath": "."
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
"name": "CounterStore",
|
|
32
|
+
"subpath": "."
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
"name": "createCounter",
|
|
36
|
+
"subpath": "."
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
"name": "wireCounter",
|
|
40
|
+
"subpath": "."
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
"name": "Counter",
|
|
44
|
+
"subpath": "./counter"
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
"name": "CounterProps",
|
|
48
|
+
"subpath": "./counter"
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
"name": "CounterState",
|
|
52
|
+
"subpath": "./counter"
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
"name": "CounterStore",
|
|
56
|
+
"subpath": "./counter"
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
"name": "createCounter",
|
|
60
|
+
"subpath": "./counter"
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
"name": "wireCounter",
|
|
64
|
+
"subpath": "./counter"
|
|
65
|
+
}
|
|
66
|
+
],
|
|
67
|
+
"sourceLinks": [
|
|
68
|
+
"README.md#use-it",
|
|
69
|
+
"src/counter.tsx"
|
|
70
|
+
],
|
|
71
|
+
"source": "src/counter.tsx",
|
|
72
|
+
"parents": {
|
|
73
|
+
"mode": "any",
|
|
74
|
+
"entries": []
|
|
75
|
+
},
|
|
76
|
+
"contexts": [
|
|
77
|
+
"controlled-counter"
|
|
78
|
+
],
|
|
79
|
+
"zones": [],
|
|
80
|
+
"children": {
|
|
81
|
+
"mode": "listed",
|
|
82
|
+
"concepts": [
|
|
83
|
+
"decrement-control",
|
|
84
|
+
"value",
|
|
85
|
+
"increment-control"
|
|
86
|
+
],
|
|
87
|
+
"requiredConcepts": [
|
|
88
|
+
"decrement-control",
|
|
89
|
+
"value",
|
|
90
|
+
"increment-control"
|
|
91
|
+
]
|
|
92
|
+
},
|
|
93
|
+
"state": [
|
|
94
|
+
{
|
|
95
|
+
"id": "count",
|
|
96
|
+
"owner": "controlled",
|
|
97
|
+
"required": true
|
|
98
|
+
}
|
|
99
|
+
],
|
|
100
|
+
"wiring": {
|
|
101
|
+
"required": true,
|
|
102
|
+
"helpers": [
|
|
103
|
+
"wireCounter"
|
|
104
|
+
],
|
|
105
|
+
"obligations": [
|
|
106
|
+
"Retain and call the disposer returned by wireCounter."
|
|
107
|
+
]
|
|
108
|
+
},
|
|
109
|
+
"responsive": {
|
|
110
|
+
"owner": "not-applicable",
|
|
111
|
+
"behaviors": []
|
|
112
|
+
},
|
|
113
|
+
"layout": {
|
|
114
|
+
"roles": [
|
|
115
|
+
"counter"
|
|
116
|
+
],
|
|
117
|
+
"geometry": {
|
|
118
|
+
"margin": "none",
|
|
119
|
+
"border": "none",
|
|
120
|
+
"padding": "none"
|
|
121
|
+
}
|
|
122
|
+
},
|
|
123
|
+
"accessibility": {
|
|
124
|
+
"obligations": [
|
|
125
|
+
"Keep increment and decrement controls named and expose the current count as output."
|
|
126
|
+
]
|
|
127
|
+
},
|
|
128
|
+
"boundaries": {
|
|
129
|
+
"rootClass": "kerf-counter",
|
|
130
|
+
"publicClasses": [
|
|
131
|
+
"kerf-counter"
|
|
132
|
+
],
|
|
133
|
+
"publicTokens": [
|
|
134
|
+
"--kerf-counter-gap"
|
|
135
|
+
]
|
|
136
|
+
},
|
|
137
|
+
"diagnostics": [],
|
|
138
|
+
"provenance": {
|
|
139
|
+
"selection": "README.md#use-it",
|
|
140
|
+
"composition": "README.md#the-rules-this-package-follows"
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
]
|
|
144
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://raw.githubusercontent.com/brianwestphal/kerf/main/create-kerf-component/component-metadata.schema.json",
|
|
3
|
+
"schemaVersion": 1,
|
|
4
|
+
"componentCatalog": "not-applicable",
|
|
5
|
+
"components": [
|
|
6
|
+
{
|
|
7
|
+
"id": "counter",
|
|
8
|
+
"name": "Counter",
|
|
9
|
+
"kind": "component",
|
|
10
|
+
"purpose": "Render a controlled counter with delegated increment and decrement actions.",
|
|
11
|
+
"source": "src/counter.tsx",
|
|
12
|
+
"publicExports": [
|
|
13
|
+
{ "name": "Counter", "subpath": "." },
|
|
14
|
+
{ "name": "CounterProps", "subpath": "." },
|
|
15
|
+
{ "name": "CounterState", "subpath": "." },
|
|
16
|
+
{ "name": "CounterStore", "subpath": "." },
|
|
17
|
+
{ "name": "createCounter", "subpath": "." },
|
|
18
|
+
{ "name": "wireCounter", "subpath": "." },
|
|
19
|
+
{ "name": "Counter", "subpath": "./counter" },
|
|
20
|
+
{ "name": "CounterProps", "subpath": "./counter" },
|
|
21
|
+
{ "name": "CounterState", "subpath": "./counter" },
|
|
22
|
+
{ "name": "CounterStore", "subpath": "./counter" },
|
|
23
|
+
{ "name": "createCounter", "subpath": "./counter" },
|
|
24
|
+
{ "name": "wireCounter", "subpath": "./counter" }
|
|
25
|
+
],
|
|
26
|
+
"sourceLinks": ["README.md#use-it", "src/counter.tsx"],
|
|
27
|
+
"composition": {
|
|
28
|
+
"parents": { "mode": "any", "entries": [] },
|
|
29
|
+
"contexts": ["controlled-counter"],
|
|
30
|
+
"zones": [],
|
|
31
|
+
"children": {
|
|
32
|
+
"mode": "listed",
|
|
33
|
+
"concepts": ["decrement-control", "value", "increment-control"],
|
|
34
|
+
"requiredConcepts": [
|
|
35
|
+
"decrement-control",
|
|
36
|
+
"value",
|
|
37
|
+
"increment-control"
|
|
38
|
+
]
|
|
39
|
+
},
|
|
40
|
+
"state": [{ "id": "count", "owner": "controlled", "required": true }],
|
|
41
|
+
"wiring": {
|
|
42
|
+
"required": true,
|
|
43
|
+
"helpers": ["wireCounter"],
|
|
44
|
+
"obligations": [
|
|
45
|
+
"Retain and call the disposer returned by wireCounter."
|
|
46
|
+
]
|
|
47
|
+
},
|
|
48
|
+
"responsive": { "owner": "not-applicable", "behaviors": [] },
|
|
49
|
+
"layout": {
|
|
50
|
+
"roles": ["counter"],
|
|
51
|
+
"geometry": { "margin": "none", "border": "none", "padding": "none" }
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
"boundaries": {
|
|
55
|
+
"rootClass": "kerf-counter",
|
|
56
|
+
"publicClasses": ["kerf-counter"],
|
|
57
|
+
"publicTokens": ["--kerf-counter-gap"]
|
|
58
|
+
},
|
|
59
|
+
"accessibility": {
|
|
60
|
+
"obligations": [
|
|
61
|
+
"Keep increment and decrement controls named and expose the current count as output."
|
|
62
|
+
]
|
|
63
|
+
},
|
|
64
|
+
"diagnostics": [],
|
|
65
|
+
"provenance": {
|
|
66
|
+
"selection": "README.md#use-it",
|
|
67
|
+
"composition": "README.md#the-rules-this-package-follows"
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
]
|
|
71
|
+
}
|
package/template/package.json
CHANGED
|
@@ -12,23 +12,35 @@
|
|
|
12
12
|
"./counter": {
|
|
13
13
|
"types": "./dist/counter.d.ts",
|
|
14
14
|
"import": "./dist/counter.js"
|
|
15
|
-
}
|
|
15
|
+
},
|
|
16
|
+
"./counter.css": "./src/counter.css"
|
|
16
17
|
},
|
|
17
18
|
"files": [
|
|
18
19
|
"dist",
|
|
20
|
+
"src/counter.css",
|
|
21
|
+
"component-composition.json",
|
|
22
|
+
"kerf.components.json",
|
|
19
23
|
"README.md",
|
|
20
24
|
"LICENSE"
|
|
21
25
|
],
|
|
22
26
|
"scripts": {
|
|
23
27
|
"build": "tsup",
|
|
28
|
+
"catalog:generate": "node scripts/kerf-component-catalog.mjs --write",
|
|
29
|
+
"catalog:check": "node scripts/kerf-component-catalog.mjs --check",
|
|
30
|
+
"check:styles": "kerf-ui-analyze --root . src",
|
|
24
31
|
"typecheck": "tsc --noEmit",
|
|
25
|
-
"prepublishOnly": "npm run build"
|
|
32
|
+
"prepublishOnly": "npm run catalog:check && npm run check:styles && npm run build"
|
|
33
|
+
},
|
|
34
|
+
"kerfComponentCatalog": {
|
|
35
|
+
"source": "./kerf.components.json",
|
|
36
|
+
"output": "./component-composition.json"
|
|
26
37
|
},
|
|
27
38
|
"peerDependencies": {
|
|
28
39
|
"kerfjs": "^5.0.0-0"
|
|
29
40
|
},
|
|
30
41
|
"devDependencies": {
|
|
31
|
-
"kerfjs": "^5.0.0-beta.
|
|
42
|
+
"kerfjs": "^5.0.0-beta.70",
|
|
43
|
+
"@kerfjs/ui": "^5.0.0-beta.70",
|
|
32
44
|
"tsup": "^8",
|
|
33
45
|
"typescript": "^5 || ^6"
|
|
34
46
|
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Counter owns this stylesheet, and this stylesheet styles only Counter.
|
|
3
|
+
*
|
|
4
|
+
* - It selects `.kerf-counter` and Counter's own internals, never another
|
|
5
|
+
* component's class, `[data-component]` root, `wa-*` element, or `::part()`.
|
|
6
|
+
* - It writes only Counter's own `--_kerf-counter-*` private variables. The
|
|
7
|
+
* context Counter hands its buttons is named after Counter, the provider.
|
|
8
|
+
* - Consumers configure Counter through its typed props (`size`) and its one
|
|
9
|
+
* public theme token (`--kerf-counter-gap`), never by restyling
|
|
10
|
+
* `.kerf-counter` from their own CSS. A need with no prop is a gap to add to
|
|
11
|
+
* Counter, not an override for the consumer to write.
|
|
12
|
+
*/
|
|
13
|
+
.kerf-counter {
|
|
14
|
+
--_kerf-counter-gap: var(--kerf-counter-gap, 0.5rem);
|
|
15
|
+
--_kerf-counter-control-size: 2rem;
|
|
16
|
+
display: inline-flex;
|
|
17
|
+
align-items: center;
|
|
18
|
+
gap: var(--_kerf-counter-gap);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
.kerf-counter[data-size="compact"] {
|
|
22
|
+
--_kerf-counter-control-size: 1.5rem;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
.kerf-counter > button {
|
|
26
|
+
inline-size: var(--_kerf-counter-control-size);
|
|
27
|
+
block-size: var(--_kerf-counter-control-size);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/*
|
|
31
|
+
* Parent context: when a Counter sits in a Kerf UI Toolbar, Counter adapts
|
|
32
|
+
* itself here, in its own stylesheet. The parent appears only as an ancestor
|
|
33
|
+
* and Counter stays the subject, so the Toolbar never styles Counter and
|
|
34
|
+
* Counter never styles the Toolbar.
|
|
35
|
+
*/
|
|
36
|
+
.kui-toolbar .kerf-counter {
|
|
37
|
+
--_kerf-counter-control-size: 1.75rem;
|
|
38
|
+
}
|
package/template/src/counter.tsx
CHANGED
|
@@ -28,6 +28,11 @@ export type CounterStore = ReturnType<typeof createCounter>;
|
|
|
28
28
|
export interface CounterProps {
|
|
29
29
|
store: CounterStore;
|
|
30
30
|
label?: string;
|
|
31
|
+
/**
|
|
32
|
+
* Control density. A typed prop is how consumers configure the component's
|
|
33
|
+
* look; they never restyle `.kerf-counter` from their own CSS.
|
|
34
|
+
*/
|
|
35
|
+
size?: 'standard' | 'compact';
|
|
31
36
|
}
|
|
32
37
|
|
|
33
38
|
/**
|
|
@@ -35,10 +40,17 @@ export interface CounterProps {
|
|
|
35
40
|
* `data-action` hooks instead of inline event handlers — inline `onClick={...}`
|
|
36
41
|
* handlers don't survive kerf's morph (and the `no-inline-jsx-event-handlers`
|
|
37
42
|
* lint rule flags them). The host wires the events; see `wireCounter` below.
|
|
43
|
+
*
|
|
44
|
+
* Its look lives in `counter.css` (the `./counter.css` export), which styles
|
|
45
|
+
* only Counter; `size` is the configuration consumers use instead of CSS.
|
|
38
46
|
*/
|
|
39
|
-
export function Counter({
|
|
47
|
+
export function Counter({
|
|
48
|
+
store,
|
|
49
|
+
label = 'Count',
|
|
50
|
+
size = 'standard',
|
|
51
|
+
}: CounterProps): SafeHtml {
|
|
40
52
|
return (
|
|
41
|
-
<div class="kerf-counter">
|
|
53
|
+
<div class="kerf-counter" data-size={size}>
|
|
42
54
|
<button type="button" data-action="counter:dec" aria-label="Decrement">
|
|
43
55
|
−
|
|
44
56
|
</button>
|
|
@@ -61,7 +73,10 @@ export function Counter({ store, label = 'Count' }: CounterProps): SafeHtml {
|
|
|
61
73
|
* single listener survives every re-render. Returns the disposer `delegate()`
|
|
62
74
|
* hands back — call it when the host unmounts.
|
|
63
75
|
*/
|
|
64
|
-
export function wireCounter(
|
|
76
|
+
export function wireCounter(
|
|
77
|
+
root: HTMLElement,
|
|
78
|
+
store: CounterStore,
|
|
79
|
+
): () => void {
|
|
65
80
|
return delegate(root, 'click', '[data-action^="counter:"]', (_event, el) => {
|
|
66
81
|
const action = el.getAttribute('data-action');
|
|
67
82
|
if (action === 'counter:inc') store.actions.inc();
|