@charcuterie/ui 2.14.0 → 2.15.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/Picker/Picker.d.ts +83 -0
- package/dist/Picker/Picker.d.ts.map +1 -0
- package/dist/Picker/Picker.js +85 -0
- package/dist/Picker/Picker.js.map +1 -0
- package/dist/QueryBuilder/QueryBuilderCombinator.d.ts +12 -33
- package/dist/QueryBuilder/QueryBuilderCombinator.d.ts.map +1 -1
- package/dist/QueryBuilder/QueryBuilderCombinator.js +32 -50
- package/dist/QueryBuilder/QueryBuilderCombinator.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/Picker/Picker.mdx +63 -0
- package/src/Picker/Picker.stories.tsx +184 -0
- package/src/Picker/Picker.test.tsx +98 -0
- package/src/Picker/Picker.tsx +186 -0
- package/src/QueryBuilder/QueryBuilder.stories.tsx +14 -63
- package/src/QueryBuilder/QueryBuilderCombinator.tsx +40 -79
- package/src/index.ts +5 -0
- package/src/sourceRules.test.ts +23 -12
|
@@ -1,8 +1,6 @@
|
|
|
1
|
-
import { useVisibility } from "@charcuterie/logic"
|
|
2
1
|
import type { ReactNode } from "react"
|
|
3
2
|
|
|
4
|
-
import {
|
|
5
|
-
import { Listbox } from "../Listbox/Listbox.tsx"
|
|
3
|
+
import { Picker } from "../Picker/Picker.tsx"
|
|
6
4
|
|
|
7
5
|
export type QueryBuilderCombinatorProps<Combinator> = {
|
|
8
6
|
/** The visible caption above the control, e.g. "Match". */
|
|
@@ -18,91 +16,54 @@ export type QueryBuilderCombinatorProps<Combinator> = {
|
|
|
18
16
|
/**
|
|
19
17
|
* One group's combinator picker.
|
|
20
18
|
*
|
|
21
|
-
* A `
|
|
19
|
+
* A `Picker` — a `Listbox` with its trigger attached — rather than a
|
|
20
|
+
* native `Select`, per
|
|
22
21
|
* [the 2026-08-10 demotion](../../../../docs/decisions/2026-08-10-listbox-and-combobox-are-the-default-and-select-is-demoted.md).
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
* catch it because the component-choice block is scoped to app
|
|
26
|
-
* packages and exempts this one — right for a primitive, wrong for a
|
|
27
|
-
* composite an app consumes whole.
|
|
22
|
+
* This file used to assemble the trigger by hand; `Picker` exists
|
|
23
|
+
* because it was the fourth place in the fleet doing so.
|
|
28
24
|
*
|
|
29
|
-
* ###
|
|
25
|
+
* ### What is left here, and why it is still a component
|
|
30
26
|
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
* `Field` wires `<label htmlFor>` by cloning its `id` onto the
|
|
39
|
-
* control. `Listbox`'s trigger cannot take one: `useAnchoredOverlay`
|
|
40
|
-
* overwrites the cloned trigger's `id` with the one the panel points
|
|
41
|
-
* `aria-labelledby` at, so the label would address an element that no
|
|
42
|
-
* longer carries that id — the exact defect `Field` exists to
|
|
43
|
-
* prevent. So the caption is plain text and the trigger is named with
|
|
44
|
-
* `aria-label`.
|
|
45
|
-
*
|
|
46
|
-
* That name is `"<label>: <current value>"` rather than bare
|
|
47
|
-
* `"<label>"` on purpose: the button's visible text is the current
|
|
48
|
-
* combinator, and WCAG 2.5.3 wants the visible text contained in the
|
|
49
|
-
* accessible name. "Match" alone would fail it; "Match: ALL — every
|
|
50
|
-
* condition" reads correctly and keeps each group's control findable
|
|
51
|
-
* by name.
|
|
52
|
-
*
|
|
53
|
-
* The combinator stays opaque here exactly as it is in the rest of
|
|
54
|
-
* the component: `String(value)` bridges it to the string-valued
|
|
55
|
-
* `Listbox`, and a selection is mapped back to the real `Combinator`
|
|
56
|
-
* before it leaves.
|
|
27
|
+
* Only the bridging. `Combinator` is opaque to `QueryBuilder`, so it
|
|
28
|
+
* cannot reach `Picker`'s string-valued API directly: `String(value)`
|
|
29
|
+
* carries it out and the matching option carries it back, so the
|
|
30
|
+
* generic never leaks. The caption is plain text rather than a
|
|
31
|
+
* `Field`, because `useAnchoredOverlay` overwrites the trigger's `id`
|
|
32
|
+
* and a `<label htmlFor>` would then address an element that no longer
|
|
33
|
+
* carries it — the exact defect `Field` exists to prevent.
|
|
57
34
|
*/
|
|
58
35
|
export const QueryBuilderCombinator = <Combinator,>({
|
|
59
36
|
label,
|
|
60
37
|
onChange,
|
|
61
38
|
options,
|
|
62
39
|
value,
|
|
63
|
-
}: QueryBuilderCombinatorProps<Combinator>): ReactNode =>
|
|
64
|
-
|
|
40
|
+
}: QueryBuilderCombinatorProps<Combinator>): ReactNode => (
|
|
41
|
+
<div className="flex flex-col gap-1.5">
|
|
42
|
+
<span className="font-medium text-content-primary text-sm">
|
|
43
|
+
{label}
|
|
44
|
+
</span>
|
|
65
45
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
46
|
+
<Picker
|
|
47
|
+
// No chevron: this is a refactor onto the shared component, and
|
|
48
|
+
// the trigger that shipped in 2.14.0 had none. Adding the
|
|
49
|
+
// affordance here is a visual change and belongs in its own PR,
|
|
50
|
+
// not smuggled in under an extraction.
|
|
51
|
+
iconEnd={null}
|
|
52
|
+
label={label}
|
|
53
|
+
onChange={(nextValue) => {
|
|
54
|
+
const match = options.find(
|
|
55
|
+
(option) => String(option.value) === nextValue,
|
|
56
|
+
)
|
|
69
57
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
return (
|
|
73
|
-
<div className="flex flex-col gap-1.5">
|
|
74
|
-
<span className="font-medium text-content-primary text-sm">
|
|
75
|
-
{label}
|
|
76
|
-
</span>
|
|
77
|
-
|
|
78
|
-
<Listbox
|
|
79
|
-
isVisible={isVisible}
|
|
80
|
-
onDismiss={hide}
|
|
81
|
-
onSelect={(nextValue) => {
|
|
82
|
-
const match = options.find(
|
|
83
|
-
(option) => String(option.value) === nextValue,
|
|
84
|
-
)
|
|
85
|
-
|
|
86
|
-
if (match) {
|
|
87
|
-
onChange(match.value)
|
|
88
|
-
}
|
|
89
|
-
}}
|
|
90
|
-
options={options.map((option) => ({
|
|
91
|
-
label: option.label,
|
|
92
|
-
value: String(option.value),
|
|
93
|
-
}))}
|
|
94
|
-
selectedValue={String(value)}
|
|
95
|
-
trigger={
|
|
96
|
-
<Button
|
|
97
|
-
appearance="outline"
|
|
98
|
-
aria-label={`${label}: ${currentLabel}`}
|
|
99
|
-
intent="neutral"
|
|
100
|
-
onClick={toggle}
|
|
101
|
-
>
|
|
102
|
-
{currentLabel}
|
|
103
|
-
</Button>
|
|
58
|
+
if (match) {
|
|
59
|
+
onChange(match.value)
|
|
104
60
|
}
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
61
|
+
}}
|
|
62
|
+
options={options.map((option) => ({
|
|
63
|
+
label: option.label,
|
|
64
|
+
value: String(option.value),
|
|
65
|
+
}))}
|
|
66
|
+
value={String(value)}
|
|
67
|
+
/>
|
|
68
|
+
</div>
|
|
69
|
+
)
|
package/src/index.ts
CHANGED
|
@@ -160,6 +160,11 @@ export type {
|
|
|
160
160
|
export { Menu } from "./Menu/Menu.tsx"
|
|
161
161
|
export type { ModalProps } from "./Modal/Modal.tsx"
|
|
162
162
|
export { Modal } from "./Modal/Modal.tsx"
|
|
163
|
+
export type {
|
|
164
|
+
PickerOption,
|
|
165
|
+
PickerProps,
|
|
166
|
+
} from "./Picker/Picker.tsx"
|
|
167
|
+
export { Picker } from "./Picker/Picker.tsx"
|
|
163
168
|
export type { PopoverProps } from "./Popover/Popover.tsx"
|
|
164
169
|
export { Popover } from "./Popover/Popover.tsx"
|
|
165
170
|
export type {
|
package/src/sourceRules.test.ts
CHANGED
|
@@ -373,7 +373,13 @@ test("the barrel is the only place components are re-exported", async () => {
|
|
|
373
373
|
// both are rendered inside a `.map` — and stay out of this count and
|
|
374
374
|
// the barrel by the `<Name>/<Name>.tsx` rule, exactly as
|
|
375
375
|
// `SegmentedOption` and `ListboxOption` do.
|
|
376
|
-
|
|
376
|
+
//
|
|
377
|
+
// `Picker` — a `Listbox` with its trigger already attached — is +1 ->
|
|
378
|
+
// 45. It is a component rather than a recipe in the docs because the
|
|
379
|
+
// fleet wrote it four separate times (plex-channels' `SelectListbox`,
|
|
380
|
+
// board-games' `SelectMenu`, mux-magic's `ListboxPicker`, and twice
|
|
381
|
+
// inside this package), each with its own hand-rolled chevron.
|
|
382
|
+
expect(componentNames.length).toBe(45)
|
|
377
383
|
|
|
378
384
|
for (const name of componentNames) {
|
|
379
385
|
expect(barrel).toContain(`export { ${name} }`)
|
|
@@ -457,17 +463,22 @@ const ENTRY_POINT_RUNTIMES: Record<
|
|
|
457
463
|
"./core": [],
|
|
458
464
|
"./jotai": ["jotai"],
|
|
459
465
|
"./preact": ["preact", "preact/hooks"],
|
|
460
|
-
// The
|
|
461
|
-
//
|
|
462
|
-
//
|
|
463
|
-
//
|
|
464
|
-
//
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
466
|
+
// The typed OpenAPI seam, split out of `./query` so that only an
|
|
467
|
+
// app whose backend publishes a spec resolves these two. This
|
|
468
|
+
// table is the thing that makes that split legible: what the row
|
|
469
|
+
// costs is the whole argument for it being its own entry point.
|
|
470
|
+
// See the [split
|
|
471
|
+
// decision](../../../docs/decisions/2026-08-13-the-openapi-seam-is-its-own-subpath-not-part-of-query.md).
|
|
472
|
+
"./openapi": ["openapi-fetch", "openapi-react-query"],
|
|
473
|
+
// The request/response data layer: react-query and `react` (the
|
|
474
|
+
// provider's `useState`), both optional peers, so an app that
|
|
475
|
+
// never imports `./query` resolves neither. It deliberately does
|
|
476
|
+
// NOT reach the openapi-* packages — when it did (via the
|
|
477
|
+
// re-export this row used to list), the type-only edge above was
|
|
478
|
+
// enough to force every non-OpenAPI consumer to install both.
|
|
479
|
+
// `react/jsx-runtime` is added only at emit, so it is not a
|
|
480
|
+
// source specifier here.
|
|
481
|
+
"./query": ["@tanstack/react-query", "react"],
|
|
471
482
|
"./signals": ["@preact/signals-core"],
|
|
472
483
|
},
|
|
473
484
|
tokens: {
|