@starci/eslint-canon-fe 2.0.0 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/contracts.ts DELETED
@@ -1,312 +0,0 @@
1
- /**
2
- * TEMPLATE - `src/components/contracts/index.ts`.
3
- *
4
- * Copy this file to that path and grow the table. Everything above the table is the SCAFFOLDING and
5
- * is the same in every repository; the entries themselves are the repository's own vocabulary, and
6
- * the seed below exists only to show each feature once.
7
- *
8
- * The law this file holds is `patterns/fe/contract/INDEX.md`. The rules that patrol what a type
9
- * cannot see are in `contract.mjs`.
10
- *
11
- * WHY THIS IS A TYPE AND NOT A CONVENTION. Two of the constraints here are closed unions, so a
12
- * value outside them is not forbidden - it is unrepresentable. That single property is what makes a
13
- * whole family of patrol rules unnecessary: there is nothing to police when the bad value cannot be
14
- * typed, and nothing to argue about when the compiler has already refused.
15
- */
16
- import type { ComponentType, ReactNode } from "react"
17
-
18
- /**
19
- * What a component drawn inside a node receives. It never receives the key that placed it: the node
20
- * is drawn by the branch, and a child that could read the key could also disagree with it.
21
- *
22
- * Declared here rather than in `props.ts` because the component types below are built from it, and
23
- * this file cannot import that one without the two forming a cycle.
24
- */
25
- export type InnerProps = { readonly isLoading?: boolean }
26
-
27
- /**
28
- * The closed set of classes a node may lay its children out with.
29
- *
30
- * `gap-[13px]` is not forbidden - it is UNREPRESENTABLE, because it is not a member. Grow this list
31
- * deliberately: a new member is a decision about the house scale, and it should read as one in the
32
- * diff rather than arriving inside a component nobody reviewed closely.
33
- */
34
- export type LayoutClassName =
35
- | "flex" | "grid" | "flex-col" | "flex-row" | "flex-wrap" | "relative"
36
- | "items-center" | "items-baseline" | "items-start"
37
- | "justify-between" | "justify-center" | "[&>*]:w-full"
38
- | "gap-1" | "gap-2" | "gap-3" | "gap-4" | "gap-6" | "gap-8"
39
- | "grid-cols-1" | "sm:grid-cols-2"
40
- | "mx-auto" | "w-full" | "text-center"
41
- | "px-3" | "px-4" | "py-2" | "py-3" | "py-6" | "p-0" | "p-4" | "p-6"
42
- | "rounded-xl" | "rounded-2xl" | "rounded-full"
43
- | "bg-surface" | "bg-default" | "shadow-surface"
44
- // Positional selectors are accepted HERE and only here - see the note on the table below.
45
- | "[&>*]:px-4" | "[&>*]:py-3"
46
- | "[&>*:first-child]:pt-4" | "[&>*:last-child]:pb-4"
47
- | "[&>*:first-child]:grow" | "[&>*:last-child]:grow"
48
- | "[&>*:nth-child(2)]:min-w-0" | "[&>*:nth-child(2)]:grow"
49
-
50
- /**
51
- * The elements a node may open.
52
- *
53
- * A node is not always a `div`: a run of days IS a list, and a field with a submit IS a form, and an
54
- * element chosen for meaning cannot be swapped for a neutral one without changing what assistive
55
- * technology reports.
56
- *
57
- * THIS UNION IS LOAD-BEARING FOR A REASON THAT IS NOT OBVIOUS. Without it the frame draws only
58
- * `div`, so any shape needing `<ul>` has nowhere lawful to live and gets filed among the leaves
59
- * instead - where it may write its own classes. That is how an entire tier fills up with
60
- * arrangements, and the duplication is invisible afterwards because no rule reads both places.
61
- *
62
- * `main` AND `ol` WERE ADDED FROM A CONSUMING REPOSITORY, which is the part worth recording. A
63
- * repository had grown its own copy of this union under a different NAME - `ContractHostTag` - with
64
- * `main` and `ol` present and `li`, `header` and `footer` missing. Neither list contained the other,
65
- * so both were wrong and neither could tell: the scaffolding above the table is supposed to be
66
- * identical everywhere, and a renamed type is a divergence no import ever reports. The members are
67
- * merged here, at the source, and the repository takes this name.
68
- *
69
- * `main` is the document's one landmark. `ol` is `ul` where the sequence is the meaning - module
70
- * three follows module two - and it is useless without the `li` beside it, which is the trap the
71
- * other repository hit: it admitted both list containers and not the item, so a list row had to be
72
- * a `div`, which is invalid HTML and silent. A `<ul>` whose children are not `<li>` stops being
73
- * announced as a list at all, and nothing turns red, because a `div` is never wrong on its own.
74
- */
75
- export type ContractHost =
76
- | "div" | "ul" | "ol" | "li" | "form" | "nav" | "main" | "section" | "header" | "footer" | "aside"
77
-
78
- /**
79
- * ONE CHILD SLOT. Either the child is another node, or it is a leaf.
80
- *
81
- * A node child names a KEY, and the key is checked against this table. A leaf child cannot: a leaf
82
- * is not a node and has no key. It names the leaf and the props it must take, and the leaf declares
83
- * the same pair - so the table never imports a component, and the compiler still matches them.
84
- *
85
- * `repeats: true` says the live slot is an array. `restingCount` is separate because it is the
86
- * loading cardinality, not a claim about the dynamic live length.
87
- */
88
- export type ChildCardinality =
89
- | { readonly repeats?: false; readonly restingCount?: never }
90
- | { readonly repeats: true; readonly restingCount: number }
91
-
92
- export type ChildSpec = ChildCardinality & {
93
- readonly contract?: string | ReadonlyArray<string>
94
- readonly leaf?: string | ReadonlyArray<string>
95
- readonly props?: Readonly<Record<string, string | number | boolean | null>>
96
- readonly optional?: true
97
- }
98
-
99
- /** One entry: a node's own classes, what sits inside it, the element it opens, and why. */
100
- export interface ContractSpec {
101
- /** The classes of the node itself. Not a prop, not reachable by a caller. */
102
- readonly classNames: ReadonlyArray<LayoutClassName>
103
- /**
104
- * WHAT SITS INSIDE THIS NODE, position by position, in the order it appears.
105
- *
106
- * THIS IS THE CHILD CONTRACT. It was a sentence in the key's name once, which meant a reader
107
- * could check it and nothing else could. Written as a list, every position has a type.
108
- *
109
- * A position is one of two things, and the difference is the difference between the tiers:
110
- *
111
- * - `{ contract }`, when the child is another node. A key this table does not define is a
112
- * compile error here, rather than an empty box on a screen somebody has to open to notice.
113
- * - `{ leaf, props }`, when the child is a leaf. Leaves have no key, so the slot names the
114
- * leaf and the props it must take instead.
115
- *
116
- * SLOTS ARE NAMED, NOT COUNTED. A record rather than a list, because a position is a fact about
117
- * the tree and a number is only a fact about the file: insert a child into a list and every
118
- * slot after it silently means something else. A name survives the insertion, reads at the call
119
- * site without counting, and gives the reason a thing to talk about.
120
- *
121
- * The whole tree above the leaves is therefore declared in this file, and a shape can be read
122
- * without opening a single component.
123
- */
124
- readonly children: { readonly [slot: string]: ChildSpec }
125
- /**
126
- * The element this node opens. Absent means `div` - the neutral box, which is most nodes.
127
- *
128
- * Stated here rather than passed by a caller: two call sites of one key that disagreed about
129
- * the element would be two different nodes wearing one name.
130
- */
131
- readonly host?: ContractHost
132
- /**
133
- * Why the children of this node sit the way they do, in one sentence.
134
- *
135
- * A REASON, never a restatement of the key: "a row of chips" only says the key again; "the tags
136
- * wrap onto their own line before the title does" is the fact that made the node exist.
137
- */
138
- readonly why: string
139
- }
140
-
141
- /**
142
- * Build the table.
143
- *
144
- * A function rather than a bare literal so the keys are checked in one place and stay literal
145
- * without an `as const` at the call site.
146
- *
147
- * THE CONSTRAINT IS SELF-REFERENTIAL, and that is what makes the child contract real: `children` is
148
- * narrowed to `keyof T`, so a child naming a key this table does not define is a compile error in
149
- * this file rather than an empty node on a screen somebody has to open to notice.
150
- */
151
- const buildContracts = <
152
- const T extends {
153
- readonly [K in keyof T]: ContractSpec & {
154
- readonly children: {
155
- readonly [slot: string]: ChildCardinality & {
156
- readonly contract?: keyof T | ReadonlyArray<keyof T>
157
- readonly leaf?: string | ReadonlyArray<string>
158
- readonly props?: Readonly<Record<string, string | number | boolean | null>>
159
- readonly optional?: true
160
- }
161
- }
162
- }
163
- },
164
- >(contracts: T): T => contracts
165
-
166
- /**
167
- * THE TABLE. Every node the design layer may draw, and the reason each one holds its children the
168
- * way it does.
169
- *
170
- * KEEP THE NAMES CHILD-FIXING. A key whose name does not say what belongs inside it stops teaching
171
- * anything, and its reason decays into a label the moment a second screen uses it.
172
- *
173
- * THE NAME IS NO LONGER WHAT HOLDS THE CHILD CONTRACT, and that is a reversal worth recording. The
174
- * child map was retired once because contents arrived as markup, which a rule cannot read: a `.map`,
175
- * a ternary and an unnamed subtree all look identical to it. Contents arrive as COMPONENTS now, so
176
- * `children` states each position and the compiler checks it. The name still has a job - it is what
177
- * a reader sees first - but it is no longer the only fence.
178
- *
179
- * POSITIONAL SELECTORS ARE ACCEPTED HERE, and only here. Naming a child instead of counting to it
180
- * would be better. The objection stands - insert something in the middle and `nth-child(2)` is
181
- * silently wrong - but the children are now declared right here, so whoever inserts one is looking
182
- * at the selector while they do it.
183
- *
184
- * The entries below are a SEED, not an inventory: one row of leaves, one node holding another node,
185
- * one that names its host. Delete them once the repository has its own.
186
- */
187
- export const CONTRACTS = buildContracts({
188
- "glyph-title-fact-row": {
189
- classNames: ["flex", "flex-row", "items-center", "gap-2", "[&>*:nth-child(2)]:min-w-0", "[&>*:nth-child(2)]:grow"],
190
- children: {
191
- glyph: { leaf: "icon", props: { size: "sm" } },
192
- title: { leaf: "text", props: { size: "md", tone: "default" } },
193
- fact: { leaf: "text", props: { size: "sm", tone: "muted" } },
194
- },
195
- why: "The glyph identifies the row faster than its name does, so it leads the line and the fact trails it - and the name between them takes the slack, because a long one must clip rather than push the figure off the end of the row.",
196
- },
197
- "heading-over-body": {
198
- classNames: ["flex", "flex-col", "gap-3"],
199
- children: {
200
- heading: { leaf: "heading", props: { level: 3 } },
201
- body: { contract: "glyph-title-fact-row" },
202
- },
203
- why: "The seam here is the only thing telling a reader that the content below belongs to this heading and not to the one above it.",
204
- },
205
- "weekday-run": {
206
- classNames: ["flex", "flex-row", "flex-wrap", "items-center", "gap-2"],
207
- children: {
208
- day: { leaf: "day-cell", props: { size: "sm" }, repeats: true, restingCount: 6 },
209
- },
210
- host: "ul",
211
- why: "A run of equal columns only reads as one span of time while the columns stay on one line, so it wraps as a whole rather than letting a single day drop away from the six beside it.",
212
- },
213
- })
214
-
215
- /** Every key in the table. A key not in this union is a compile error at the call site. */
216
- export type ContractKey = keyof typeof CONTRACTS
217
-
218
- /**
219
- * Read one entry, widened to the shared shape.
220
- *
221
- * @param name - The key to read.
222
- */
223
- export const contractSpec = (name: ContractKey): ContractSpec => CONTRACTS[name]
224
-
225
- /** Every key, in declaration order, so gates and tests can walk the vocabulary. */
226
- export const CONTRACT_KEYS: ReadonlyArray<ContractKey> = Object.keys(CONTRACTS) as Array<ContractKey>
227
-
228
- /** Checked named slots for one key. Content records are values, not pretend React components. */
229
- export type ContractSlots<K extends ContractKey> = {
230
- readonly kind: "slots"
231
- readonly meta: { readonly shape: "contract"; readonly contract: K }
232
- readonly slots: ChildrenOf<K>
233
- }
234
-
235
- /** A branch-owned node that has already drawn wrapper mechanics a content contract cannot express. */
236
- export type ContractProjection<K extends ContractKey> = {
237
- readonly kind: "projection"
238
- readonly meta: { readonly shape: "contract"; readonly contract: K }
239
- readonly project: () => ReactNode
240
- }
241
-
242
- /** A real component type whose runtime props stay separate from its exact contract identity. */
243
- export type ContractRenderComponent<K extends ContractKey, P> = ComponentType<P> & {
244
- readonly kind: "component"
245
- readonly meta: { readonly shape: "contract"; readonly contract: K }
246
- }
247
-
248
- /** Checked bound content used when a caller has already supplied every slot value. */
249
- export type BoundContractComponent<K extends ContractKey> = ContractSlots<K> | ContractProjection<K>
250
-
251
- /** Bound content by default; supplying `P` selects the stable component-type lane. */
252
- export type ContractComponent<K extends ContractKey, P = undefined> = [P] extends [undefined]
253
- ? BoundContractComponent<K>
254
- : ContractRenderComponent<K, P>
255
-
256
- /**
257
- * A leaf, typed by its NAME and the props it takes. It has no key, because it is not a node.
258
- *
259
- * The name is what separates two leaves that happen to accept the same props: a slot asking for a
260
- * glyph and a slot asking for a one-word label are not interchangeable, and without the name the
261
- * compiler would have no way to say so.
262
- *
263
- * THE NAME LIVES ON THE COMPONENT'S OWN METADATA, beside the tier marker every component already
264
- * carries. As a property rather than a module export, because a module export is invisible to the
265
- * type of the thing being passed - and this whole arrangement is worth having only if the compiler
266
- * can read it at the call site.
267
- *
268
- * ```tsx
269
- * export const Icon = ({ props, isLoading }: IconProps) => ...
270
- * Icon.meta = { tier: "leaf", name: "icon" } as const
271
- * ```
272
- */
273
- export type LeafComponent<N extends string, P> = {
274
- (): ReactNode
275
- readonly meta: { readonly shape: "leaf"; readonly name: N; readonly props: P }
276
- }
277
-
278
- type ContractChild<S> = S extends { readonly contract: infer K }
279
- ? K extends ContractKey
280
- ? ContractComponent<K>
281
- : K extends ReadonlyArray<infer A extends ContractKey> ? ContractComponent<A> : never
282
- : never
283
-
284
- type LeafChild<S> = S extends { readonly leaf: infer N }
285
- ? (N extends ReadonlyArray<infer A> ? A : N) extends infer L extends string
286
- ? LeafComponent<L, S extends { readonly props: infer P } ? P : Readonly<Record<never, never>>>
287
- : never
288
- : never
289
-
290
- type OneChild<S> = ContractChild<S> | LeafChild<S>
291
- type ChildValue<S> = S extends { readonly repeats: true } ? ReadonlyArray<OneChild<S>> : OneChild<S>
292
-
293
- type RequiredSlots<K extends ContractKey> = {
294
- [S in keyof (typeof CONTRACTS)[K]["children"]]:
295
- (typeof CONTRACTS)[K]["children"][S] extends { readonly optional: true } ? never : S
296
- }[keyof (typeof CONTRACTS)[K]["children"]]
297
-
298
- type OptionalSlots<K extends ContractKey> = Exclude<keyof (typeof CONTRACTS)[K]["children"], RequiredSlots<K>>
299
-
300
- /**
301
- * The components a key requires inside it, by slot name.
302
- *
303
- * This is the child contract as the compiler sees it. A leaf of the wrong name, a leaf with the
304
- * wrong props, a node where the entry asked for a leaf, a missing slot and a slot the entry never
305
- * declared are five different compile errors at the call site rather than five things a reviewer
306
- * has to notice.
307
- */
308
- export type ChildrenOf<K extends ContractKey> = {
309
- readonly [S in RequiredSlots<K>]: ChildValue<(typeof CONTRACTS)[K]["children"][S]>
310
- } & {
311
- readonly [S in OptionalSlots<K>]?: ChildValue<(typeof CONTRACTS)[K]["children"][S]>
312
- }
package/props.ts DELETED
@@ -1,145 +0,0 @@
1
- /**
2
- * TEMPLATE - `src/components/contracts/props.ts`.
3
- *
4
- * Copy this file to that path unchanged. Unlike the contract table beside it, there is nothing here
5
- * for a repository to grow: these are the slot shapes themselves, and a repository that edits them
6
- * has changed the rules rather than filled them in.
7
- *
8
- * THE SLOT SHAPES, AS TYPES RATHER THAN AS A CONVENTION.
9
- *
10
- * Every component below a block takes a FIXED set of named slots and no others. Written as a type
11
- * alias per tier, a fourth slot is not discouraged - it does not compile, because the alias is the
12
- * whole shape and there is nowhere to put one.
13
- *
14
- * This is the difference between a rule and a fence. `interface XProps { props: XData; isLoading?: boolean }`
15
- * is a rule: correct today, and one `extends` away from carrying a `className` next month.
16
- * `type XProps = LeafProps<XData>` is a fence.
17
- */
18
- import type { ContractComponent, ContractKey } from "./index"
19
-
20
- export type { InnerProps } from "./index"
21
-
22
- /**
23
- * What DATA is: anything a JSON document could hold.
24
- *
25
- * A function does not satisfy it, and that is the only thing stopping a component being smuggled
26
- * through `props` - which is why handlers travel in their own slot rather than beside the data.
27
- *
28
- * NOTE FOR AUTHORS: a component's data must be declared with `type`, not `interface`. TypeScript
29
- * gives an implicit index signature to a type alias and not to an interface, so an interface
30
- * silently fails this constraint. That is not a quirk to work around - it is the constraint doing
31
- * its job.
32
- */
33
- export type DataValue =
34
- | string
35
- | number
36
- | boolean
37
- | null
38
- | undefined
39
- | ReadonlyArray<DataValue>
40
- | { readonly [key: string]: DataValue }
41
-
42
- /** The shape any component's data must have: data all the way down. */
43
- export type ComponentData = { readonly [key: string]: DataValue }
44
-
45
- /** The shape any component's handlers must have: functions, kept apart from the data. */
46
- export type ComponentActions = { readonly [key: string]: ((...args: Array<never>) => void) | undefined }
47
-
48
- /**
49
- * A LEAF's props. Three slots, no fourth.
50
- *
51
- * `props` - what it draws. `on` - what it does. `isLoading` - handed down, never decided here.
52
- * No `render`: only a branch assembles. No `className`: a caller who can restyle a node has
53
- * become its second owner, and the component now has two authors who never speak.
54
- */
55
- export type LeafProps<D extends ComponentData, A extends ComponentActions = ComponentActions> = {
56
- readonly props: D
57
- readonly on?: A
58
- readonly isLoading?: boolean
59
- }
60
-
61
- /**
62
- * A COMPOSITE's props. The same three slots a leaf has, and the absence of a fourth is the point.
63
- *
64
- * A composite is a CLOSED shape: it assembles leaves into an arrangement a caller can use but
65
- * cannot rearrange. That boundary with `branch` is held by this type rather than by a convention -
66
- * there is no `render` here, so an author who wanted a slot cannot express one and has to decide
67
- * which tier they are actually writing. A boundary the compiler holds does not drift.
68
- * CLOSED DOES NOT MEAN FREEHAND. A composite still assembles through a typed `Tree` contract;
69
- * raw structural markup would create an unnamed layout outside the registry. If a caller may
70
- * supply the content or recipe, the component is a branch rather than a composite.
71
- *
72
- * It is structurally identical to {@link LeafProps} and deliberately NOT an alias of it: the two
73
- * tiers answer different questions, and a shared name would let a reader conclude they are the same
74
- * kind of thing. What separates them is what the file may DO, not what it may accept.
75
- */
76
- export type CompositeProps<D extends ComponentData, A extends ComponentActions = ComponentActions> = {
77
- readonly props: D
78
- readonly on?: A
79
- readonly isLoading?: boolean
80
- }
81
-
82
- /**
83
- * A BRANCH's props. The same three a leaf has, plus the key it draws and the components that fill
84
- * the slots that key declares.
85
- *
86
- * THIS IS THE SLOT THAT USED TO BE `children`, AND THE CHANGE IS NOT COSMETIC. `children` is markup
87
- * that has already been built: it can be a `.map`, a ternary, a fragment, or a subtree nobody named,
88
- * so nothing above it can say what is inside a node and no rule can check. `render` is a contract
89
- * value in one of two lanes. This alias is the bound lane: Tree and ordinary branches receive
90
- * named slots or an explicit projection. A data-driven surface uses {@link ContractRenderBranchProps}
91
- * to receive a stable component type and pass changing runtime `props` into it.
92
- *
93
- * `contract` and `render` are one decision written twice, and `ContractComponent<K>` ties them together:
94
- * the key fixes which slots exist and what may fill each, so a card cannot say one thing and hold
95
- * another. A wrong leaf, wrong leaf props, a node where a leaf was asked for, a missing slot and a
96
- * slot nobody declared are five compile errors rather than five things a reviewer has to spot.
97
- *
98
- * A bare callback or JSX value may not arrive: neither carries the exact key metadata that makes
99
- * `contract` and `render` one checked decision.
100
- *
101
- * THERE IS NO LIST VARIANT, and there does not need to be. A slot that holds many says so in the
102
- * table with `repeats: true`; that slot becomes an array. `restingCount` separately fixes how many
103
- * placeholders draw while loading, because live length is dynamic and is not the skeleton count.
104
- */
105
- export type BranchProps<
106
- D extends ComponentData,
107
- K extends ContractKey,
108
- A extends ComponentActions = ComponentActions,
109
- > = {
110
- readonly props: D
111
- readonly on?: A
112
- readonly contract: K
113
- readonly render: ContractComponent<NoInfer<K>>
114
- readonly isLoading?: boolean
115
- }
116
-
117
- /**
118
- * A data-driven contract host's props. Runtime data stays in `props`; `render` is a stable real
119
- * component type branded for the exact key. `SurfaceListCard` uses this lane because a repeated
120
- * domain collection must not be closed into a new callback descriptor at every host call site.
121
- */
122
- export type ContractRenderBranchProps<
123
- D extends ComponentData,
124
- K extends ContractKey,
125
- A extends ComponentActions = ComponentActions,
126
- > = {
127
- readonly props: D
128
- readonly on?: A
129
- readonly contract: K
130
- readonly render: ContractComponent<NoInfer<K>, LeafProps<D, A>>
131
- readonly isLoading?: boolean
132
- }
133
-
134
- /**
135
- * A BLOCK's presentational half. Two slots.
136
- *
137
- * `state` is the business situation and it picks a tree; `props` is what that tree says. There is
138
- * no `isLoading` here - a block WRITES the flag when it hands a tree down, and never receives one.
139
- * The type is a union per state at the call site, so the data of a situation a surface is NOT in
140
- * cannot be passed, and the data of the one it IS in cannot be omitted.
141
- */
142
- export type BlockProps<S extends string, D extends ComponentData> = {
143
- readonly state: S
144
- readonly props: D
145
- }