create-kerf-component 5.0.0-beta.55 → 5.0.0-beta.57

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 CHANGED
@@ -12,6 +12,7 @@ cd my-widgets
12
12
  npm install
13
13
  npm run build
14
14
  npm run catalog:check
15
+ npm run check:styles
15
16
  ```
16
17
 
17
18
  Run it with **no argument** (`npm create kerf-component`) and it prompts for the
@@ -36,11 +37,21 @@ A ready-to-publish component package that encodes the rules from the kerf docs
36
37
  component needs:
37
38
  - **per-instance state via a factory + props** (`createCounter` → `<Counter store={…} />`), and
38
39
  - **a `wire(root)` delegation disposer** (`wireCounter`) instead of inline event handlers.
40
+ - **A stylesheet that owns only its component** (`src/counter.css`, exported as
41
+ `./counter.css`): it styles `.kerf-counter` and its own internals, writes only
42
+ `--_kerf-counter-*` private variables (the context it gives its buttons is
43
+ named after Counter), exposes a typed `size` prop and one public token
44
+ (`--kerf-counter-gap`) as its configuration, and adapts itself inside a Kerf
45
+ UI Toolbar from its own stylesheet (`.kui-toolbar .kerf-counter`). `npm run
46
+ check:styles` runs `kerf-ui-analyze` (from the `@kerfjs/ui` dev dependency)
47
+ over `src/`, so a rule that restyles another package's component, touches
48
+ its private variables, overrides a token a typed prop sets, or uses a hook
49
+ class (`KUI-L019`–`KUI-L022`) fails before `prepublishOnly` publishes.
39
50
  - **Author-owned AI metadata** in `kerf.components.json`, with explicit purpose,
40
51
  exports, composition, geometry, public `rootClass`, tokens, accessibility,
41
52
  and source links. Run
42
53
  `npm run catalog:generate` to emit the package-qualified
43
- `component-catalog-v2.json`; `npm run catalog:check` fails on drift, deleted
54
+ `component-composition.json`; `npm run catalog:check` fails on drift, deleted
44
55
  sources, renamed exports, duplicate ids, an omitted author decision, or any
45
56
  source/output field that violates the shipped schemas. Export verification
46
57
  uses the TypeScript/TSX syntax tree, so JSX text, nested scopes, comments, and
@@ -54,6 +65,13 @@ A ready-to-publish component package that encodes the rules from the kerf docs
54
65
  not render, remove, or treat those attributes as their own state. The field
55
66
  is optional; names must be unique per component, and the checker rejects a
56
67
  helper that is not listed in `wiring.helpers`.
68
+ `boundaries.placeableClasses` (optional) names the public classes an app may
69
+ write onto its own elements; every other public class is the component's
70
+ rendered anatomy, which `eslint-plugin-kerfjs` reports on an app-owned
71
+ element as `KUI-L103` with the component to render. `boundaries.rootElement`
72
+ names the element the component itself renders around its placeable classes,
73
+ so a plain element of that tag carrying them is reported too. Placeable
74
+ classes must be public; `rootElement` requires them.
57
75
  A wrapper that renders a cataloged component declares it under
58
76
  `composition.rendersAs` (for example
59
77
  `["@kerfjs/ui:toolbar-control-group"]`), so the `ui-composition` lint treats
@@ -79,17 +97,18 @@ my-widgets/
79
97
  ├── tsconfig.json # jsxImportSource: "kerfjs"
80
98
  ├── tsup.config.ts # external: ['kerfjs'], format esm, dts
81
99
  ├── kerf.components.json # explicit source metadata (never inferred from pixels)
82
- ├── component-catalog-v2.json # deterministic generated AI catalog
100
+ ├── component-composition.json # deterministic generated AI catalog
83
101
  ├── scripts/
84
102
  │ ├── kerf-component-catalog.mjs # local generator + check mode
85
103
  │ ├── component-metadata.schema.json # author-source schema used by the checker
86
- │ └── component-catalog-v2.schema.json # emitted-catalog schema used by the checker
104
+ │ └── component-composition.schema.json # emitted-catalog schema used by the checker
87
105
  ├── LICENSE # MIT license with the package contributor notice
88
106
  ├── .gitignore
89
107
  ├── README.md
90
108
  └── src/
91
109
  ├── index.ts # public barrel
92
- └── counter.tsx # factory + component + wire() disposer
110
+ ├── counter.tsx # factory + component + wire() disposer
111
+ └── counter.css # styles only Counter; configured by props and tokens
93
112
  ```
94
113
 
95
114
  This package is part of the kerf repository and releases in lockstep with
package/catalog.js CHANGED
@@ -20,7 +20,7 @@ const metadataSchema = JSON.parse(
20
20
  readFileSync(join(scriptRoot, 'component-metadata.schema.json'), 'utf8'),
21
21
  );
22
22
  const catalogSchema = JSON.parse(
23
- readFileSync(join(scriptRoot, 'component-catalog-v2.schema.json'), 'utf8'),
23
+ readFileSync(join(scriptRoot, 'component-composition.schema.json'), 'utf8'),
24
24
  );
25
25
 
26
26
  export class CatalogError extends Error {
@@ -619,6 +619,31 @@ function validateComponent(
619
619
  diagnostics.push(
620
620
  `${at}.boundaries.rootClass: expected null or one publicClasses entry`,
621
621
  );
622
+ // An application may place only the declared subset of public classes on
623
+ // its own elements; every other public class is the component's rendered
624
+ // anatomy (eslint-plugin-kerfjs KUI-L103). `rootElement` names the element
625
+ // the component renders around its placeable classes.
626
+ if (component.boundaries && 'placeableClasses' in component.boundaries) {
627
+ validateStringList(
628
+ component.boundaries.placeableClasses,
629
+ `${at}.boundaries.placeableClasses`,
630
+ diagnostics,
631
+ );
632
+ for (const className of Array.isArray(component.boundaries.placeableClasses)
633
+ ? component.boundaries.placeableClasses
634
+ : [])
635
+ if (!component.boundaries.publicClasses?.includes(className))
636
+ diagnostics.push(
637
+ `${at}.boundaries.placeableClasses: ${className} is not one of publicClasses`,
638
+ );
639
+ }
640
+ if (
641
+ component.boundaries?.rootElement !== undefined &&
642
+ !component.boundaries.placeableClasses?.length
643
+ )
644
+ diagnostics.push(
645
+ `${at}.boundaries.rootElement: requires a non-empty placeableClasses`,
646
+ );
622
647
  validateStringList(
623
648
  component.boundaries?.publicTokens,
624
649
  `${at}.boundaries.publicTokens`,
@@ -697,7 +722,7 @@ function dependencyCatalogKeys(packageRoot, packageName, cache) {
697
722
  } catch {
698
723
  // An unreadable manifest leaves the package unresolved below.
699
724
  }
700
- catalogPath ??= join(dependencyRoot, 'ai', 'component-catalog-v2.json');
725
+ catalogPath ??= join(dependencyRoot, 'ai', 'component-composition.json');
701
726
  try {
702
727
  const catalog = JSON.parse(readFileSync(catalogPath, 'utf8'));
703
728
  keys = new Set(
@@ -800,11 +825,11 @@ export function generateCatalogs(root = process.cwd()) {
800
825
  .map((component) => toEntry(packageJson.name, component));
801
826
  const catalog = {
802
827
  $schema:
803
- 'https://raw.githubusercontent.com/brianwestphal/kerf/main/ui/ai/component-catalog-extension-v2.schema.json',
804
- schemaVersion: 2,
828
+ 'https://raw.githubusercontent.com/brianwestphal/kerf/main/ui/ai/component-composition-extension.schema.json',
829
+ schemaVersion: 1,
805
830
  package: packageJson.name,
806
831
  compatibility: {
807
- v1Catalog: metadata.v1Catalog ?? 'not-applicable',
832
+ componentCatalog: metadata.componentCatalog ?? 'not-applicable',
808
833
  identity: 'package:id',
809
834
  },
810
835
  entries,
@@ -1,18 +1,25 @@
1
1
  {
2
2
  "$schema": "https://json-schema.org/draft/2020-12/schema",
3
- "$id": "https://raw.githubusercontent.com/brianwestphal/kerf/main/ui/ai/component-catalog-v2.schema.json",
4
- "title": "Kerf UI composition catalog v2",
3
+ "$id": "https://raw.githubusercontent.com/brianwestphal/kerf/main/ui/ai/component-composition.schema.json",
4
+ "title": "Kerf UI composition catalog",
5
5
  "type": "object",
6
6
  "required": ["schemaVersion", "package", "compatibility", "entries"],
7
7
  "properties": {
8
8
  "$schema": { "type": "string" },
9
- "schemaVersion": { "const": 2 },
9
+ "schemaVersion": {
10
+ "const": 1,
11
+ "description": "Version of the composition catalog format itself, independent of component-catalog.json's own schemaVersion."
12
+ },
10
13
  "package": { "type": "string", "minLength": 1 },
11
14
  "compatibility": {
12
15
  "type": "object",
13
- "required": ["v1Catalog", "identity"],
16
+ "required": ["componentCatalog", "identity"],
14
17
  "properties": {
15
- "v1Catalog": { "type": "string", "minLength": 1 },
18
+ "componentCatalog": {
19
+ "type": "string",
20
+ "minLength": 1,
21
+ "description": "Relative path to the component (selection) catalog this composition layer describes, or \"not-applicable\" when the package publishes none."
22
+ },
16
23
  "identity": { "const": "package:id" }
17
24
  },
18
25
  "additionalProperties": false
@@ -274,6 +281,15 @@
274
281
  ]
275
282
  },
276
283
  "publicClasses": { "$ref": "#/$defs/stringList" },
284
+ "placeableClasses": {
285
+ "$ref": "#/$defs/stringList",
286
+ "description": "The subset of publicClasses an application may place on its own elements. Absent means none: every public class is the component's rendered anatomy, reached by rendering the component."
287
+ },
288
+ "rootElement": {
289
+ "type": "string",
290
+ "pattern": "^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$",
291
+ "description": "The intrinsic element the component renders at its root. A placeableClasses class on a plain element of this tag recreates the component; the class stays placeable on any other carrier element."
292
+ },
277
293
  "publicTokens": { "$ref": "#/$defs/stringList" },
278
294
  "publicParts": { "$ref": "#/$defs/stringList" }
279
295
  },
@@ -7,7 +7,11 @@
7
7
  "properties": {
8
8
  "$schema": { "type": "string" },
9
9
  "schemaVersion": { "const": 1 },
10
- "v1Catalog": { "type": "string", "minLength": 1 },
10
+ "componentCatalog": {
11
+ "type": "string",
12
+ "minLength": 1,
13
+ "description": "Relative path to the package's component (selection) catalog, copied into the generated composition catalog's compatibility.componentCatalog; defaults to \"not-applicable\"."
14
+ },
11
15
  "components": {
12
16
  "type": "array",
13
17
  "items": { "$ref": "#/$defs/component" }
@@ -121,6 +125,15 @@
121
125
  ]
122
126
  },
123
127
  "publicClasses": { "$ref": "#/$defs/stringList" },
128
+ "placeableClasses": {
129
+ "$ref": "#/$defs/stringList",
130
+ "description": "The subset of publicClasses an application may place on its own elements (a layout utility, item geometry on another carrier). Absent means none: every public class is the component's rendered anatomy, which eslint-plugin-kerfjs reports on an application-owned element as KUI-L103 with the component to render."
131
+ },
132
+ "rootElement": {
133
+ "type": "string",
134
+ "pattern": "^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$",
135
+ "description": "The intrinsic element the component renders around its own placeableClasses. A placeable class on a plain element of this tag is the component recreated (KUI-L103); another carrier element keeps it. Requires placeableClasses."
136
+ },
124
137
  "publicTokens": { "$ref": "#/$defs/stringList" }
125
138
  },
126
139
  "additionalProperties": false
package/index.js CHANGED
@@ -136,7 +136,7 @@ async function main(argv) {
136
136
  );
137
137
  for (const schema of [
138
138
  'component-metadata.schema.json',
139
- 'component-catalog-v2.schema.json',
139
+ 'component-composition.schema.json',
140
140
  ])
141
141
  copyFileSync(
142
142
  join(dirname(fileURLToPath(import.meta.url)), schema),
@@ -162,8 +162,9 @@ async function main(argv) {
162
162
  ' npm install\n' +
163
163
  ' npm run build # tsup → ESM + .d.ts (kerfjs stays external)\n' +
164
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' +
165
166
  ' npm run typecheck\n\n' +
166
- 'Edit src/counter.tsx and kerf.components.json, 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',
167
168
  );
168
169
  }
169
170
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-kerf-component",
3
- "version": "5.0.0-beta.55",
3
+ "version": "5.0.0-beta.57",
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",
@@ -33,7 +33,7 @@
33
33
  "index.js",
34
34
  "catalog.js",
35
35
  "component-metadata.schema.json",
36
- "component-catalog-v2.schema.json",
36
+ "component-composition.schema.json",
37
37
  "template",
38
38
  "README.md",
39
39
  "LICENSE"
@@ -9,7 +9,8 @@ scaffolded with `create-kerf-component`.
9
9
  npm install
10
10
  npm run build # tsup → dist/ (ESM + .d.ts); kerfjs stays external
11
11
  npm run typecheck
12
- npm run catalog:check # verify component-catalog-v2.json is current
12
+ npm run catalog:check # verify component-composition.json is current
13
+ npm run check:styles # each component styles only itself (kerf-ui-analyze)
13
14
  ```
14
15
 
15
16
  ## Use it
@@ -20,11 +21,12 @@ render the component like any local function — there's no extra toolchain:
20
21
  ```tsx
21
22
  import { mount } from "kerfjs";
22
23
  import { Counter, createCounter, wireCounter } from "__PKG_NAME__";
24
+ import "__PKG_NAME__/counter.css";
23
25
 
24
26
  const counter = createCounter(0); // per-instance state (a factory)
25
27
  const root = document.getElementById("app")!;
26
28
 
27
- mount(root, () => <Counter store={counter} label="Clicks" />);
29
+ mount(root, () => <Counter store={counter} label="Clicks" size="compact" />);
28
30
 
29
31
  const dispose = wireCounter(root, counter); // delegation disposer (call on teardown)
30
32
  ```
@@ -44,6 +46,14 @@ have to:
44
46
  - **No inline JSX event handlers.** Components are pure `(props) => SafeHtml`
45
47
  string-builders; emit `data-action` hooks and let the host wire events with
46
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`.
47
57
  - **Build emits ESM + `.d.ts`; `tsconfig` sets `jsxImportSource: "kerfjs"`.**
48
58
  - **Never import `kerfjs/dev`.** kerf's dev diagnostics install global hooks, so
49
59
  installing them is the consuming _app's_ call, not a library's — the app
@@ -66,7 +76,14 @@ the metadata/catalog pair, the README, and the license.
66
76
  public exports, composition rules, geometry ownership, tokens, accessibility,
67
77
  and source links. `boundaries.rootClass` explicitly names the public class that
68
78
  owns runtime geometry (or is `null` when none does); `publicClasses` order has no
69
- semantic meaning. Keep decisions explicit: the generator deliberately does not
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
70
87
  derive semantic or geometry ownership from rendered appearance. It validates
71
88
  both this source and the generated catalog against the schema copies beside the
72
89
  checker, including rejection of unknown fields. Named exports must exist in
@@ -75,7 +92,7 @@ are not exports. The checker uses this package's installed TypeScript compiler,
75
92
  so run `npm install` before the first local catalog command.
76
93
 
77
94
  ```bash
78
- npm run catalog:generate # write component-catalog-v2.json
95
+ npm run catalog:generate # write component-composition.json
79
96
  npm run catalog:check # no writes; fail when metadata, source, or output drift
80
97
  ```
81
98
 
@@ -1,9 +1,9 @@
1
1
  {
2
- "$schema": "https://raw.githubusercontent.com/brianwestphal/kerf/main/ui/ai/component-catalog-extension-v2.schema.json",
3
- "schemaVersion": 2,
2
+ "$schema": "https://raw.githubusercontent.com/brianwestphal/kerf/main/ui/ai/component-composition-extension.schema.json",
3
+ "schemaVersion": 1,
4
4
  "package": "__PKG_NAME__",
5
5
  "compatibility": {
6
- "v1Catalog": "not-applicable",
6
+ "componentCatalog": "not-applicable",
7
7
  "identity": "package:id"
8
8
  },
9
9
  "entries": [
@@ -130,7 +130,9 @@
130
130
  "publicClasses": [
131
131
  "kerf-counter"
132
132
  ],
133
- "publicTokens": []
133
+ "publicTokens": [
134
+ "--kerf-counter-gap"
135
+ ]
134
136
  },
135
137
  "diagnostics": [],
136
138
  "provenance": {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://raw.githubusercontent.com/brianwestphal/kerf/main/create-kerf-component/component-metadata.schema.json",
3
3
  "schemaVersion": 1,
4
- "v1Catalog": "not-applicable",
4
+ "componentCatalog": "not-applicable",
5
5
  "components": [
6
6
  {
7
7
  "id": "counter",
@@ -54,7 +54,7 @@
54
54
  "boundaries": {
55
55
  "rootClass": "kerf-counter",
56
56
  "publicClasses": ["kerf-counter"],
57
- "publicTokens": []
57
+ "publicTokens": ["--kerf-counter-gap"]
58
58
  },
59
59
  "accessibility": {
60
60
  "obligations": [
@@ -12,11 +12,13 @@
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",
19
- "component-catalog-v2.json",
20
+ "src/counter.css",
21
+ "component-composition.json",
20
22
  "kerf.components.json",
21
23
  "README.md",
22
24
  "LICENSE"
@@ -25,18 +27,20 @@
25
27
  "build": "tsup",
26
28
  "catalog:generate": "node scripts/kerf-component-catalog.mjs --write",
27
29
  "catalog:check": "node scripts/kerf-component-catalog.mjs --check",
30
+ "check:styles": "kerf-ui-analyze --root . src",
28
31
  "typecheck": "tsc --noEmit",
29
- "prepublishOnly": "npm run catalog:check && npm run build"
32
+ "prepublishOnly": "npm run catalog:check && npm run check:styles && npm run build"
30
33
  },
31
34
  "kerfComponentCatalog": {
32
35
  "source": "./kerf.components.json",
33
- "output": "./component-catalog-v2.json"
36
+ "output": "./component-composition.json"
34
37
  },
35
38
  "peerDependencies": {
36
39
  "kerfjs": "^5.0.0-0"
37
40
  },
38
41
  "devDependencies": {
39
- "kerfjs": "^5.0.0-beta.55",
42
+ "kerfjs": "^5.0.0-beta.57",
43
+ "@kerfjs/ui": "^5.0.0-beta.57",
40
44
  "tsup": "^8",
41
45
  "typescript": "^5 || ^6"
42
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
+ }
@@ -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({ store, label = 'Count' }: CounterProps): SafeHtml {
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>