@charcuterie/ui 0.2.0 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/Field/Field.d.ts +41 -6
- package/dist/Field/Field.d.ts.map +1 -1
- package/dist/Field/Field.js +50 -8
- package/dist/Field/Field.js.map +1 -1
- package/dist/Field/FieldGroup.d.ts +58 -0
- package/dist/Field/FieldGroup.d.ts.map +1 -0
- package/dist/Field/FieldGroup.js +65 -0
- package/dist/Field/FieldGroup.js.map +1 -0
- package/dist/LogViewer/LogViewer.d.ts +3 -1
- package/dist/LogViewer/LogViewer.d.ts.map +1 -1
- package/dist/LogViewer/LogViewer.js +62 -1
- package/dist/LogViewer/LogViewer.js.map +1 -1
- package/dist/Select/Select.d.ts +10 -2
- package/dist/Select/Select.d.ts.map +1 -1
- package/dist/Select/Select.js.map +1 -1
- package/dist/Tooltip/Tooltip.d.ts +16 -2
- package/dist/Tooltip/Tooltip.d.ts.map +1 -1
- package/dist/Tooltip/Tooltip.js +23 -3
- package/dist/Tooltip/Tooltip.js.map +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/slotProps.d.ts +121 -0
- package/dist/slotProps.d.ts.map +1 -0
- package/dist/slotProps.js +55 -0
- package/dist/slotProps.js.map +1 -0
- package/dist/slotWiring.d.ts +32 -0
- package/dist/slotWiring.d.ts.map +1 -0
- package/dist/slotWiring.js +115 -0
- package/dist/slotWiring.js.map +1 -0
- package/package.json +3 -3
- package/src/Field/Field.mdx +73 -3
- package/src/Field/Field.stories.tsx +136 -0
- package/src/Field/Field.test.tsx +207 -5
- package/src/Field/Field.tsx +70 -21
- package/src/Field/FieldGroup.tsx +138 -0
- package/src/LogViewer/LogViewer.mdx +36 -1
- package/src/LogViewer/LogViewer.stories.tsx +34 -0
- package/src/LogViewer/LogViewer.test.tsx +65 -2
- package/src/LogViewer/LogViewer.tsx +67 -1
- package/src/Menu/Menu.mdx +23 -0
- package/src/Menu/Menu.stories.tsx +67 -0
- package/src/Menu/Menu.test.tsx +113 -2
- package/src/Popover/Popover.mdx +5 -0
- package/src/Select/Select.stories.tsx +10 -2
- package/src/Select/Select.tsx +10 -2
- package/src/Tooltip/Tooltip.tsx +29 -5
- package/src/index.ts +4 -0
- package/src/slotProps.ts +163 -0
- package/src/slotWiring.ts +173 -0
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AAOH,OAAO,EAAE,SAAS,EAAE,MAAM,0BAA2B,CAAA;AAKrD,OAAO,EAAE,KAAK,EAAE,MAAM,kBAAmB,CAAA;AAEzC,OAAO,EAAE,KAAK,EAAE,MAAM,kBAAmB,CAAA;AAEzC,OAAO,EAAE,MAAM,EAAE,MAAM,oBAAqB,CAAA;AAM5C,OAAO,EAAE,IAAI,EAAE,MAAM,gBAAiB,CAAA;AAEtC,OAAO,EACL,gBAAgB,EAChB,kBAAkB,EAClB,cAAc,EACd,uBAAuB,EACvB,sBAAsB,EACtB,kBAAkB,GACnB,MAAM,oBAAoB,CAAA;AAK3B,OAAO,EAAE,UAAU,EAAE,MAAM,4BAA6B,CAAA;AAExD,OAAO,EAAE,KAAK,EAAE,MAAM,kBAAmB,CAAA;AAEzC,OAAO,EAAE,YAAY,EAAE,MAAM,gCAAiC,CAAA;AAE9D,OAAO,EAAE,UAAU,EAAE,MAAM,4BAA6B,CAAA;AAExD,OAAO,EACL,cAAc,EACd,gBAAgB,EAChB,uBAAuB,EACvB,oBAAoB,EACpB,kBAAkB,EAClB,uBAAuB,GACxB,MAAM,mBAAmB,CAAA;AAK1B,OAAO,EAAE,mBAAmB,EAAE,MAAM,8CAA+C,CAAA;AAKnF,OAAO,EAAE,SAAS,EAAE,MAAM,0BAA2B,CAAA;AAKrD,OAAO,EAAE,SAAS,EAAE,MAAM,0BAA2B,CAAA;AAErD,OAAO,EAAE,gBAAgB,EAAE,MAAM,4BAA4B,CAAA;AAK7D,OAAO,EAAE,IAAI,EAAE,MAAM,gBAAiB,CAAA;AAKtC,OAAO,EAAE,KAAK,EAAE,MAAM,kBAAmB,CAAA;AAEzC,OAAO,EAAE,OAAO,EAAE,MAAM,sBAAuB,CAAA;AAK/C,OAAO,EAAE,WAAW,EAAE,MAAM,8BAA+B,CAAA;AAE3D,OAAO,EACL,iBAAiB,EACjB,iBAAiB,EACjB,eAAe,GAChB,MAAM,gCAAgC,CAAA;AAKvC,OAAO,EAAE,gBAAgB,EAAE,MAAM,wCAAyC,CAAA;AAO1E,OAAO,EAAE,MAAM,EAAE,MAAM,oBAAqB,CAAA;AAK5C,OAAO,EAAE,QAAQ,EAAE,MAAM,wBAAyB,CAAA;AAKlD,OAAO,EAAE,mBAAmB,EAAE,MAAM,8CAA+C,CAAA;AAEnF,OAAO,EAAE,OAAO,EAAE,MAAM,sBAAuB,CAAA;AAC/C,OAAO,EACL,cAAc,EACd,aAAa,EACb,mBAAmB,EACnB,kBAAkB,EAClB,mBAAmB,GACpB,MAAM,mBAAmB,CAAA;AAO1B,OAAO,EAAE,IAAI,EAAE,MAAM,gBAAiB,CAAA;AAEtC,OAAO,EAAE,KAAK,EAAE,MAAM,kBAAmB,CAAA;AAKzC,OAAO,EAAE,WAAW,EAAE,MAAM,wBAAyB,CAAA;AAErD,OAAO,EAAE,gBAAgB,EAAE,MAAM,2BAA2B,CAAA;AAE5D,OAAO,EAAE,OAAO,EAAE,MAAM,sBAAuB,CAAA;AAC/C,OAAO,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAA;AAE9C,OAAO,EAAE,cAAc,EAAE,MAAM,oCAAqC,CAAA"}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AAOH,OAAO,EAAE,SAAS,EAAE,MAAM,0BAA2B,CAAA;AAKrD,OAAO,EAAE,KAAK,EAAE,MAAM,kBAAmB,CAAA;AAEzC,OAAO,EAAE,KAAK,EAAE,MAAM,kBAAmB,CAAA;AAEzC,OAAO,EAAE,MAAM,EAAE,MAAM,oBAAqB,CAAA;AAM5C,OAAO,EAAE,IAAI,EAAE,MAAM,gBAAiB,CAAA;AAEtC,OAAO,EACL,gBAAgB,EAChB,kBAAkB,EAClB,cAAc,EACd,uBAAuB,EACvB,sBAAsB,EACtB,kBAAkB,GACnB,MAAM,oBAAoB,CAAA;AAK3B,OAAO,EAAE,UAAU,EAAE,MAAM,4BAA6B,CAAA;AAExD,OAAO,EAAE,KAAK,EAAE,MAAM,kBAAmB,CAAA;AAEzC,OAAO,EAAE,UAAU,EAAE,MAAM,uBAAwB,CAAA;AAEnD,OAAO,EAAE,YAAY,EAAE,MAAM,gCAAiC,CAAA;AAE9D,OAAO,EAAE,UAAU,EAAE,MAAM,4BAA6B,CAAA;AAExD,OAAO,EACL,cAAc,EACd,gBAAgB,EAChB,uBAAuB,EACvB,oBAAoB,EACpB,kBAAkB,EAClB,uBAAuB,GACxB,MAAM,mBAAmB,CAAA;AAK1B,OAAO,EAAE,mBAAmB,EAAE,MAAM,8CAA+C,CAAA;AAKnF,OAAO,EAAE,SAAS,EAAE,MAAM,0BAA2B,CAAA;AAKrD,OAAO,EAAE,SAAS,EAAE,MAAM,0BAA2B,CAAA;AAErD,OAAO,EAAE,gBAAgB,EAAE,MAAM,4BAA4B,CAAA;AAK7D,OAAO,EAAE,IAAI,EAAE,MAAM,gBAAiB,CAAA;AAKtC,OAAO,EAAE,KAAK,EAAE,MAAM,kBAAmB,CAAA;AAEzC,OAAO,EAAE,OAAO,EAAE,MAAM,sBAAuB,CAAA;AAK/C,OAAO,EAAE,WAAW,EAAE,MAAM,8BAA+B,CAAA;AAE3D,OAAO,EACL,iBAAiB,EACjB,iBAAiB,EACjB,eAAe,GAChB,MAAM,gCAAgC,CAAA;AAKvC,OAAO,EAAE,gBAAgB,EAAE,MAAM,wCAAyC,CAAA;AAO1E,OAAO,EAAE,MAAM,EAAE,MAAM,oBAAqB,CAAA;AAK5C,OAAO,EAAE,QAAQ,EAAE,MAAM,wBAAyB,CAAA;AAKlD,OAAO,EAAE,mBAAmB,EAAE,MAAM,8CAA+C,CAAA;AAEnF,OAAO,EAAE,OAAO,EAAE,MAAM,sBAAuB,CAAA;AAE/C,OAAO,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAA;AAC/C,OAAO,EACL,cAAc,EACd,aAAa,EACb,mBAAmB,EACnB,kBAAkB,EAClB,mBAAmB,GACpB,MAAM,mBAAmB,CAAA;AAO1B,OAAO,EAAE,IAAI,EAAE,MAAM,gBAAiB,CAAA;AAEtC,OAAO,EAAE,KAAK,EAAE,MAAM,kBAAmB,CAAA;AAKzC,OAAO,EAAE,WAAW,EAAE,MAAM,wBAAyB,CAAA;AAErD,OAAO,EAAE,gBAAgB,EAAE,MAAM,2BAA2B,CAAA;AAE5D,OAAO,EAAE,OAAO,EAAE,MAAM,sBAAuB,CAAA;AAC/C,OAAO,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAA;AAE9C,OAAO,EAAE,cAAc,EAAE,MAAM,oCAAqC,CAAA"}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import type { InputHTMLAttributes } from "react";
|
|
2
|
+
/**
|
|
3
|
+
* The props a **cloning ancestor** injects into the control below it.
|
|
4
|
+
*
|
|
5
|
+
* `Field` and `Tooltip` both take one child and `cloneElement` onto
|
|
6
|
+
* it — "cloned, not wrapped", which is what lets a `Field` go around
|
|
7
|
+
* an `<input>`, a `Select`, or an app's own control without changing
|
|
8
|
+
* that control's layout. The contract has a hole in it the moment two
|
|
9
|
+
* of them are nested:
|
|
10
|
+
*
|
|
11
|
+
* ```tsx
|
|
12
|
+
* <Field label="Pattern" isRequired>
|
|
13
|
+
* <Tooltip label="A JavaScript regular expression">
|
|
14
|
+
* <input />
|
|
15
|
+
* </Tooltip>
|
|
16
|
+
* </Field>
|
|
17
|
+
* ```
|
|
18
|
+
*
|
|
19
|
+
* `Field` clones onto the **`Tooltip` element**, so `id`,
|
|
20
|
+
* `aria-describedby`, `aria-invalid` and `required` are handed to a
|
|
21
|
+
* component that declares none of them — and `cloneElement` does not
|
|
22
|
+
* care. React drops them without a warning, TypeScript never sees
|
|
23
|
+
* them (`Children.only` returns a `ReactElement` with `any` props),
|
|
24
|
+
* every test still passes, and the render is pixel-identical. The
|
|
25
|
+
* `<label>` then points at an `id` that is nowhere in the document.
|
|
26
|
+
*
|
|
27
|
+
* **Silent prop-dropping is the worst version of this**, so the rule
|
|
28
|
+
* is: *a slot component is a pass-through*. Anything in `SlotProps`
|
|
29
|
+
* that arrives from above is forwarded to the child this component
|
|
30
|
+
* clones onto, so a chain of slots behaves like the one control at
|
|
31
|
+
* the bottom of it.
|
|
32
|
+
*
|
|
33
|
+
* ### The list is closed, the forwarding is not
|
|
34
|
+
*
|
|
35
|
+
* Worth being exact, because the two are easy to conflate. This type
|
|
36
|
+
* is a closed list of five keys rather than an index signature: it is
|
|
37
|
+
* exactly the wiring `Field` writes plus the wiring `Tooltip` writes,
|
|
38
|
+
* it is greppable, and it is the set whose **merge semantics are
|
|
39
|
+
* defined** — which for four of them is last-write-wins and for
|
|
40
|
+
* `aria-describedby` is a join.
|
|
41
|
+
*
|
|
42
|
+
* The *runtime* forwards everything. Both components collect their
|
|
43
|
+
* received props with a rest spread and `mergeSlotProps` starts from
|
|
44
|
+
* `...receivedProps`, so an ancestor's `onPointerEnter`, its
|
|
45
|
+
* `onFocus`, its `ref` — none of which are named here — reach the
|
|
46
|
+
* control at the bottom too. That is not an oversight, it is load
|
|
47
|
+
* bearing: a `Tooltip` around a `Field` hands down `useHover`,
|
|
48
|
+
* `useFocus`, `useDismiss` and `refs.setReference`, and a `Field`
|
|
49
|
+
* that forwarded only the five keys below would leave the tip with no
|
|
50
|
+
* trigger and no anchor. A closed *runtime* filter would be
|
|
51
|
+
* mux-magic's `FieldTooltip` defect rebuilt inside the fix for it.
|
|
52
|
+
*
|
|
53
|
+
* The five keys, and who writes them:
|
|
54
|
+
*
|
|
55
|
+
* | Key | Written by |
|
|
56
|
+
* | --- | --- |
|
|
57
|
+
* | `aria-describedby` | `Field` (its description and its error) **and** `Tooltip` (its tip) — the one key that is a list, and the reason `mergeSlotProps` exists |
|
|
58
|
+
* | `aria-invalid` | `Field`, derived from `error` being present |
|
|
59
|
+
* | `aria-required` | `Field`, from `isRequired` — the announced constraint, for a composite control the browser does not validate |
|
|
60
|
+
* | `id` | `Field`, so its `<label htmlFor>` has something to point at. A caller may also pass one directly, which is how a consumer pins a stable id for a deep link or an autofill hint |
|
|
61
|
+
* | `required` | `Field`, from `isRequired` — the constraint the browser validates |
|
|
62
|
+
*
|
|
63
|
+
* ### Why these are `Pick`ed and not written out
|
|
64
|
+
*
|
|
65
|
+
* Three of the five are booleans whose names cannot start with `is`
|
|
66
|
+
* or `has`, because they are not our names — they are the DOM's, and
|
|
67
|
+
* they are spread onto a DOM node verbatim. The house rule has
|
|
68
|
+
* [no carve-out](../../../docs/decisions/2026-07-29-is-has-rule-has-no-external-api-carve-out.md)
|
|
69
|
+
* for that: a *type property* is a shape we own, an object literal
|
|
70
|
+
* key is someone else's contract, and no `eslint-disable` is
|
|
71
|
+
* available because unused disable directives are themselves an
|
|
72
|
+
* error.
|
|
73
|
+
*
|
|
74
|
+
* So this type does not declare a shape. It **selects** one out of
|
|
75
|
+
* React's own `InputHTMLAttributes`, which is the honest description
|
|
76
|
+
* of what it is: five keys of the props React already types for an
|
|
77
|
+
* `<input>`, forwarded unchanged. The same reasoning as the
|
|
78
|
+
* `Record<string, boolean>` in `createReactAdapter` — we are not
|
|
79
|
+
* describing React's shape, we are naming the keys of it we touch —
|
|
80
|
+
* and it has the side benefit that `aria-invalid` gets React's real
|
|
81
|
+
* type (`"grammar"` and `"spelling"` are legal values) rather than
|
|
82
|
+
* the `boolean` a hand-written version would have narrowed it to.
|
|
83
|
+
*/
|
|
84
|
+
export type SlotProps = Pick<InputHTMLAttributes<HTMLElement>, "aria-describedby" | "aria-invalid" | "aria-required" | "id" | "required">;
|
|
85
|
+
/**
|
|
86
|
+
* Merge what arrived from above with what this component writes
|
|
87
|
+
* itself.
|
|
88
|
+
*
|
|
89
|
+
* Everything here is a single value and last-write-wins is correct
|
|
90
|
+
* for it — **except `aria-describedby`, which is a list**. Two slot
|
|
91
|
+
* components each writing their own onto one control is the entire
|
|
92
|
+
* nesting problem in one attribute: a `Field` naming its description
|
|
93
|
+
* and its error, and a `Tooltip` naming its tip. A plain spread keeps
|
|
94
|
+
* one of them and loses the other, which is the same silent drop by a
|
|
95
|
+
* shorter route.
|
|
96
|
+
*
|
|
97
|
+
* **Outer first.** A screen reader reads the list in order, and the
|
|
98
|
+
* field's own description and error are the control's primary
|
|
99
|
+
* explanation; a tip is supplementary by construction. The ordering
|
|
100
|
+
* inside a single `Field` — description, then error — is `Field`'s
|
|
101
|
+
* own concern and is unaffected.
|
|
102
|
+
*
|
|
103
|
+
* `undefined` in `ownProps` is *absence*, not an instruction to
|
|
104
|
+
* clear. Both callers spell their optional wiring as
|
|
105
|
+
* `isRequired || undefined`, so a plain spread would let an inner
|
|
106
|
+
* `Field` with no error erase an outer one's `aria-invalid` — the
|
|
107
|
+
* same silent drop, one level down.
|
|
108
|
+
*
|
|
109
|
+
* ### The wiring is not a value, and does not last-write-win
|
|
110
|
+
*
|
|
111
|
+
* `ref` and the `on*` handlers are merged rather than replaced, by
|
|
112
|
+
* `mergeSlotWiring`. This was the 1.0.0 hole: `mergeSlotProps`
|
|
113
|
+
* settled the five *attributes* and left the two props that are not
|
|
114
|
+
* attributes on last-write-wins, so a `Menu` and a `Tooltip` on one
|
|
115
|
+
* trigger each handed it a floating-ui `refs.setReference` and the
|
|
116
|
+
* inner one won. The outer panel had no anchor and rendered in the
|
|
117
|
+
* corner of the viewport — no error, no failing test, and it looks
|
|
118
|
+
* like a CSS bug.
|
|
119
|
+
*/
|
|
120
|
+
export declare const mergeSlotProps: <OwnProps extends Record<string, unknown>>(receivedProps: SlotProps, ownProps: OwnProps) => OwnProps & SlotProps;
|
|
121
|
+
//# sourceMappingURL=slotProps.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"slotProps.d.ts","sourceRoot":"","sources":["../src/slotProps.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,OAAO,CAAA;AAIhD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiFG;AACH,MAAM,MAAM,SAAS,GAAG,IAAI,CAC1B,mBAAmB,CAAC,WAAW,CAAC,EAC9B,kBAAkB,GAClB,cAAc,GACd,eAAe,GACf,IAAI,GACJ,UAAU,CACb,CAAA;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AACH,eAAO,MAAM,cAAc,GACzB,QAAQ,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAExC,eAAe,SAAS,EACxB,UAAU,QAAQ,KACjB,QAAQ,GAAG,SA2Bb,CAAA"}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { mergeSlotWiring } from "./slotWiring.js";
|
|
2
|
+
/**
|
|
3
|
+
* Merge what arrived from above with what this component writes
|
|
4
|
+
* itself.
|
|
5
|
+
*
|
|
6
|
+
* Everything here is a single value and last-write-wins is correct
|
|
7
|
+
* for it — **except `aria-describedby`, which is a list**. Two slot
|
|
8
|
+
* components each writing their own onto one control is the entire
|
|
9
|
+
* nesting problem in one attribute: a `Field` naming its description
|
|
10
|
+
* and its error, and a `Tooltip` naming its tip. A plain spread keeps
|
|
11
|
+
* one of them and loses the other, which is the same silent drop by a
|
|
12
|
+
* shorter route.
|
|
13
|
+
*
|
|
14
|
+
* **Outer first.** A screen reader reads the list in order, and the
|
|
15
|
+
* field's own description and error are the control's primary
|
|
16
|
+
* explanation; a tip is supplementary by construction. The ordering
|
|
17
|
+
* inside a single `Field` — description, then error — is `Field`'s
|
|
18
|
+
* own concern and is unaffected.
|
|
19
|
+
*
|
|
20
|
+
* `undefined` in `ownProps` is *absence*, not an instruction to
|
|
21
|
+
* clear. Both callers spell their optional wiring as
|
|
22
|
+
* `isRequired || undefined`, so a plain spread would let an inner
|
|
23
|
+
* `Field` with no error erase an outer one's `aria-invalid` — the
|
|
24
|
+
* same silent drop, one level down.
|
|
25
|
+
*
|
|
26
|
+
* ### The wiring is not a value, and does not last-write-win
|
|
27
|
+
*
|
|
28
|
+
* `ref` and the `on*` handlers are merged rather than replaced, by
|
|
29
|
+
* `mergeSlotWiring`. This was the 1.0.0 hole: `mergeSlotProps`
|
|
30
|
+
* settled the five *attributes* and left the two props that are not
|
|
31
|
+
* attributes on last-write-wins, so a `Menu` and a `Tooltip` on one
|
|
32
|
+
* trigger each handed it a floating-ui `refs.setReference` and the
|
|
33
|
+
* inner one won. The outer panel had no anchor and rendered in the
|
|
34
|
+
* corner of the viewport — no error, no failing test, and it looks
|
|
35
|
+
* like a CSS bug.
|
|
36
|
+
*/
|
|
37
|
+
export const mergeSlotProps = (receivedProps, ownProps) => {
|
|
38
|
+
const definedOwnProps = Object.fromEntries(Object.entries(ownProps).filter(([, value]) => value !== undefined));
|
|
39
|
+
const describedBy = [
|
|
40
|
+
receivedProps["aria-describedby"],
|
|
41
|
+
ownProps["aria-describedby"],
|
|
42
|
+
]
|
|
43
|
+
.filter(Boolean)
|
|
44
|
+
.join(" ");
|
|
45
|
+
return {
|
|
46
|
+
...receivedProps,
|
|
47
|
+
...definedOwnProps,
|
|
48
|
+
"aria-describedby": describedBy === "" ? undefined : describedBy,
|
|
49
|
+
// Last, because it is the only merge that has to beat
|
|
50
|
+
// `definedOwnProps` — it is composing what that spread would
|
|
51
|
+
// otherwise have thrown away.
|
|
52
|
+
...mergeSlotWiring(receivedProps, definedOwnProps),
|
|
53
|
+
};
|
|
54
|
+
};
|
|
55
|
+
//# sourceMappingURL=slotProps.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"slotProps.js","sourceRoot":"","sources":["../src/slotProps.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAA;AA6FjD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AACH,MAAM,CAAC,MAAM,cAAc,GAAG,CAG5B,aAAwB,EACxB,QAAkB,EACI,EAAE;IACxB,MAAM,eAAe,GAAG,MAAM,CAAC,WAAW,CACxC,MAAM,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC,MAAM,CAC7B,CAAC,CAAC,EAAE,KAAK,CAAC,EAAE,EAAE,CAAC,KAAK,KAAK,SAAS,CACnC,CACU,CAAA;IAEb,MAAM,WAAW,GAAG;QAClB,aAAa,CAAC,kBAAkB,CAAC;QACjC,QAAQ,CAAC,kBAAkB,CAAC;KAC7B;SACE,MAAM,CAAC,OAAO,CAAC;SACf,IAAI,CAAC,GAAG,CAAC,CAAA;IAEZ,OAAO;QACL,GAAG,aAAa;QAChB,GAAG,eAAe;QAClB,kBAAkB,EAChB,WAAW,KAAK,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,WAAW;QAC9C,sDAAsD;QACtD,6DAA6D;QAC7D,8BAA8B;QAC9B,GAAG,eAAe,CAChB,aAAwC,EACxC,eAAe,CAChB;KACF,CAAA;AACH,CAAC,CAAA"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The two props a slot may not overwrite, and how it merges them.
|
|
3
|
+
*
|
|
4
|
+
* `mergeSlotProps` next door settles the five *attributes* two
|
|
5
|
+
* nested slots both write, and last-write-wins is right for four of
|
|
6
|
+
* them. It is not right for the wiring, because neither of these is
|
|
7
|
+
* a value:
|
|
8
|
+
*
|
|
9
|
+
* - **`ref`** is a subscription. `Menu` and `Tooltip` on one trigger
|
|
10
|
+
* both hand it a floating-ui `refs.setReference` — the anchor each
|
|
11
|
+
* of them positions its panel against — and the inner one used to
|
|
12
|
+
* replace the outer one's. Nothing threw. The outer panel simply
|
|
13
|
+
* had no reference element, so floating-ui left it at
|
|
14
|
+
* `left: 0; top: 0` in the corner of the viewport, which reads as
|
|
15
|
+
* a CSS bug and is not one.
|
|
16
|
+
* - **`on*`** is a listener, and listeners are plural.
|
|
17
|
+
*
|
|
18
|
+
* ### Why this is not imported from `@charcuterie/logic`
|
|
19
|
+
*
|
|
20
|
+
* `useClonedChild`'s `mergeClonedProps` applies the same rule one
|
|
21
|
+
* level down — between a slot and the element a caller wrote — and
|
|
22
|
+
* the two are near-identical. Sharing them means a new public export
|
|
23
|
+
* on `@charcuterie/logic`, which is a `minor`; this is a patch, and
|
|
24
|
+
* the duplication is thirty lines against a wrong version number.
|
|
25
|
+
* Queued in `docs/2026-08-02-reported-by-consumers-queued-for-1-1.md`.
|
|
26
|
+
*/
|
|
27
|
+
/**
|
|
28
|
+
* The keys of `ownProps` that must not simply replace the ones
|
|
29
|
+
* `receivedProps` arrived with. Spread **last**, over both.
|
|
30
|
+
*/
|
|
31
|
+
export declare const mergeSlotWiring: (receivedProps: Record<string, unknown>, ownProps: Record<string, unknown>) => Record<string, unknown>;
|
|
32
|
+
//# sourceMappingURL=slotWiring.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"slotWiring.d.ts","sourceRoot":"","sources":["../src/slotWiring.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAuGH;;;GAGG;AACH,eAAO,MAAM,eAAe,GAC1B,eAAe,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACtC,UAAU,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAChC,MAAM,CAAC,MAAM,EAAE,OAAO,CAqCtB,CAAA"}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The two props a slot may not overwrite, and how it merges them.
|
|
3
|
+
*
|
|
4
|
+
* `mergeSlotProps` next door settles the five *attributes* two
|
|
5
|
+
* nested slots both write, and last-write-wins is right for four of
|
|
6
|
+
* them. It is not right for the wiring, because neither of these is
|
|
7
|
+
* a value:
|
|
8
|
+
*
|
|
9
|
+
* - **`ref`** is a subscription. `Menu` and `Tooltip` on one trigger
|
|
10
|
+
* both hand it a floating-ui `refs.setReference` — the anchor each
|
|
11
|
+
* of them positions its panel against — and the inner one used to
|
|
12
|
+
* replace the outer one's. Nothing threw. The outer panel simply
|
|
13
|
+
* had no reference element, so floating-ui left it at
|
|
14
|
+
* `left: 0; top: 0` in the corner of the viewport, which reads as
|
|
15
|
+
* a CSS bug and is not one.
|
|
16
|
+
* - **`on*`** is a listener, and listeners are plural.
|
|
17
|
+
*
|
|
18
|
+
* ### Why this is not imported from `@charcuterie/logic`
|
|
19
|
+
*
|
|
20
|
+
* `useClonedChild`'s `mergeClonedProps` applies the same rule one
|
|
21
|
+
* level down — between a slot and the element a caller wrote — and
|
|
22
|
+
* the two are near-identical. Sharing them means a new public export
|
|
23
|
+
* on `@charcuterie/logic`, which is a `minor`; this is a patch, and
|
|
24
|
+
* the duplication is thirty lines against a wrong version number.
|
|
25
|
+
* Queued in `docs/2026-08-02-reported-by-consumers-queued-for-1-1.md`.
|
|
26
|
+
*/
|
|
27
|
+
const isEventHandlerName = (name) => /^on[A-Z]/.test(name);
|
|
28
|
+
const isMergeableRef = (value) => typeof value === "function" ||
|
|
29
|
+
(typeof value === "object" &&
|
|
30
|
+
value !== null &&
|
|
31
|
+
"current" in value);
|
|
32
|
+
const setRef = (ref, node) => {
|
|
33
|
+
if (typeof ref === "function") {
|
|
34
|
+
return ref(node);
|
|
35
|
+
}
|
|
36
|
+
ref.current = node;
|
|
37
|
+
return undefined;
|
|
38
|
+
};
|
|
39
|
+
/**
|
|
40
|
+
* Every pair of refs ever merged, so the same two always come back
|
|
41
|
+
* as the **same function** — which is load-bearing rather than
|
|
42
|
+
* frugal.
|
|
43
|
+
*
|
|
44
|
+
* React re-runs a callback ref whenever its identity changes between
|
|
45
|
+
* renders, detaching with the old one and attaching with the new.
|
|
46
|
+
* Both refs here are floating-ui `setReference`s, and calling one
|
|
47
|
+
* sets state; a merged ref rebuilt every render would call both
|
|
48
|
+
* twice per render, each call scheduling the render that rebuilds
|
|
49
|
+
* it. `mergeSlotProps` is a plain function called during render, so
|
|
50
|
+
* it cannot reach for `useMemo` — the memo has to be in the identity
|
|
51
|
+
* of the inputs, which is what a `WeakMap` of `WeakMap`s is.
|
|
52
|
+
*/
|
|
53
|
+
const mergedRefs = new WeakMap();
|
|
54
|
+
const mergeRefs = (first, second) => {
|
|
55
|
+
const bySecond = mergedRefs.get(first) ??
|
|
56
|
+
new WeakMap();
|
|
57
|
+
mergedRefs.set(first, bySecond);
|
|
58
|
+
const cached = bySecond.get(second);
|
|
59
|
+
if (cached) {
|
|
60
|
+
return cached;
|
|
61
|
+
}
|
|
62
|
+
const merged = (node) => {
|
|
63
|
+
const cleanups = [
|
|
64
|
+
setRef(first, node),
|
|
65
|
+
setRef(second, node),
|
|
66
|
+
];
|
|
67
|
+
// Per ref: one that returned a cleanup has opted out of being
|
|
68
|
+
// called with `null`, and one that did not still expects it.
|
|
69
|
+
return () => {
|
|
70
|
+
for (const [index, cleanup] of cleanups.entries()) {
|
|
71
|
+
if (typeof cleanup === "function") {
|
|
72
|
+
cleanup();
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
setRef(index === 0 ? first : second, null);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
};
|
|
80
|
+
bySecond.set(second, merged);
|
|
81
|
+
return merged;
|
|
82
|
+
};
|
|
83
|
+
const chainHandlers = (first, second) => (...args) => {
|
|
84
|
+
// Outer first, the same order `aria-describedby` is joined in:
|
|
85
|
+
// the merge order is structural — whoever wrapped whom — and the
|
|
86
|
+
// author of the nesting is the one who chose it.
|
|
87
|
+
first(...args);
|
|
88
|
+
second(...args);
|
|
89
|
+
};
|
|
90
|
+
/**
|
|
91
|
+
* The keys of `ownProps` that must not simply replace the ones
|
|
92
|
+
* `receivedProps` arrived with. Spread **last**, over both.
|
|
93
|
+
*/
|
|
94
|
+
export const mergeSlotWiring = (receivedProps, ownProps) => Object.fromEntries(Object.entries(ownProps).flatMap(([name, ownValue]) => {
|
|
95
|
+
const receivedValue = receivedProps[name];
|
|
96
|
+
if (name === "ref" &&
|
|
97
|
+
isMergeableRef(receivedValue) &&
|
|
98
|
+
isMergeableRef(ownValue)) {
|
|
99
|
+
return [
|
|
100
|
+
[name, mergeRefs(receivedValue, ownValue)],
|
|
101
|
+
];
|
|
102
|
+
}
|
|
103
|
+
if (isEventHandlerName(name) &&
|
|
104
|
+
typeof receivedValue === "function" &&
|
|
105
|
+
typeof ownValue === "function") {
|
|
106
|
+
return [
|
|
107
|
+
[
|
|
108
|
+
name,
|
|
109
|
+
chainHandlers(receivedValue, ownValue),
|
|
110
|
+
],
|
|
111
|
+
];
|
|
112
|
+
}
|
|
113
|
+
return [];
|
|
114
|
+
}));
|
|
115
|
+
//# sourceMappingURL=slotWiring.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"slotWiring.js","sourceRoot":"","sources":["../src/slotWiring.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@charcuterie/ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "1.0.1",
|
|
4
4
|
"description": "The Charcuterie components. Tokens for look, @charcuterie/logic for state, ARIA that agents can actually drive.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -43,8 +43,8 @@
|
|
|
43
43
|
"typecheck": "tsc --noEmit"
|
|
44
44
|
},
|
|
45
45
|
"dependencies": {
|
|
46
|
-
"@charcuterie/logic": "0.1
|
|
47
|
-
"@charcuterie/tokens": "0.
|
|
46
|
+
"@charcuterie/logic": "1.0.1",
|
|
47
|
+
"@charcuterie/tokens": "1.0.0",
|
|
48
48
|
"@floating-ui/react": "^0.27.20"
|
|
49
49
|
},
|
|
50
50
|
"peerDependencies": {
|
package/src/Field/Field.mdx
CHANGED
|
@@ -7,9 +7,23 @@ import * as FieldStories from "./Field.stories";
|
|
|
7
7
|
# Field
|
|
8
8
|
|
|
9
9
|
Sixteen files in the fleet spell a label, a control, and a hint, and no two spell the
|
|
10
|
-
wiring the same way.
|
|
11
|
-
|
|
12
|
-
|
|
10
|
+
wiring the same way.
|
|
11
|
+
|
|
12
|
+
An earlier version of this page said mux-magic's `FieldLabel` renders a `<label>` with
|
|
13
|
+
**no `htmlFor`**. That is false, and worth correcting rather than quietly deleting: it
|
|
14
|
+
renders ``htmlFor={`${stepId}-${field.name}`}``, and has since it was written. The defect
|
|
15
|
+
is at the *other* end of the pair — **8 of its 16 call sites never render that id on
|
|
16
|
+
anything**. Measured on `mux-magic@master`, the eight that do are `BooleanField`,
|
|
17
|
+
`ChapterSplitsField`, `EnumField`, `NumberField`, `NumberWithLookupField`, `PathField`,
|
|
18
|
+
`StringArrayField` and `StringField`. Of the rest, `RegexWithFlagsField` mints its own
|
|
19
|
+
`patternId` / `flagsId` / `sampleId` and `SubtitleRulesField` spells
|
|
20
|
+
``${step.command}-hasDefaultRules`` where the label says ``${step.id}-…``.
|
|
21
|
+
|
|
22
|
+
The outcome is the same unnamed textbox by a worse route: a `<label>` pointing at an id
|
|
23
|
+
that is nowhere in the document reads as correct in the label's own file, and is only
|
|
24
|
+
wrong in the *other* file. Half of them are broken and nothing says which half. That is
|
|
25
|
+
the argument for a component rather than a convention — here the id is minted and consumed
|
|
26
|
+
in one place and cannot be spelled twice.
|
|
13
27
|
|
|
14
28
|
<Canvas of={FieldStories.Default} />
|
|
15
29
|
|
|
@@ -25,6 +39,62 @@ Cloned, not wrapped — the same contract as `Popover`'s trigger. An `<input>`,
|
|
|
25
39
|
|
|
26
40
|
<Canvas of={FieldStories.AllVariants} />
|
|
27
41
|
|
|
42
|
+
## A slot is a pass-through
|
|
43
|
+
|
|
44
|
+
Which is a rule this component needed and did not have. `Field` and `Tooltip` both clone
|
|
45
|
+
onto their single child, so the obvious thing to write —
|
|
46
|
+
|
|
47
|
+
```tsx
|
|
48
|
+
<Field label="Rename pattern" isRequired>
|
|
49
|
+
<Tooltip label="A JavaScript regular expression.">
|
|
50
|
+
<input />
|
|
51
|
+
</Tooltip>
|
|
52
|
+
</Field>
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
— handed `id`, `aria-describedby`, `aria-invalid` and `required` to the **`Tooltip`
|
|
56
|
+
component**, which declares none of them. `cloneElement` does not care. React drops all
|
|
57
|
+
four without a warning, TypeScript never sees it (`Children.only` returns a `ReactElement`
|
|
58
|
+
whose props are `any`), every test still passed, and the render was pixel-identical. The
|
|
59
|
+
only symptom was a `<label htmlFor>` pointing at an id nowhere in the document — the exact
|
|
60
|
+
defect above, reproduced by the component built to prevent it, and this time by the
|
|
61
|
+
library rather than by a consumer.
|
|
62
|
+
|
|
63
|
+
**Silent prop-dropping is the worst version of this**, so anything a slot receives from
|
|
64
|
+
above is forwarded to the child it clones onto, and a chain of slots behaves like the one
|
|
65
|
+
control at the bottom of it. `aria-describedby` — the one prop that is a *list* — is
|
|
66
|
+
merged rather than overwritten, outer first.
|
|
67
|
+
|
|
68
|
+
It works in both orders, and the second is not symmetrical: a `Tooltip` around a `Field`
|
|
69
|
+
hands down not one attribute but a working component — floating-ui's hover, focus and
|
|
70
|
+
dismiss handlers and `refs.setReference`. Those reach the control too, or the tip is a
|
|
71
|
+
floating node with no trigger and no anchor.
|
|
72
|
+
|
|
73
|
+
<Canvas of={FieldStories.Nested} />
|
|
74
|
+
|
|
75
|
+
## Several controls are a `FieldGroup`, not a `Field`
|
|
76
|
+
|
|
77
|
+
`Field` mints one id and clones it onto one child. An `id` names one element and a
|
|
78
|
+
`<label htmlFor>` points at one, so the moment there are two controls the label names one
|
|
79
|
+
of them and leaves the rest anonymous. Six of mux-magic's sixteen field components are in
|
|
80
|
+
that position — `RegexWithFlagsField` (pattern, flags, sample, and a checkbox),
|
|
81
|
+
`NumberWithLookupField`, `LanguageCodeField`, `LanguageCodesField`, `SubtitleTypesField`
|
|
82
|
+
and `RenameRegexField`.
|
|
83
|
+
|
|
84
|
+
`FieldGroup` renders `<fieldset>` + `<legend>` and does **not** clone. This is the one
|
|
85
|
+
place in the library where `<fieldset>` is right: `AccordionSection` rejected it for its
|
|
86
|
+
panel precisely because that element means *form-control grouping*, and here the content
|
|
87
|
+
really is one. `<legend>` names the group natively, so no `role="group"` and no
|
|
88
|
+
`aria-labelledby`.
|
|
89
|
+
|
|
90
|
+
What a group cannot carry is `aria-invalid`, which has no group form — it belongs on the
|
|
91
|
+
control that is actually invalid, and a `FieldGroup` does not know which one that is. So
|
|
92
|
+
`error` here is **described, not asserted**: it joins the `<fieldset>`'s
|
|
93
|
+
`aria-describedby`. Cloning `aria-invalid` onto every child would mark the valid ones
|
|
94
|
+
invalid. A control that is itself invalid says so in its own `Field`.
|
|
95
|
+
|
|
96
|
+
<Canvas of={FieldStories.Group} />
|
|
97
|
+
|
|
28
98
|
## `createLinkedIds` is *not* here, and that is the finding
|
|
29
99
|
|
|
30
100
|
The plan expected `useLinkedIds` to do this wiring. It does not fit, for the same reason it
|
|
@@ -5,7 +5,9 @@ import {
|
|
|
5
5
|
StoryGrid,
|
|
6
6
|
} from "../board.storyHelpers.tsx"
|
|
7
7
|
import { Select } from "../Select/Select.tsx"
|
|
8
|
+
import { Tooltip } from "../Tooltip/Tooltip.tsx"
|
|
8
9
|
import { Field } from "./Field.tsx"
|
|
10
|
+
import { FieldGroup } from "./FieldGroup.tsx"
|
|
9
11
|
|
|
10
12
|
const TEXT_INPUT_CLASS =
|
|
11
13
|
"w-full rounded-md border border-border-default bg-surface-raised px-3 py-2 text-content-primary text-sm focus-visible:outline-solid focus-visible:outline-(length:--focus-ring-width) focus-visible:outline-offset-(--focus-ring-offset) focus-visible:outline-focus-ring"
|
|
@@ -142,3 +144,137 @@ export const AllStates: Story = {
|
|
|
142
144
|
</StoryGrid>
|
|
143
145
|
),
|
|
144
146
|
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Two slot components around one control, in both orders.
|
|
150
|
+
*
|
|
151
|
+
* `Field` and `Tooltip` each clone onto their single child, so
|
|
152
|
+
* nesting them used to hand `Field`'s `id`, `aria-describedby`,
|
|
153
|
+
* `aria-invalid` and `required` to the `Tooltip` **component** —
|
|
154
|
+
* which declares none of them and dropped all four, silently, with an
|
|
155
|
+
* identical render and a `<label>` pointing at an id that was nowhere
|
|
156
|
+
* in the document.
|
|
157
|
+
*
|
|
158
|
+
* A slot is a pass-through: whatever arrives from above is forwarded
|
|
159
|
+
* to the child at the bottom of the chain, and `aria-describedby` —
|
|
160
|
+
* the one prop that is a *list* — is merged rather than overwritten,
|
|
161
|
+
* outer first.
|
|
162
|
+
*/
|
|
163
|
+
export const Nested: Story = {
|
|
164
|
+
args: {
|
|
165
|
+
children: (
|
|
166
|
+
<input className={TEXT_INPUT_CLASS} type="text" />
|
|
167
|
+
),
|
|
168
|
+
label: "Output directory",
|
|
169
|
+
},
|
|
170
|
+
render: () => (
|
|
171
|
+
<StoryGrid columns={2}>
|
|
172
|
+
<StoryCell label="tooltip inside field">
|
|
173
|
+
<Field
|
|
174
|
+
description="Applied to every title."
|
|
175
|
+
isRequired
|
|
176
|
+
label="Rename pattern"
|
|
177
|
+
>
|
|
178
|
+
<Tooltip label="A JavaScript regular expression.">
|
|
179
|
+
<input
|
|
180
|
+
className={TEXT_INPUT_CLASS}
|
|
181
|
+
defaultValue="^(.*)$"
|
|
182
|
+
type="text"
|
|
183
|
+
/>
|
|
184
|
+
</Tooltip>
|
|
185
|
+
</Field>
|
|
186
|
+
</StoryCell>
|
|
187
|
+
|
|
188
|
+
<StoryCell label="field inside tooltip">
|
|
189
|
+
<Tooltip label="Where finished rips are moved.">
|
|
190
|
+
<Field error="Not writable." label="Archive path">
|
|
191
|
+
<input
|
|
192
|
+
className={TEXT_INPUT_CLASS}
|
|
193
|
+
defaultValue="/mnt/readonly"
|
|
194
|
+
type="text"
|
|
195
|
+
/>
|
|
196
|
+
</Field>
|
|
197
|
+
</Tooltip>
|
|
198
|
+
</StoryCell>
|
|
199
|
+
</StoryGrid>
|
|
200
|
+
),
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* `FieldGroup` — one label over **several** controls, which is where
|
|
205
|
+
* `Field` cannot go: an `id` names one element and a
|
|
206
|
+
* `<label htmlFor>` points at one.
|
|
207
|
+
*
|
|
208
|
+
* Six of mux-magic's sixteen field components are in this position,
|
|
209
|
+
* and every one of them renders a `FieldLabel` whose `htmlFor` names
|
|
210
|
+
* at best one of the controls under it. The element for the job is a
|
|
211
|
+
* `<fieldset>` with a `<legend>` — the one place in this library
|
|
212
|
+
* where `<fieldset>` is right, because here the content really *is* a
|
|
213
|
+
* form-control grouping.
|
|
214
|
+
*/
|
|
215
|
+
export const Group: Story = {
|
|
216
|
+
args: {
|
|
217
|
+
children: (
|
|
218
|
+
<input className={TEXT_INPUT_CLASS} type="text" />
|
|
219
|
+
),
|
|
220
|
+
label: "Output directory",
|
|
221
|
+
},
|
|
222
|
+
render: () => (
|
|
223
|
+
<StoryGrid columns={2}>
|
|
224
|
+
<StoryCell label="several controls">
|
|
225
|
+
<FieldGroup
|
|
226
|
+
description="Applied to every title in the disc."
|
|
227
|
+
isRequired
|
|
228
|
+
label="Rename pattern"
|
|
229
|
+
>
|
|
230
|
+
<div className="flex gap-2">
|
|
231
|
+
<input
|
|
232
|
+
aria-label="Pattern"
|
|
233
|
+
className={TEXT_INPUT_CLASS}
|
|
234
|
+
defaultValue="^(.*)$"
|
|
235
|
+
type="text"
|
|
236
|
+
/>
|
|
237
|
+
|
|
238
|
+
<input
|
|
239
|
+
aria-label="Flags"
|
|
240
|
+
className={TEXT_INPUT_CLASS}
|
|
241
|
+
defaultValue="gi"
|
|
242
|
+
type="text"
|
|
243
|
+
/>
|
|
244
|
+
</div>
|
|
245
|
+
|
|
246
|
+
<input
|
|
247
|
+
aria-label="Sample"
|
|
248
|
+
className={TEXT_INPUT_CLASS}
|
|
249
|
+
defaultValue="Disc 1 — Title 04"
|
|
250
|
+
type="text"
|
|
251
|
+
/>
|
|
252
|
+
</FieldGroup>
|
|
253
|
+
</StoryCell>
|
|
254
|
+
|
|
255
|
+
<StoryCell label="invalid group">
|
|
256
|
+
<FieldGroup
|
|
257
|
+
error="A pattern needs at least one capture group."
|
|
258
|
+
label="Chapter split"
|
|
259
|
+
>
|
|
260
|
+
<div className="flex gap-2">
|
|
261
|
+
<Select
|
|
262
|
+
label="Unit"
|
|
263
|
+
options={[
|
|
264
|
+
{ label: "Chapters", value: "chapters" },
|
|
265
|
+
{ label: "Minutes", value: "minutes" },
|
|
266
|
+
]}
|
|
267
|
+
/>
|
|
268
|
+
|
|
269
|
+
<input
|
|
270
|
+
aria-label="Every"
|
|
271
|
+
className={TEXT_INPUT_CLASS}
|
|
272
|
+
defaultValue="5"
|
|
273
|
+
type="number"
|
|
274
|
+
/>
|
|
275
|
+
</div>
|
|
276
|
+
</FieldGroup>
|
|
277
|
+
</StoryCell>
|
|
278
|
+
</StoryGrid>
|
|
279
|
+
),
|
|
280
|
+
}
|