efficient-easel 0.0.0 → 0.97.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/LICENSE +10 -0
- package/README.md +818 -4
- package/dist/autosave/index.d.ts +101 -0
- package/dist/autosave/index.js +1 -0
- package/dist/blocks/index.d.ts +306 -0
- package/dist/blocks/index.js +0 -0
- package/dist/buckets-1vRlQceY.d.ts +13 -0
- package/dist/canvas/index.d.ts +194 -0
- package/dist/canvas/index.js +1 -0
- package/dist/chrome/chrome.css +570 -0
- package/dist/chrome/index.d.ts +219 -0
- package/dist/chrome/index.js +21 -0
- package/dist/chunk-2LUSK32B.js +1 -0
- package/dist/chunk-2OAZYSWJ.js +1 -0
- package/dist/chunk-67VZRQLG.js +13 -0
- package/dist/chunk-6OPSJGAI.js +1 -0
- package/dist/chunk-6PS6STGA.js +1 -0
- package/dist/chunk-6WY6ZXJG.js +1 -0
- package/dist/chunk-7A7O2JWQ.js +1 -0
- package/dist/chunk-BZ5ASXOL.js +1 -0
- package/dist/chunk-CERTFXCV.js +13 -0
- package/dist/chunk-D2BCHTQR.js +1 -0
- package/dist/chunk-FI23ALVJ.js +1 -0
- package/dist/chunk-GGTPNGFJ.js +1 -0
- package/dist/chunk-H4B72XCJ.js +1 -0
- package/dist/chunk-H6DDIBQ4.js +1 -0
- package/dist/chunk-KDIVA6WD.js +1 -0
- package/dist/chunk-KRXQM65X.js +1 -0
- package/dist/chunk-MCLBHENY.js +1 -0
- package/dist/chunk-MFIRYGHW.js +1 -0
- package/dist/chunk-NKB3KVAB.js +1 -0
- package/dist/chunk-O2NLQBFX.js +1 -0
- package/dist/chunk-OGZFDJ6G.js +1 -0
- package/dist/chunk-OWBO4WKA.js +0 -0
- package/dist/chunk-QB3BZIRA.js +5 -0
- package/dist/chunk-RE4LP46F.js +1 -0
- package/dist/chunk-STFMZJKU.js +14 -0
- package/dist/chunk-SUO5627P.js +1 -0
- package/dist/chunk-TDA4HH54.js +1 -0
- package/dist/chunk-WK3AFPXY.js +1 -0
- package/dist/chunk-YIMRTIBN.js +3 -0
- package/dist/chunk-YNZZYHA4.js +450 -0
- package/dist/chunk-YRLPFZW3.js +1 -0
- package/dist/chunk-ZAOM6PFH.js +1 -0
- package/dist/chunk-ZNCEOSXO.js +1 -0
- package/dist/copy/copy.css +282 -0
- package/dist/copy/index.d.ts +336 -0
- package/dist/copy/index.js +10 -0
- package/dist/data/index.d.ts +375 -0
- package/dist/data/index.js +1 -0
- package/dist/drag/drag.css +360 -0
- package/dist/drag/index.d.ts +379 -0
- package/dist/drag/index.js +18 -0
- package/dist/edit-lock/index.d.ts +354 -0
- package/dist/edit-lock/index.js +1 -0
- package/dist/editor/editor.css +58 -0
- package/dist/editor/index.d.ts +361 -0
- package/dist/editor/index.js +9 -0
- package/dist/fields/fields.css +207 -0
- package/dist/fields/index.d.ts +273 -0
- package/dist/fields/index.js +46 -0
- package/dist/gold/gold.css +196 -0
- package/dist/gold/index.d.ts +280 -0
- package/dist/gold/index.js +1 -0
- package/dist/icons/index.d.ts +474 -0
- package/dist/icons/index.js +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +1 -0
- package/dist/markup/index.d.ts +452 -0
- package/dist/markup/index.js +1 -0
- package/dist/menu/index.d.ts +49 -0
- package/dist/menu/index.js +15 -0
- package/dist/menu/menu.css +254 -0
- package/dist/node-ops/index.d.ts +91 -0
- package/dist/node-ops/index.js +1 -0
- package/dist/panel/index.d.ts +1088 -0
- package/dist/panel/index.js +16 -0
- package/dist/panel/panel.css +1460 -0
- package/dist/pick/index.d.ts +153 -0
- package/dist/pick/index.js +14 -0
- package/dist/pick/pick.css +235 -0
- package/dist/plan-DChbUXdS.d.ts +88 -0
- package/dist/rail/index.d.ts +145 -0
- package/dist/rail/index.js +1 -0
- package/dist/rail/rail.css +175 -0
- package/dist/render/index.d.ts +117 -0
- package/dist/render/index.js +5 -0
- package/dist/resolve-D68pb3Bx.d.ts +57 -0
- package/dist/shell/index.d.ts +159 -0
- package/dist/shell/index.js +1 -0
- package/dist/shell/shell.css +122 -0
- package/dist/strings/catalogue.d.ts +141 -0
- package/dist/strings/catalogue.js +1 -0
- package/dist/strings/index.d.ts +56 -0
- package/dist/strings/index.js +2 -0
- package/dist/style-clipboard/index.d.ts +184 -0
- package/dist/style-clipboard/index.js +1 -0
- package/dist/tree/index.d.ts +173 -0
- package/dist/tree/index.js +1 -0
- package/dist/tree/tree.css +618 -0
- package/dist/types-41704JE4.d.ts +112 -0
- package/dist/types-CEqUFH2j.d.ts +129 -0
- package/dist/types-CuNiSnX6.d.ts +204 -0
- package/dist/types-DNrn_w4V.d.ts +88 -0
- package/dist/types-Dy6oR6Z2.d.ts +153 -0
- package/dist/types-pGBXevBU.d.ts +131 -0
- package/dist/useNodeOps-BF0fQsif.d.ts +68 -0
- package/dist/watch-DmasQA61.d.ts +60 -0
- package/dist/wordFit-9B9nqyf3.d.ts +35 -0
- package/dist/words-B2YLaKEr.d.ts +30 -0
- package/dist/words-B87GIFWH.d.ts +86 -0
- package/dist/words-BK-npta-.d.ts +27 -0
- package/dist/words-CD3WADyb.d.ts +51 -0
- package/dist/words-CsRWXkY4.d.ts +129 -0
- package/dist/words-D9ChvbNC.d.ts +52 -0
- package/dist/words-DOhF-4D4.d.ts +35 -0
- package/dist/words-DagJdwIA.d.ts +78 -0
- package/dist/words-DfxEp_RH.d.ts +142 -0
- package/package.json +202 -3
|
@@ -0,0 +1,1088 @@
|
|
|
1
|
+
import * as react from 'react';
|
|
2
|
+
import { ReactNode } from 'react';
|
|
3
|
+
import { W as WordFit } from '../wordFit-9B9nqyf3.js';
|
|
4
|
+
export { a as SETTINGS_BUCKETS, S as SettingsBucket } from '../buckets-1vRlQceY.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The settings-panel primitives — the shared grammar BOTH editors' inspector
|
|
8
|
+
* panels are built from (owner-approved design, 2026-08-20).
|
|
9
|
+
*
|
|
10
|
+
* Every one is CONTROLLED and hook-free: the host owns which tab is active and
|
|
11
|
+
* which groups are open, exactly as the rail's host owns selection. That is what
|
|
12
|
+
* keeps this a directive-free leaf a Next-16 server component can import.
|
|
13
|
+
*/
|
|
14
|
+
/** One tab in the strip. `badge` shows a count (e.g. unsaved overrides). */
|
|
15
|
+
interface SettingsTabItem {
|
|
16
|
+
/** What this tab IS, as `activeId` and `onSelect` name it. Not a DOM id. */
|
|
17
|
+
id: string;
|
|
18
|
+
/**
|
|
19
|
+
* What the tab is CALLED — and, since card 8810, its name in every tier.
|
|
20
|
+
*
|
|
21
|
+
* Required, and a plain string, on purpose. Give the tab an `icon` and a
|
|
22
|
+
* narrow panel takes this word off the screen, but never out of the tab: it
|
|
23
|
+
* stays in the markup and it is carried as `title` and `aria-label` on the
|
|
24
|
+
* button at every width. An icon-only control with no name is the exact
|
|
25
|
+
* defect card 8801 shipped and had to have caught in review, and a separate
|
|
26
|
+
* optional `ariaLabel` prop is how that happens again. Here it cannot — the
|
|
27
|
+
* name is the field the tab has always had, and it is not optional.
|
|
28
|
+
*/
|
|
29
|
+
label: string;
|
|
30
|
+
/**
|
|
31
|
+
* An 18px glyph from `efficient-easel/icons`, so the tab can spend less width
|
|
32
|
+
* than its word needs when the panel is dragged narrow (card 8810).
|
|
33
|
+
*
|
|
34
|
+
* Optional, and the strip is unchanged without it: a tab with no glyph has
|
|
35
|
+
* nothing to fall back to, so it keeps its word at every width and the strip
|
|
36
|
+
* wraps for it exactly as it did before. Pass icons for ALL the tabs in a
|
|
37
|
+
* strip or none of them — a row that is half words and half glyphs below
|
|
38
|
+
* 270px reads as a rendering fault, not as a decision.
|
|
39
|
+
*/
|
|
40
|
+
icon?: ReactNode;
|
|
41
|
+
badge?: number;
|
|
42
|
+
/**
|
|
43
|
+
* The DOM id of the element that shows this tab's settings — `aria-controls`.
|
|
44
|
+
*
|
|
45
|
+
* Without it a tab is announced as "tab, 1 of 4, selected" and there is no
|
|
46
|
+
* panel it is bound to, which is what `role="tab"` promises there will be.
|
|
47
|
+
* The shell cannot supply the id itself: only the host knows what element the
|
|
48
|
+
* settings land in, and in Puck's flat sibling list that element may be one
|
|
49
|
+
* the shell never rendered. Optional because a host that has no such element
|
|
50
|
+
* to point at should render NO attribute rather than one pointing nowhere.
|
|
51
|
+
*/
|
|
52
|
+
panelId?: string;
|
|
53
|
+
/**
|
|
54
|
+
* A DOM id for the tab BUTTON, so the panel above can name itself with
|
|
55
|
+
* `aria-labelledby` and the pair is wired in both directions.
|
|
56
|
+
*/
|
|
57
|
+
htmlId?: string;
|
|
58
|
+
}
|
|
59
|
+
interface SettingsTabsProps {
|
|
60
|
+
items: SettingsTabItem[];
|
|
61
|
+
/** The `id` of the tab currently shown. */
|
|
62
|
+
activeId: string;
|
|
63
|
+
onSelect: (id: string) => void;
|
|
64
|
+
ariaLabel?: string;
|
|
65
|
+
className?: string;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* WHERE THIS STRIP'S WORDS STOP FITTING — the one thing the shell cannot know
|
|
69
|
+
* about a segmented control, measured by the consumer that owns the words.
|
|
70
|
+
*
|
|
71
|
+
* The same shape the top-bar rail hands the same rule (`WordFit`, in
|
|
72
|
+
* `src/fit/wordFit.ts`, where both numbers are described).
|
|
73
|
+
*
|
|
74
|
+
* MEASURE THEM, do not count letters. The chip's font resolves to SF on a Mac
|
|
75
|
+
* and Segoe UI on Windows, so one table of letter widths would be wrong on most
|
|
76
|
+
* machines; measured against real labels a letter-count guess is out by up to
|
|
77
|
+
* 7%, which is enough to drop a word that fits. Render the strip and read the
|
|
78
|
+
* label spans — `tests/fixtures/segmented-fit.html` does exactly that and
|
|
79
|
+
* prints both numbers. If a label is about to change and the number cannot be
|
|
80
|
+
* re-measured today, round it up.
|
|
81
|
+
*/
|
|
82
|
+
type SettingsSegmentedFit = WordFit;
|
|
83
|
+
/**
|
|
84
|
+
* WHAT AN OPTION IS CALLED, and the one rule that governs it: EVERY option has a
|
|
85
|
+
* name, and the name is a string (card 66303).
|
|
86
|
+
*
|
|
87
|
+
* `label` is spent three ways — the visible mark, `title`, and `aria-label` — so
|
|
88
|
+
* it cannot simply be widened to a `ReactNode` and left there: a node reaches
|
|
89
|
+
* the two attributes as `[object Object]`, which is a tooltip that says nothing
|
|
90
|
+
* and a screen reader that says worse. Nor can it be split into `icon` + `label`
|
|
91
|
+
* and the word left to fall away at a threshold: both spans render, so a strip
|
|
92
|
+
* whose mark IS its label would draw the mark AND the word beside it.
|
|
93
|
+
*
|
|
94
|
+
* So the two cases are told apart in the type, and each carries its own name:
|
|
95
|
+
*
|
|
96
|
+
* a WORDED option `{ label: 'Left' }` — the word is the name. `title` is
|
|
97
|
+
* optional and almost never given; this is every option that
|
|
98
|
+
* exists today and none of them changes.
|
|
99
|
+
* a DRAWN option `{ label: <AlignGlyph/>, title: 'Left' }` — the mark cannot
|
|
100
|
+
* be read aloud, so `title` is REQUIRED and is the name.
|
|
101
|
+
*
|
|
102
|
+
* The part panel's Alignment row is the case that forced it: three alignments
|
|
103
|
+
* have no honest three-letter word between them, so the label is the rule
|
|
104
|
+
* itself. Case is the same shape one step milder — Aa / AA / aa are letterforms,
|
|
105
|
+
* not the words Normal / Uppercase / Lowercase that name them. Passing `title`
|
|
106
|
+
* beside a WORDED label is allowed and overrides the word, which is what a
|
|
107
|
+
* strip whose words are abbreviations wants.
|
|
108
|
+
*
|
|
109
|
+
* ── NO CONSUMER TODAY, AND CARRIED ON PURPOSE ────────────────────────────────
|
|
110
|
+
*
|
|
111
|
+
* Written down so the next reader does not find this unused and tidy it away.
|
|
112
|
+
* The rows above stopped needing it while this was being built: card 66246
|
|
113
|
+
* (`efficient-shop` 151afa212, merged 2026-08-31) moved the part panel's three
|
|
114
|
+
* tables onto `icon` + a real word — `{ id: "left", label: "Left", icon:
|
|
115
|
+
* <AlignLeftIcon/> }` — on the owner's ruling that an option says its name AND
|
|
116
|
+
* draws its glyph. Every `label` in the shop is now a string, and this arm has
|
|
117
|
+
* no caller anywhere. That is measured on merged main, not on a pending PR.
|
|
118
|
+
*
|
|
119
|
+
* It stays because it is the shape THE PACKAGE NEXT DOOR ALREADY USES.
|
|
120
|
+
* `efficient-shell` ships its own `Segmented`, and its `SegmentedOption` has
|
|
121
|
+
* carried `label: ReactNode` all along. Identical at the two copies installed on
|
|
122
|
+
* this desk — 4.63.0 under `editor-shell`, 4.67.0 under `efficient-shop`:
|
|
123
|
+
*
|
|
124
|
+
* interface SegmentedOption<T extends string = string> {
|
|
125
|
+
* value: T; label: ReactNode; icon?: ReactNode; disabled?: boolean;
|
|
126
|
+
* }
|
|
127
|
+
*
|
|
128
|
+
* A shared control that refuses a shape its sibling accepts is a fork one level
|
|
129
|
+
* down, which is the fault this whole family of cards exists to stop. Matching
|
|
130
|
+
* it costs nothing while nothing calls it.
|
|
131
|
+
*
|
|
132
|
+
* ── WHERE THE TWO SHELLS CONVERGE, AND WHERE THEY DO NOT ─────────────────────
|
|
133
|
+
*
|
|
134
|
+
* Read this before repeating "the two shells are converged", because it is only
|
|
135
|
+
* true of three fields out of four, and the fourth is the one a caller touches
|
|
136
|
+
* most:
|
|
137
|
+
*
|
|
138
|
+
* efficient-shell editor-shell
|
|
139
|
+
* the label label: ReactNode label: ReactNode converged
|
|
140
|
+
* the glyph icon?: ReactNode icon?: ReactNode converged
|
|
141
|
+
* out of force disabled?: boolean disabled?: boolean converged
|
|
142
|
+
* ─────────────────────────────────────────────────────────────────────────
|
|
143
|
+
* the VALUE value: T id: V NOT converged
|
|
144
|
+
* T extends string V unconstrained
|
|
145
|
+
*
|
|
146
|
+
* BOTH halves of that last row are deliberate.
|
|
147
|
+
*
|
|
148
|
+
* `id` and not `value`, because this field has been called `id` since the type
|
|
149
|
+
* existed and renaming it would move something for every caller already using
|
|
150
|
+
* it — which is the one thing card 66303 was not allowed to do.
|
|
151
|
+
*
|
|
152
|
+
* `V` UNCONSTRAINED and not `T extends string`, because the shop stores a
|
|
153
|
+
* choice as a boolean: `reverse` at `efficient-shop/lib/puck/config.tsx:6747` is
|
|
154
|
+
* `false` = image left and `true` = image right on every published page across
|
|
155
|
+
* every tenant. `T extends string` cannot hold it, and giving that row the house
|
|
156
|
+
* strip by storing "left"/"right" would be a migration of live customer data
|
|
157
|
+
* rather than a control change. So on the value type this package is the WIDER
|
|
158
|
+
* of the two, not the poorer — the opposite of the direction the rest of this
|
|
159
|
+
* note runs.
|
|
160
|
+
*
|
|
161
|
+
* ── AND WHERE WE DELIBERATELY GO FURTHER ─────────────────────────────────────
|
|
162
|
+
*
|
|
163
|
+
* That sibling declaration has NO name field beside its node label. A drawing
|
|
164
|
+
* that cannot name itself is exactly the defect the 2026-08-06 audit closed on
|
|
165
|
+
* the shop's strip, so the `title` here is REQUIRED — a guarantee, not a
|
|
166
|
+
* feature. The union makes a nameless drawing unconstructable rather than a
|
|
167
|
+
* documented risk; the sibling's shape permits one.
|
|
168
|
+
*
|
|
169
|
+
* One more thing that declaration settles, recorded because it was argued: its
|
|
170
|
+
* `disabled` is PER OPTION and it has no strip-level one at all. That is the
|
|
171
|
+
* independent support for the per-option field on
|
|
172
|
+
* `SettingsSegmentedOptionBase` — the sibling has shipped exactly that.
|
|
173
|
+
*/
|
|
174
|
+
type SettingsSegmentedLabel = {
|
|
175
|
+
label: string;
|
|
176
|
+
title?: string;
|
|
177
|
+
} | {
|
|
178
|
+
label: ReactNode;
|
|
179
|
+
title: string;
|
|
180
|
+
};
|
|
181
|
+
interface SettingsSegmentedOptionBase<V = string> {
|
|
182
|
+
/**
|
|
183
|
+
* The VALUE this option writes, as `value` and `onChange` name it.
|
|
184
|
+
*
|
|
185
|
+
* `V` and not `string` since card 66303. A weight is a NUMBER — the storefront
|
|
186
|
+
* part panel's row is `[400, 500, 600, 700]` — and "Image side" is a BOOLEAN
|
|
187
|
+
* on every published page across every tenant, so giving those rows the house
|
|
188
|
+
* strip by storing "left"/"right" would be a migration of live customer data
|
|
189
|
+
* rather than a control change. `V` defaults to `string`, so every existing
|
|
190
|
+
* declaration means exactly what it did.
|
|
191
|
+
*/
|
|
192
|
+
id: V;
|
|
193
|
+
/**
|
|
194
|
+
* A 16px glyph from `efficient-easel/icons`, so the option can spend less width
|
|
195
|
+
* than its word needs when the panel is dragged narrow.
|
|
196
|
+
*
|
|
197
|
+
* Optional, and the strip is unchanged without it: an option with no glyph
|
|
198
|
+
* has nothing to fall back to, so it keeps its word at every width. Pass
|
|
199
|
+
* glyphs for ALL the options or none of them — half words and half glyphs
|
|
200
|
+
* reads as a rendering fault, not as a decision.
|
|
201
|
+
*
|
|
202
|
+
* NOT the same thing as a DRAWN label. An `icon` is a glyph BESIDE the word,
|
|
203
|
+
* and the word is what it falls back from; a drawn label replaces the word
|
|
204
|
+
* altogether and is named by its `title`. An option wanting the second one
|
|
205
|
+
* puts the mark in `label`, not here.
|
|
206
|
+
*/
|
|
207
|
+
icon?: ReactNode;
|
|
208
|
+
/**
|
|
209
|
+
* This choice exists but cannot be taken right now.
|
|
210
|
+
*
|
|
211
|
+
* Renders the button's own `disabled`, not a fade over it, and that is the
|
|
212
|
+
* whole point of it living here: this component owns the `<button>`, so a host
|
|
213
|
+
* dimming a parent instead leaves every segment focusable and announcing as
|
|
214
|
+
* ENABLED — the exact defect the 2026-08-06 audit and card 66034 closed.
|
|
215
|
+
*
|
|
216
|
+
* Dimmed, NEVER hidden, and the pressed segment still reads as pressed: the
|
|
217
|
+
* merchant's saved answer is still their answer, it simply is not in force.
|
|
218
|
+
* Defaults absent, so nothing an existing caller renders moves.
|
|
219
|
+
*
|
|
220
|
+
* NO CONSUMER TODAY, and it is carried on purpose rather than by oversight —
|
|
221
|
+
* so the next reader does not find it unused and tidy it away. The case it is
|
|
222
|
+
* for is one row away: card 66246 draws Weight as four letterform glyphs, and
|
|
223
|
+
* a merchant's typeface may only carry two real weights (measured there —
|
|
224
|
+
* Helvetica, Georgia, Verdana and Impact render Regular and Medium as the same
|
|
225
|
+
* letter, and Semibold and Bold as the same letter). That is a choice the
|
|
226
|
+
* strip CAN offer and the font cannot honour, which is this field and not the
|
|
227
|
+
* strip-level one beside it.
|
|
228
|
+
*/
|
|
229
|
+
disabled?: boolean;
|
|
230
|
+
}
|
|
231
|
+
/** One option in a segmented control. */
|
|
232
|
+
type SettingsSegmentedOption<V = string> = SettingsSegmentedOptionBase<V> & SettingsSegmentedLabel;
|
|
233
|
+
/**
|
|
234
|
+
* A SEGMENTED CONTROL — the tab strip's chip, with the other promise.
|
|
235
|
+
*
|
|
236
|
+
* `SettingsTabs` is a TABLIST: its chips switch a panel, which is what
|
|
237
|
+
* `role="tab"` promises. These chips switch nothing. They write a value and the
|
|
238
|
+
* panel stays where it is, so they are pressed buttons in a group, and the
|
|
239
|
+
* marker is `aria-pressed` — the same one `efficient-shop`'s own `Segmented`
|
|
240
|
+
* has carried since the 2026-08-06 audit.
|
|
241
|
+
*
|
|
242
|
+
* NOT `radiogroup` / `aria-checked`, and the reason is the keyboard rather than
|
|
243
|
+
* the taxonomy. `role="radio"` promises roving focus: one Tab stop for the set,
|
|
244
|
+
* arrows moving between the options and selecting as they go. This is a
|
|
245
|
+
* hook-free controlled leaf — it owns no focus and runs no key handler, and a
|
|
246
|
+
* host cannot add one to a `<button>` it did not render. Radio roles over
|
|
247
|
+
* plain tab-stopped buttons would promise a keyboard that is not there, which
|
|
248
|
+
* is the same defect as `role="tab"` over no panel.
|
|
249
|
+
*/
|
|
250
|
+
interface SettingsSegmentedProps<V = string> {
|
|
251
|
+
/**
|
|
252
|
+
* `readonly` so a panel may declare its option table `as const` and keep the
|
|
253
|
+
* literal value types — which is how every one of them is already written.
|
|
254
|
+
* A widening only: a mutable array still binds.
|
|
255
|
+
*/
|
|
256
|
+
options: readonly SettingsSegmentedOption<V>[];
|
|
257
|
+
/**
|
|
258
|
+
* The `id` of the option currently set.
|
|
259
|
+
*
|
|
260
|
+
* A value matching NO option presses nothing, and that is a state a caller
|
|
261
|
+
* relies on rather than an edge: the part panel hands `""` when the selection
|
|
262
|
+
* is mixed across several parts. The match is an EQUALITY and never a
|
|
263
|
+
* truthiness test — `false` is a real answer ("image left"), and `value ||
|
|
264
|
+
* fallback` collapses it into the other one.
|
|
265
|
+
*/
|
|
266
|
+
value: V;
|
|
267
|
+
onChange: (next: V) => void;
|
|
268
|
+
/**
|
|
269
|
+
* What the group is called — "Weight", "Alignment". Required: a group of
|
|
270
|
+
* pressed buttons with no name is a set a screen reader cannot introduce.
|
|
271
|
+
*/
|
|
272
|
+
ariaLabel: string;
|
|
273
|
+
/**
|
|
274
|
+
* Where this strip's words stop fitting. Optional, and WITHOUT IT NOTHING
|
|
275
|
+
* DROPS: an unmeasured strip keeps every word at every width, because the
|
|
276
|
+
* only alternative is guessing where to hide one. Give the options glyphs and
|
|
277
|
+
* this too, or the glyphs have nothing to fall back at.
|
|
278
|
+
*/
|
|
279
|
+
fit?: SettingsSegmentedFit;
|
|
280
|
+
/**
|
|
281
|
+
* The whole strip exists but cannot apply right now, because another setting
|
|
282
|
+
* has taken the decision away — the Header's "Text colour at the top" under a
|
|
283
|
+
* pinned Background (card 66216).
|
|
284
|
+
*
|
|
285
|
+
* Every segment goes out of force AND the track says so, so the control goes
|
|
286
|
+
* quiet as one thing rather than as three identically faded chips. Per-option
|
|
287
|
+
* `disabled` is the other half of this: use that for a choice the strip cannot
|
|
288
|
+
* offer, and this for a strip the panel cannot offer.
|
|
289
|
+
*
|
|
290
|
+
* Dimmed, never hidden — a control that disappears reads as a feature that was
|
|
291
|
+
* removed, and leaves the merchant no way to see what to change to get it
|
|
292
|
+
* back. Defaults false, so every existing caller renders byte for byte as it
|
|
293
|
+
* did.
|
|
294
|
+
*/
|
|
295
|
+
disabled?: boolean;
|
|
296
|
+
className?: string;
|
|
297
|
+
}
|
|
298
|
+
interface SettingsGroupProps {
|
|
299
|
+
/** The group's name — "Items", "Spacing", "Typeface". */
|
|
300
|
+
title: string;
|
|
301
|
+
/** Open state lives with the host, so it can persist it. */
|
|
302
|
+
open: boolean;
|
|
303
|
+
onToggle: (open: boolean) => void;
|
|
304
|
+
/**
|
|
305
|
+
* How many settings in this group differ from their default. Shown as a chip
|
|
306
|
+
* on the pill so a FOLDED group still admits it holds changes.
|
|
307
|
+
*/
|
|
308
|
+
count?: number;
|
|
309
|
+
/** Ties the pill to its body for assistive tech (`aria-controls`). */
|
|
310
|
+
id?: string;
|
|
311
|
+
children: ReactNode;
|
|
312
|
+
className?: string;
|
|
313
|
+
}
|
|
314
|
+
/**
|
|
315
|
+
* A group heading with nothing folded under it — see `SettingsGroupHead`.
|
|
316
|
+
*
|
|
317
|
+
* Deliberately NOT `SettingsGroupProps` minus a few fields: a heading has no
|
|
318
|
+
* open state to own, nothing to toggle, and no count, because a heading hides
|
|
319
|
+
* nothing that a count would have to admit to.
|
|
320
|
+
*/
|
|
321
|
+
interface SettingsGroupHeadProps {
|
|
322
|
+
/** The heading's name — "Items", "Spacing", "Typeface". */
|
|
323
|
+
title: string;
|
|
324
|
+
/**
|
|
325
|
+
* Which heading level this is in the HOST's outline. Defaults to 3, the level
|
|
326
|
+
* a panel section sits at under a panel title; only the host knows what sits
|
|
327
|
+
* above it.
|
|
328
|
+
*/
|
|
329
|
+
level?: 2 | 3 | 4 | 5 | 6;
|
|
330
|
+
/** A DOM id, so the rows it heads can point `aria-labelledby` at it. */
|
|
331
|
+
id?: string;
|
|
332
|
+
className?: string;
|
|
333
|
+
}
|
|
334
|
+
interface SettingsGroupSubProps {
|
|
335
|
+
/** The heading's name — "Content", "Background", "Image". */
|
|
336
|
+
title: string;
|
|
337
|
+
/**
|
|
338
|
+
* Which heading level this is in the HOST's outline. Defaults to 4 — one under
|
|
339
|
+
* `SettingsGroupHead`'s 3, because this tier sits under that pill. Only the
|
|
340
|
+
* host knows what is actually above it.
|
|
341
|
+
*/
|
|
342
|
+
level?: 2 | 3 | 4 | 5 | 6;
|
|
343
|
+
/** A DOM id, so the rows it heads can point `aria-labelledby` at it. */
|
|
344
|
+
id?: string;
|
|
345
|
+
className?: string;
|
|
346
|
+
}
|
|
347
|
+
interface SettingsFieldProps {
|
|
348
|
+
label: string;
|
|
349
|
+
/**
|
|
350
|
+
* An `id` for the label text, so a control that cannot be reached by
|
|
351
|
+
* `htmlFor` can still be named by it.
|
|
352
|
+
*
|
|
353
|
+
* That is the boolean row: `htmlFor` associates a <label> with a form
|
|
354
|
+
* control, and a switch is a <button>. Give the label an id here and hand the
|
|
355
|
+
* same id to the switch's `ariaLabelledBy`, and the accessible name and the
|
|
356
|
+
* visible name are the same string.
|
|
357
|
+
*/
|
|
358
|
+
labelId?: string;
|
|
359
|
+
/** An 18px glyph from `efficient-easel/icons` — one meaning, one SVG. */
|
|
360
|
+
icon?: ReactNode;
|
|
361
|
+
/** The current value, right-aligned on the label row ("34 px", "style"). */
|
|
362
|
+
value?: ReactNode;
|
|
363
|
+
/** One quiet line under the control. Use it instead of a clause-long label. */
|
|
364
|
+
hint?: ReactNode;
|
|
365
|
+
/** Marks the row as changed from its default: the label takes the accent. */
|
|
366
|
+
changed?: boolean;
|
|
367
|
+
/** Set when the control is a single form element, so the label points at it. */
|
|
368
|
+
htmlFor?: string;
|
|
369
|
+
/**
|
|
370
|
+
* A trailing control on the label row — a reset dot, a badge, or the SWITCH
|
|
371
|
+
* of a boolean row (card 8801, Option A: a yes-or-no setting is one line, not
|
|
372
|
+
* two, so the switch takes the spot a value like "34 px" normally sits in).
|
|
373
|
+
*/
|
|
374
|
+
action?: ReactNode;
|
|
375
|
+
/**
|
|
376
|
+
* The control itself, on its own line under the label.
|
|
377
|
+
*
|
|
378
|
+
* OPTIONAL since card 8801, and only so a boolean row can put its switch in
|
|
379
|
+
* `action` and render no control line at all. Omitting it is not a way to
|
|
380
|
+
* draw a label on its own: a field with NO children and NO action has no
|
|
381
|
+
* control on either slot, so it renders nothing rather than leaving an empty
|
|
382
|
+
* row behind. Every field that passes a control is unaffected.
|
|
383
|
+
*/
|
|
384
|
+
children?: ReactNode;
|
|
385
|
+
className?: string;
|
|
386
|
+
}
|
|
387
|
+
/**
|
|
388
|
+
* A switch has no text of its own, so without one of these it announces
|
|
389
|
+
* "switch, on" and nothing else. The union makes an unnamed switch a COMPILE
|
|
390
|
+
* error rather than a documented risk — both editors are about to grow six of
|
|
391
|
+
* these per panel, and every one is a chance to forget.
|
|
392
|
+
*
|
|
393
|
+
* Prefer `ariaLabelledBy` inside a `SettingsField`: point it at the field's
|
|
394
|
+
* `labelId` and the accessible name IS the visible label, one string doing two
|
|
395
|
+
* jobs, so the two cannot drift apart. `ariaLabel` is for a switch standing on
|
|
396
|
+
* its own with no visible label to point at.
|
|
397
|
+
*
|
|
398
|
+
* `?: never` on the other arm is what makes the two mutually exclusive while
|
|
399
|
+
* still letting the component destructure both names.
|
|
400
|
+
*/
|
|
401
|
+
type SettingsSwitchLabel = {
|
|
402
|
+
ariaLabel: string;
|
|
403
|
+
ariaLabelledBy?: never;
|
|
404
|
+
} | {
|
|
405
|
+
ariaLabelledBy: string;
|
|
406
|
+
ariaLabel?: never;
|
|
407
|
+
};
|
|
408
|
+
interface SettingsSwitchBaseProps {
|
|
409
|
+
/** Whether the setting is on. The HOST owns it; the switch never flips itself. */
|
|
410
|
+
checked: boolean;
|
|
411
|
+
/** Asked to flip. `next` is the value the merchant wants, not the current one. */
|
|
412
|
+
onChange: (next: boolean) => void;
|
|
413
|
+
disabled?: boolean;
|
|
414
|
+
id?: string;
|
|
415
|
+
className?: string;
|
|
416
|
+
}
|
|
417
|
+
type SettingsSwitchProps = SettingsSwitchBaseProps & SettingsSwitchLabel;
|
|
418
|
+
/**
|
|
419
|
+
* A DRILL-IN ROW — a name, a one-line preview, and a chevron that says pressing
|
|
420
|
+
* it takes the panel down a level (card 8819).
|
|
421
|
+
*
|
|
422
|
+
* The storefront editor's text editing becomes a drill-down: the section screen
|
|
423
|
+
* lists a section's text parts as these rows, and pressing one scopes the
|
|
424
|
+
* inspector to that one part's editor. It lives in the shell for the reason the
|
|
425
|
+
* rest of this grammar does — the campaign designer draws the same list, and a
|
|
426
|
+
* second hand-rolled copy is exactly how the two editors drift.
|
|
427
|
+
*
|
|
428
|
+
* CONTROLLED and hook-free like the other four: it holds no open/closed state of
|
|
429
|
+
* its own. Pressing it calls `onOpen`, and the HOST decides what drilling down
|
|
430
|
+
* means, because only the host owns the panel's navigation.
|
|
431
|
+
*/
|
|
432
|
+
interface SettingsDrillProps {
|
|
433
|
+
/** The part's name — the loud primary line ("Heading", "Slide name"). */
|
|
434
|
+
name: string;
|
|
435
|
+
/**
|
|
436
|
+
* One line of what the part reads as, shown quiet under the name and clipped
|
|
437
|
+
* with an ellipsis. Optional: a row can be a name alone, and an absent preview
|
|
438
|
+
* draws no empty second line rather than a blank one.
|
|
439
|
+
*/
|
|
440
|
+
preview?: ReactNode;
|
|
441
|
+
/**
|
|
442
|
+
* Mark the PREVIEW as belonging to a part with no place on the page — an
|
|
443
|
+
* identifier the eye never draws (a `Slide name`), reachable only from this
|
|
444
|
+
* list. It reads quiet and italic. The mark is on the preview, not the row:
|
|
445
|
+
* the name still reads as a name.
|
|
446
|
+
*/
|
|
447
|
+
offPage?: boolean;
|
|
448
|
+
/**
|
|
449
|
+
* A trailing glyph before the chevron — the "hidden on the page" eye is the
|
|
450
|
+
* standing case. A ReactNode, not a boolean, so the host owns the icon and its
|
|
451
|
+
* accessible name (a bare glyph says nothing to a screen reader on its own).
|
|
452
|
+
*/
|
|
453
|
+
status?: ReactNode;
|
|
454
|
+
/** Asked to open. The host owns what drilling down does. */
|
|
455
|
+
onOpen: () => void;
|
|
456
|
+
/** A DOM id for the row button, so a host can point a label at it. */
|
|
457
|
+
id?: string;
|
|
458
|
+
className?: string;
|
|
459
|
+
}
|
|
460
|
+
/** One of the nine positions {@link SettingsPlacementProps} offers. */
|
|
461
|
+
interface SettingsPlacementPoint {
|
|
462
|
+
/** What the host stores when this cell is pressed. Also the React key, so
|
|
463
|
+
* the nine must be distinct. */
|
|
464
|
+
value: string;
|
|
465
|
+
/** The cell's accessible name — "Top left", "Centre". Read aloud on its own,
|
|
466
|
+
* so it names the POSITION and never the value behind it. */
|
|
467
|
+
label: string;
|
|
468
|
+
/**
|
|
469
|
+
* This position cannot be chosen. The caller's arithmetic decides it (a
|
|
470
|
+
* position that already holds all it can draw), never this component.
|
|
471
|
+
* Disabled rather than absent: nine squares that became eight would read as
|
|
472
|
+
* a control that had broken.
|
|
473
|
+
*/
|
|
474
|
+
disabled?: boolean;
|
|
475
|
+
/** Why, on hover. Falls back to {@link label}, so a plain cell still has a
|
|
476
|
+
* tooltip and a closed one explains itself. */
|
|
477
|
+
title?: string;
|
|
478
|
+
/**
|
|
479
|
+
* Drawn IN PLACE OF the cell's dot, not beside it.
|
|
480
|
+
*
|
|
481
|
+
* The grid's default mark is a dot, because nine positions are answered by
|
|
482
|
+
* "which square", and a dot is the smallest thing that can say "this one".
|
|
483
|
+
* One control asks a different question with the same nine squares: the
|
|
484
|
+
* Scrolling Bar's `Scroll direction`, where four live cells mean a direction
|
|
485
|
+
* of TRAVEL and a dot cannot say which way. Those cells take an arrow.
|
|
486
|
+
*
|
|
487
|
+
* ⚠️ IT REPLACES THE DOT. A cell drawing both would put an arrow beside a dot
|
|
488
|
+
* inside a 26.67px square, and the dot would read as a second control. Leave
|
|
489
|
+
* it off and the cell renders exactly what it has always rendered — which is
|
|
490
|
+
* what keeps the five existing call sites byte-identical.
|
|
491
|
+
*
|
|
492
|
+
* Silent: the slot is `aria-hidden`, because the button around it already
|
|
493
|
+
* names the position and two voices in one cell is worse than one.
|
|
494
|
+
*/
|
|
495
|
+
icon?: ReactNode;
|
|
496
|
+
}
|
|
497
|
+
/** One of the four corners {@link SettingsCornersProps} offers. */
|
|
498
|
+
interface SettingsCornerPoint {
|
|
499
|
+
/** What the host stores when this corner is on. Also the React key and half
|
|
500
|
+
* of the cell's class name, so the four must be distinct and CSS-safe. */
|
|
501
|
+
value: string;
|
|
502
|
+
/** The cell's accessible name — "Top left". Read aloud on its own, so it
|
|
503
|
+
* names the CORNER and never the value behind it. */
|
|
504
|
+
label: string;
|
|
505
|
+
/**
|
|
506
|
+
* This corner cannot be chosen. The caller's arithmetic decides it (a box
|
|
507
|
+
* whose neighbour already rounds that edge), never this component. Disabled
|
|
508
|
+
* rather than absent: four cells that became three would read as a control
|
|
509
|
+
* that had broken.
|
|
510
|
+
*/
|
|
511
|
+
disabled?: boolean;
|
|
512
|
+
/** Why, on hover. Falls back to {@link label}, so a plain cell still has a
|
|
513
|
+
* tooltip and a closed one explains itself. */
|
|
514
|
+
title?: string;
|
|
515
|
+
}
|
|
516
|
+
interface SettingsCornersProps {
|
|
517
|
+
/** The group's accessible name. NOT drawn — the visible label belongs to the
|
|
518
|
+
* `SettingsField` row around it, the same split `SettingsSwitch` makes. */
|
|
519
|
+
label: string;
|
|
520
|
+
/**
|
|
521
|
+
* The corners that are ON, already resolved by the host: this component has
|
|
522
|
+
* no opinion about what an absent or empty value means. An empty array is a
|
|
523
|
+
* real answer (every corner square), so a host that reads absence as "all
|
|
524
|
+
* four" resolves that before handing this in.
|
|
525
|
+
*/
|
|
526
|
+
value: readonly string[];
|
|
527
|
+
/**
|
|
528
|
+
* The four cells. No default: a component that invented four corner values
|
|
529
|
+
* would be answering a question only the host can answer, and the order it
|
|
530
|
+
* stores them in is the host's too.
|
|
531
|
+
*/
|
|
532
|
+
corners: readonly SettingsCornerPoint[];
|
|
533
|
+
/** The new set, in the order the cells were pressed. The host normalises. */
|
|
534
|
+
onChange: (next: string[]) => void;
|
|
535
|
+
/** A DOM id for the group, so a host can point a label at it. */
|
|
536
|
+
id?: string;
|
|
537
|
+
className?: string;
|
|
538
|
+
}
|
|
539
|
+
interface SettingsPlacementProps {
|
|
540
|
+
/** The radiogroup's accessible name. NOT drawn — the visible label belongs to
|
|
541
|
+
* the `SettingsField` row around it, the same split `SettingsSwitch` makes. */
|
|
542
|
+
label: string;
|
|
543
|
+
/** The chosen position, already resolved by the host: this component has no
|
|
544
|
+
* opinion about what an absent value means. */
|
|
545
|
+
value: string;
|
|
546
|
+
/**
|
|
547
|
+
* The nine cells, in reading order — top-left first, bottom-right last. No
|
|
548
|
+
* default: a component that invented nine positions would be answering a
|
|
549
|
+
* question only the host can answer, and the two callers ask different ones
|
|
550
|
+
* (a picture's focal point, a block's anchor).
|
|
551
|
+
*/
|
|
552
|
+
points: readonly SettingsPlacementPoint[];
|
|
553
|
+
onChange: (next: string) => void;
|
|
554
|
+
/** A DOM id for the radiogroup, so a host can point a label at it. */
|
|
555
|
+
id?: string;
|
|
556
|
+
className?: string;
|
|
557
|
+
}
|
|
558
|
+
/**
|
|
559
|
+
* A FULL-WIDTH ACTION BUTTON that sits under a group's rows and COMMITS.
|
|
560
|
+
*
|
|
561
|
+
* "Apply to all cards" is the first caller: the merchant has styled one card and
|
|
562
|
+
* wants the other eleven to match. That is a WRITE, and a write is why this is
|
|
563
|
+
* not `SettingsDrill` with the chevron taken off — see the docblock on
|
|
564
|
+
* `SettingsAction` for the whole of that argument.
|
|
565
|
+
*
|
|
566
|
+
* Controlled and hook-free like every primitive here. The shell owns the look
|
|
567
|
+
* and the announcement; the host owns the words, the write and the timer that
|
|
568
|
+
* takes {@link SettingsActionBaseProps.done} back down.
|
|
569
|
+
*/
|
|
570
|
+
interface SettingsActionBaseProps {
|
|
571
|
+
/**
|
|
572
|
+
* What the button says — "Apply to all cards". The package supplies NO
|
|
573
|
+
* default, the way `SettingsCorners` invents no corner names: the words belong
|
|
574
|
+
* to the section that knows what it is about to rewrite.
|
|
575
|
+
*/
|
|
576
|
+
label: ReactNode;
|
|
577
|
+
/** Pressed. The write is the host's; this component performs none of it. */
|
|
578
|
+
onPress: () => void;
|
|
579
|
+
/**
|
|
580
|
+
* The press is not available. A REAL `<button disabled>`, dimmed and still
|
|
581
|
+
* there — never hidden. A control that vanishes leaves a merchant looking for
|
|
582
|
+
* something they remember being there, and hiding it also takes away the one
|
|
583
|
+
* place the reason can be read. Say the reason in {@link
|
|
584
|
+
* SettingsActionNote.note}.
|
|
585
|
+
*/
|
|
586
|
+
disabled?: boolean;
|
|
587
|
+
/**
|
|
588
|
+
* The confirmation state — the press has landed. The HOST owns it and owns the
|
|
589
|
+
* timer that takes it back down, because only the host knows when its write
|
|
590
|
+
* finished.
|
|
591
|
+
*
|
|
592
|
+
* The shell owns what it LOOKS like and, more importantly, that it is
|
|
593
|
+
* ANNOUNCED: this button rewrites things that are not on screen, so a host
|
|
594
|
+
* that only swapped its own label would get the words right and the
|
|
595
|
+
* announcement wrong. See `SettingsAction`'s `role="status"` region.
|
|
596
|
+
*/
|
|
597
|
+
done?: boolean;
|
|
598
|
+
/**
|
|
599
|
+
* What the button says while {@link done} — "Applied to 12 cards". The host's
|
|
600
|
+
* words again, and the string the live region announces. Falls back to
|
|
601
|
+
* {@link label}, so a host that flips `done` without a second string still
|
|
602
|
+
* gets the state and the announcement rather than nothing.
|
|
603
|
+
*/
|
|
604
|
+
doneLabel?: ReactNode;
|
|
605
|
+
/** A glyph before the words. `aria-hidden`: the label already names the press. */
|
|
606
|
+
icon?: ReactNode;
|
|
607
|
+
className?: string;
|
|
608
|
+
}
|
|
609
|
+
/**
|
|
610
|
+
* The note, and the id it needs to reach anybody.
|
|
611
|
+
*
|
|
612
|
+
* `note` is where "every card already matches" or "this rewrites all 12 cards"
|
|
613
|
+
* goes, and it is wired to the button by `aria-describedby` — which needs a DOM
|
|
614
|
+
* id, and this package is hook-free so it cannot invent one (no `useId`). The
|
|
615
|
+
* union makes the id a COMPILE error rather than a silent hole: pass a note and
|
|
616
|
+
* you must pass an `id`.
|
|
617
|
+
*
|
|
618
|
+
* ⚠️ THIS IS THE REASON THE CONTROL IS HERE RATHER THAN IN THE SHOP.
|
|
619
|
+
* `efficient-shop`'s own `components/AddChildRow.tsx` explains its disabled
|
|
620
|
+
* button with a `title`, and a `title` on a DISABLED button reaches no screen
|
|
621
|
+
* reader and no keyboard — the tooltip needs a hover the control no longer
|
|
622
|
+
* answers. That hole cannot be patched from the shop's side while the shop draws
|
|
623
|
+
* its own button, so the button moves here and brings the wiring with it.
|
|
624
|
+
*
|
|
625
|
+
* `?: never` on the other arm is what makes the two mutually exclusive while
|
|
626
|
+
* still letting the component destructure both names — the same shape
|
|
627
|
+
* {@link SettingsSwitchLabel} uses.
|
|
628
|
+
*/
|
|
629
|
+
type SettingsActionNote = {
|
|
630
|
+
note: ReactNode;
|
|
631
|
+
id: string;
|
|
632
|
+
} | {
|
|
633
|
+
note?: never;
|
|
634
|
+
id?: string;
|
|
635
|
+
};
|
|
636
|
+
type SettingsActionProps = SettingsActionBaseProps & SettingsActionNote;
|
|
637
|
+
|
|
638
|
+
/**
|
|
639
|
+
* The tab strip under the panel head.
|
|
640
|
+
*
|
|
641
|
+
* It replaces the old stack of fold headers a merchant had to open and hunt
|
|
642
|
+
* through: the buckets a section actually has become tabs, in one fixed order,
|
|
643
|
+
* so the panel keeps the same shape on every section. A section shows only the
|
|
644
|
+
* tabs it has — pass three items and three tabs render.
|
|
645
|
+
*
|
|
646
|
+
* Controlled: the host decides `activeId`. No hooks, no browser globals.
|
|
647
|
+
*
|
|
648
|
+
* A tab can be WIRED to what it shows (`panelId` / `htmlId` on the item). That
|
|
649
|
+
* is not decoration: `role="tab"` promises a panel, and a tab strip that names
|
|
650
|
+
* none announces "tab, 1 of 4, selected" over nothing. The shell cannot supply
|
|
651
|
+
* those ids — only the host knows the element the settings land in — but until
|
|
652
|
+
* card 8802.d it could not ACCEPT them either, so no host could fix it. A tab
|
|
653
|
+
* with nothing to point at renders no attribute at all; an empty
|
|
654
|
+
* `aria-controls` is worse than a missing one, because it names an element
|
|
655
|
+
* that does not exist instead of admitting there is none.
|
|
656
|
+
*
|
|
657
|
+
* A SEGMENTED CONTROL (card 66138). The strip is a grey track and the chosen
|
|
658
|
+
* bucket is a raised white chip on it. Lewis, looking at a real panel: the tab
|
|
659
|
+
* he is NOT on does not invite a click — it does not even look like a button.
|
|
660
|
+
* The track is the answer to that, because it makes every segment look
|
|
661
|
+
* pressable, where a mark under the chosen tab only ever made the chosen tab
|
|
662
|
+
* louder. It replaces the underline strip and card 8811's tint ground; see the
|
|
663
|
+
* long note on `.es-tab.is-active` in `panel.css`.
|
|
664
|
+
*
|
|
665
|
+
* A segment takes the width its OWN label needs and shares what is left over,
|
|
666
|
+
* rather than every segment taking the width of the widest label. Equal
|
|
667
|
+
* segments were measured and cost too much: three of them need a 267px track
|
|
668
|
+
* and four need 355px, against the 281px the shop's default panel gives.
|
|
669
|
+
*
|
|
670
|
+
* THE WORD DROPS WHERE IT STOPS FITTING. Natural widths fit two and three
|
|
671
|
+
* buckets across the whole panel-drag band; they do not fit four, where the
|
|
672
|
+
* last segment ends 39px past the panel edge. So given an `icon`, a segment can
|
|
673
|
+
* spend less width instead of more — the word goes and the glyph stays.
|
|
674
|
+
*
|
|
675
|
+
* Where that happens depends on HOW MANY buckets a section has, because with
|
|
676
|
+
* natural widths the question is about the sum of the labels:
|
|
677
|
+
*
|
|
678
|
+
* two buckets both words, at every width the panel drags to.
|
|
679
|
+
* three buckets words down to 240px, then glyph plus the chosen word.
|
|
680
|
+
* four buckets glyph plus the chosen word under 320px; glyphs under 200px.
|
|
681
|
+
*
|
|
682
|
+
* Which state is a CSS decision, taken against the PANEL's width — see the
|
|
683
|
+
* `@container` blocks in `panel.css`, and the wrapper below that gives them
|
|
684
|
+
* something to measure. React cannot know the answer at render time, and it
|
|
685
|
+
* does not need to: BOTH parts are always in the markup, and CSS shows the one
|
|
686
|
+
* that fits. The word is only ever hidden, never dropped, and the label rides
|
|
687
|
+
* along as `title` and `aria-label` on every tab in every state, so the glyph
|
|
688
|
+
* never has to speak for the tab.
|
|
689
|
+
*
|
|
690
|
+
* A tab with no icon is untouched at every width — it has nothing to fall back
|
|
691
|
+
* to, so it keeps its word.
|
|
692
|
+
*/
|
|
693
|
+
declare function SettingsTabs({ items, activeId, onSelect, ariaLabel, className, }: SettingsTabsProps): react.JSX.Element;
|
|
694
|
+
|
|
695
|
+
/**
|
|
696
|
+
* A segmented control: one row of chips, one of them pressed.
|
|
697
|
+
*
|
|
698
|
+
* THE SAME CHIP AS `SettingsTabs`, AND A DIFFERENT PROMISE. Everything a
|
|
699
|
+
* merchant sees is shared — the grey track, the raised white chip for the one
|
|
700
|
+
* he is on, the glyph drawn at every width, the word that drops where it stops
|
|
701
|
+
* fitting. `panel.css` dresses both from one rule, so there is no second copy
|
|
702
|
+
* of the chip grammar to drift.
|
|
703
|
+
*
|
|
704
|
+
* What differs is what the control SAYS it does. `SettingsTabs` is a tablist
|
|
705
|
+
* and `role="tab"` promises that pressing a chip switches a panel. These chips
|
|
706
|
+
* switch nothing: they write a value and the panel stays where it is. So this
|
|
707
|
+
* is a group of pressed buttons, and its marker is `aria-pressed` — which is
|
|
708
|
+
* also what the storefront's own `Segmented` has carried since the 2026-08-06
|
|
709
|
+
* audit, so the two editors keep saying the same thing about the same control.
|
|
710
|
+
*
|
|
711
|
+
* That distinction is not decoration. Reach for `SettingsTabs` on a Weight row
|
|
712
|
+
* and a control that is correct today starts announcing "tab, 1 of 4, selected"
|
|
713
|
+
* over a panel that is not there — three times in one part panel. It is the
|
|
714
|
+
* same split the package already made between `SettingsGroup` and
|
|
715
|
+
* `SettingsGroupHead`: same pill, different promise, two components.
|
|
716
|
+
*
|
|
717
|
+
* Controlled: the host owns `value`. No hooks, no browser globals.
|
|
718
|
+
*
|
|
719
|
+
* ── WHERE THE WORD DROPS, AND WHY THE STRIP HAS TO SAY ───────────────────────
|
|
720
|
+
*
|
|
721
|
+
* The tab strip's tiers are keyed on how many tabs there are, which works
|
|
722
|
+
* because the shell knows those four words. It cannot work here. Measured
|
|
723
|
+
* (`tests/fixtures/segmented-fit.html`), Case — Normal / Uppercase / Lowercase
|
|
724
|
+
* — needs 296px for its words and Alignment — Left / Centre / Right — needs
|
|
725
|
+
* 221px. Both are three-option rows, and the panel drags between 181px and
|
|
726
|
+
* 281px, so a single three-option threshold is visibly wrong for one of them.
|
|
727
|
+
* Whether the words fit is a question about the SUM of the labels; the count is
|
|
728
|
+
* only a stand-in, and it stops standing in the moment a second strip arrives.
|
|
729
|
+
*
|
|
730
|
+
* So the strip carries its own measurement. `fit` is the width of its WORDS,
|
|
731
|
+
* nothing else — the chip's padding, glyph and gaps are the shell's, named on
|
|
732
|
+
* the panel root, and `panel.css` adds them. Those three custom properties
|
|
733
|
+
* (`wordFitVars`) are that measurement and the count, handed to the stylesheet, which
|
|
734
|
+
* compares them against the panel's live width in `calc()`. A container query
|
|
735
|
+
* could not: its condition takes a literal length, so it cannot ask a question
|
|
736
|
+
* whose answer is different for every strip.
|
|
737
|
+
*
|
|
738
|
+
* Without `fit` nothing drops — an unmeasured strip keeps every word at every
|
|
739
|
+
* width, which is exactly what a strip with no glyphs already does.
|
|
740
|
+
*
|
|
741
|
+
* ── WHAT CARD 66303 WIDENED, AND WHY IT HAD TO BE HERE ───────────────────────
|
|
742
|
+
*
|
|
743
|
+
* `efficient-shop` draws its own `Segmented` beside this one and could not stop:
|
|
744
|
+
* four things twelve live call sites need were missing here. Generic `V`
|
|
745
|
+
* (weights are numbers, "image side" is a boolean on every published page), a
|
|
746
|
+
* DRAWN label with its own `title`, and `disabled` — on one option or on the
|
|
747
|
+
* strip.
|
|
748
|
+
*
|
|
749
|
+
* The last two could not have been bridged by a wrapper the shop writes, which
|
|
750
|
+
* is the reason they are props and not a consumer's problem. This component
|
|
751
|
+
* renders the `<button>`s, so nothing outside it can disable them; dimming a
|
|
752
|
+
* parent instead leaves them focusable and announcing as ENABLED, which is the
|
|
753
|
+
* regression the 2026-08-06 audit and card 66034 closed. And `label` is spent as
|
|
754
|
+
* `title` and `aria-label` as well as the visible mark, so a node in it reaches
|
|
755
|
+
* both attributes as `[object Object]` — while splitting it into `icon` +
|
|
756
|
+
* `label` renders BOTH spans, and Alignment would draw the rule and the word
|
|
757
|
+
* "Left" beside it.
|
|
758
|
+
*/
|
|
759
|
+
declare function SettingsSegmented<V = string>({ options, value, onChange, ariaLabel, fit, disabled, className, }: SettingsSegmentedProps<V>): react.JSX.Element;
|
|
760
|
+
|
|
761
|
+
/**
|
|
762
|
+
* A group of settings under a soft pill row that folds with − / +.
|
|
763
|
+
*
|
|
764
|
+
* The sign is drawn by `panel.css` from `aria-expanded`, so the open state has
|
|
765
|
+
* exactly one home (the attribute assistive tech already reads) instead of a
|
|
766
|
+
* second copy in the markup.
|
|
767
|
+
*
|
|
768
|
+
* `count` is the reason a fold here is safe: a folded group that holds changes
|
|
769
|
+
* still says so on its pill, so nothing a merchant changed can hide.
|
|
770
|
+
*/
|
|
771
|
+
declare function SettingsGroup({ title, open, onToggle, count, id, children, className, }: SettingsGroupProps): react.JSX.Element;
|
|
772
|
+
|
|
773
|
+
/**
|
|
774
|
+
* A group heading with NO group under it — the pill on its own.
|
|
775
|
+
*
|
|
776
|
+
* It exists because Puck lays the inspector out as a FLAT sibling list: a field
|
|
777
|
+
* renders next to the heading above it, never inside it, so nothing here can
|
|
778
|
+
* wrap the rows that follow. Heading a run of settings is therefore a job for a
|
|
779
|
+
* component that heads and nothing else.
|
|
780
|
+
*
|
|
781
|
+
* `SettingsGroup` cannot do that job. It always renders its body div, and its
|
|
782
|
+
* pill is always a <button> carrying `aria-expanded`, `aria-controls` and the
|
|
783
|
+
* − / + sign. Used header-only it would draw a control that claims to expand
|
|
784
|
+
* something, does nothing when clicked, and tells a screen reader the same lie.
|
|
785
|
+
* A component that folds and a component that titles are two different things,
|
|
786
|
+
* and this is the one that titles.
|
|
787
|
+
*
|
|
788
|
+
* So it is a HEADING, not a control: a real <h3> (the level is the host's, since
|
|
789
|
+
* only the host knows the page outline around it) with no role bolted on, no
|
|
790
|
+
* expanded state, no sign, no body. In a flat list the heading list is the only
|
|
791
|
+
* structure a screen reader has left to navigate by, which is the whole reason
|
|
792
|
+
* this renders a heading element rather than a styled <div>.
|
|
793
|
+
*
|
|
794
|
+
* It wears `.es-group-head` — the same pill as a real group's — so a section
|
|
795
|
+
* that folds and a section that does not read as the same panel. `panel.css`
|
|
796
|
+
* scopes the cursor and the hover tint to `button.es-group-head`, so the
|
|
797
|
+
* heading does not offer a press it cannot honour.
|
|
798
|
+
*
|
|
799
|
+
* NO `count`. A folded group needs one because a fold can hide a change; a
|
|
800
|
+
* heading folds nothing, so nothing can hide behind it.
|
|
801
|
+
*/
|
|
802
|
+
declare function SettingsGroupHead({ title, level, id, className, }: SettingsGroupHeadProps): react.JSX.Element;
|
|
803
|
+
|
|
804
|
+
/**
|
|
805
|
+
* The QUIET second tier — a heading one rung under `SettingsGroupHead`.
|
|
806
|
+
*
|
|
807
|
+
* It exists because a panel sometimes has two levels of heading and this package
|
|
808
|
+
* only ever drew one. `SettingsGroupHead` is a filled pill, and a pill cannot
|
|
809
|
+
* serve a tier AND its subordinate: two runs of settings under one pill each
|
|
810
|
+
* need a name, and giving them the same pill says they are siblings of the thing
|
|
811
|
+
* above them rather than parts of it.
|
|
812
|
+
*
|
|
813
|
+
* THE SHOP ALREADY DREW THIS LOOK, TWICE, WHICH IS WHY IT BELONGS HERE.
|
|
814
|
+
* `efficient-shop` hand-rolled `.sf-field-group` in the settings panel and
|
|
815
|
+
* `.sf-group-label` in the Layers tree, and they are the same type to the
|
|
816
|
+
* decimal — 10.5px / 600 / .07em / muted / uppercase. One look, hand-rolled in
|
|
817
|
+
* two panels of one editor, is the definition of a thing the shell should own.
|
|
818
|
+
*
|
|
819
|
+
* ⚠️ NOT THE PILL WITH ITS FILL SWITCHED OFF, and that is the whole reason this
|
|
820
|
+
* is a component and not a `variant` flag on the other one. The shop's own
|
|
821
|
+
* `WHY NOT SettingsGroupHead` docblock refused that shape in advance: a named
|
|
822
|
+
* look the shell draws, "not a boolean that switches the pill off. A flag whose
|
|
823
|
+
* job is to turn the design off moves the fork inside the shell instead of
|
|
824
|
+
* removing it." So there is no path from here to the pill's look and none back —
|
|
825
|
+
* two tiers, two looks, each one this package's own.
|
|
826
|
+
*
|
|
827
|
+
* WHY IT IS QUIET BY TYPE AND THE PILL IS LOUD BY GROUND. The pill separates
|
|
828
|
+
* itself from the settings around it with a fill; this separates itself with
|
|
829
|
+
* CASE and COLOUR and takes no ground at all. That is what lets the two sit in
|
|
830
|
+
* one panel without competing, and it is also why this tier can carry no rule:
|
|
831
|
+
* a hairline under a heading separates it from the rows it heads, which is the
|
|
832
|
+
* opposite of what a heading is for. Removing those rules is the change the
|
|
833
|
+
* owner asked for.
|
|
834
|
+
*
|
|
835
|
+
* A REAL HEADING, defaulting to `h4` — one under the pill's default `h3`,
|
|
836
|
+
* because that is where a subordinate heading sits when the pill above it took
|
|
837
|
+
* the 3. The host may say otherwise; only the host knows the outline around it.
|
|
838
|
+
* Puck lays an inspector out as a FLAT sibling list, so the heading list is the
|
|
839
|
+
* only structure a screen reader has left to navigate by.
|
|
840
|
+
*
|
|
841
|
+
* NO fold, no count, no sign, no body — the same discipline as
|
|
842
|
+
* {@link SettingsGroupHead}, and for the same reason: a component that folds and
|
|
843
|
+
* a component that titles are two different things, and this is one that titles.
|
|
844
|
+
*/
|
|
845
|
+
declare function SettingsGroupSub({ title, level, id, className, }: SettingsGroupSubProps): react.JSX.Element;
|
|
846
|
+
|
|
847
|
+
/**
|
|
848
|
+
* One setting: a label row, then its control.
|
|
849
|
+
*
|
|
850
|
+
* The row is stacked rather than side-by-side so the control gets the panel's
|
|
851
|
+
* full width — a 308px panel cannot afford a label column AND a usable slider.
|
|
852
|
+
* The label reads small and quiet; the VALUE sits at the end of the same line in
|
|
853
|
+
* the text colour, which is what a merchant scans for. That contrast is the
|
|
854
|
+
* point: before this, label and value looked alike and the panel read as noise.
|
|
855
|
+
*
|
|
856
|
+
* `hint` is where a clause-long explanation goes. A label is a name, never a
|
|
857
|
+
* sentence.
|
|
858
|
+
*
|
|
859
|
+
* A BOOLEAN row is the one exception to "control on its own line" (card 8801,
|
|
860
|
+
* Option A, approved). A switch is small and it is the whole control, so it
|
|
861
|
+
* rides the label line in the `action` slot and the row costs one line instead
|
|
862
|
+
* of two — which is what lets a 308px panel show four settings where it used to
|
|
863
|
+
* show three. That is the only reason `children` is optional: a field with no
|
|
864
|
+
* children draws no control row, and its label line IS the row.
|
|
865
|
+
*
|
|
866
|
+
* A field with no children AND no action has no control on either slot. That is
|
|
867
|
+
* a mistake at the call site, so it renders NOTHING — an empty row would hide
|
|
868
|
+
* the mistake behind 20px of blank panel.
|
|
869
|
+
*/
|
|
870
|
+
declare function SettingsField({ label, labelId, icon, value, hint, changed, htmlFor, action, children, className, }: SettingsFieldProps): react.JSX.Element | null;
|
|
871
|
+
|
|
872
|
+
/**
|
|
873
|
+
* The yes-or-no control — ONE shape, both editors (card 8801).
|
|
874
|
+
*
|
|
875
|
+
* Point 5 of the approved settings design says one switch style everywhere.
|
|
876
|
+
* Before this, the storefront editor drew a tick box and the campaign designer
|
|
877
|
+
* drew a green switch, so a merchant met two shapes for the same question and
|
|
878
|
+
* had to learn the control twice. Both now import this file, which is the only
|
|
879
|
+
* thing that stops them drifting apart again.
|
|
880
|
+
*
|
|
881
|
+
* It is a real <button>, and that is the whole keyboard story: a button is
|
|
882
|
+
* activated by Space and by Enter in every browser, and that activation IS a
|
|
883
|
+
* click — so the one `onClick` below serves mouse and keyboard alike. A div
|
|
884
|
+
* wearing `role="switch"` would need a key handler of our own. Do not add one.
|
|
885
|
+
* `type="button"` keeps it from submitting a form it happens to sit in.
|
|
886
|
+
*
|
|
887
|
+
* It cannot exist without a NAME. `SettingsSwitchProps` demands `ariaLabel` or
|
|
888
|
+
* `ariaLabelledBy` and forbids both at once, so an unnamed switch — which a
|
|
889
|
+
* screen reader reads out as "switch, on" and nothing more — is a compile error
|
|
890
|
+
* instead of something a docstring asks you to remember.
|
|
891
|
+
*
|
|
892
|
+
* ONE-WAY DEPENDENCY. `SettingsField` imports this module so it can recognise a
|
|
893
|
+
* switch in its `action` slot and hand it the field's label as a name. This
|
|
894
|
+
* module must NEVER import `SettingsField` back — that would close a cycle
|
|
895
|
+
* between two siblings in the same folder. It imports `./types` and nothing
|
|
896
|
+
* else, and `tests/settings-switch.test.tsx` (o) keeps it that way.
|
|
897
|
+
*
|
|
898
|
+
* CONTROLLED and hook-free like the other three: `checked` comes from the host
|
|
899
|
+
* and the switch only ever ASKS to be flipped. That is what keeps this module a
|
|
900
|
+
* directive-free leaf a Next-16 server component can import.
|
|
901
|
+
*/
|
|
902
|
+
declare function SettingsSwitch({ checked, onChange, disabled, id, ariaLabel, ariaLabelledBy, className, }: SettingsSwitchProps): react.JSX.Element;
|
|
903
|
+
|
|
904
|
+
/**
|
|
905
|
+
* A DRILL-IN ROW — a name, a one-line preview, and a chevron (card 8819).
|
|
906
|
+
*
|
|
907
|
+
* The storefront editor's text editing becomes a panel drill-down: the section
|
|
908
|
+
* screen no longer holds a text box for every part; it LISTS the parts as these
|
|
909
|
+
* rows, and pressing one takes the inspector down a level to that part's own
|
|
910
|
+
* editor. A part the eye hid — or one that never draws on the page at all, like
|
|
911
|
+
* a `Slide name` — has no words on the canvas to click, so this list is the only
|
|
912
|
+
* way back to it. That is why the list exists, and why the row is the shell's,
|
|
913
|
+
* not the shop's: the campaign designer draws the same list.
|
|
914
|
+
*
|
|
915
|
+
* IT IS ONE BUTTON, not a control beside some text. The name and the preview are
|
|
916
|
+
* the button's own content, so they ARE its accessible name — a chevron floating
|
|
917
|
+
* next to unlabelled text would be a press a screen reader could not describe.
|
|
918
|
+
* The chevron is `aria-hidden` (pure direction); the status slot is NOT, because
|
|
919
|
+
* its meaning is the host's to name — it passes an icon carrying its own label
|
|
920
|
+
* (a `title`, a visually-hidden word), and hiding the slot would bury that.
|
|
921
|
+
*
|
|
922
|
+
* CONTROLLED and hook-free like the other four primitives: it owns no open state.
|
|
923
|
+
* Pressing it calls `onOpen`, and the host decides what drilling down means —
|
|
924
|
+
* which keeps this a directive-free leaf a Next-16 server component can import.
|
|
925
|
+
*/
|
|
926
|
+
declare function SettingsDrill({ name, preview, offPage, status, onOpen, id, className, }: SettingsDrillProps): react.JSX.Element;
|
|
927
|
+
|
|
928
|
+
/**
|
|
929
|
+
* A 3×3 grid of positions, one of them lit — "which of these nine?".
|
|
930
|
+
*
|
|
931
|
+
* ONE CONTROL, TWO QUESTIONS THAT ARE THE SAME QUESTION. A merchant asks it of
|
|
932
|
+
* an image ("which part of this crop stays in view?") and of a block ("where on
|
|
933
|
+
* the thing behind it does this sit?"). Both are one of nine positions, both
|
|
934
|
+
* want the merchant to PRESS THE CORNER rather than read nine words out of a
|
|
935
|
+
* dropdown, and drawing them as two controls would be two things to keep
|
|
936
|
+
* looking alike for ever.
|
|
937
|
+
*
|
|
938
|
+
* ── WHY IT IS HERE NOW, AND WHY IT WAS NOT BEFORE ───────────────────────────
|
|
939
|
+
*
|
|
940
|
+
* It lived in `efficient-shop` as `FocalPicker`, and its own comment recorded
|
|
941
|
+
* the rule for moving it: card 66189 said *"the day a second section wants it,
|
|
942
|
+
* that is the card that promotes it"*. Card 66214 was that second caller and
|
|
943
|
+
* deliberately declined — the control was days old and had already changed
|
|
944
|
+
* signature once, so freezing its shape in a released package would have been
|
|
945
|
+
* early. That was right then.
|
|
946
|
+
*
|
|
947
|
+
* It is not right now: the shape has not moved since, and the sections that
|
|
948
|
+
* hold boxes take it past six call sites. A local copy with that many callers
|
|
949
|
+
* is the shape that becomes a second source, so it comes here — the rule its
|
|
950
|
+
* own author wrote, applied on the day it fires.
|
|
951
|
+
*
|
|
952
|
+
* ── WHAT IT DOES NOT DO ─────────────────────────────────────────────────────
|
|
953
|
+
*
|
|
954
|
+
* It renders the CONTROL only. The row around it — the glyph, the label, the
|
|
955
|
+
* caption — is `SettingsField`'s, exactly as `SettingsSwitch` splits. `label`
|
|
956
|
+
* is the radiogroup's accessible name, never a visible one.
|
|
957
|
+
*
|
|
958
|
+
* ── A CELL MAY DRAW A GLYPH INSTEAD OF ITS DOT ──────────────────────────────
|
|
959
|
+
*
|
|
960
|
+
* Nine positions are answered by "which square", and a dot is the smallest mark
|
|
961
|
+
* that says "this one". ONE control asks a different question with the same
|
|
962
|
+
* nine squares: the Scrolling Bar's `Scroll direction`, where four live cells
|
|
963
|
+
* mean a direction of TRAVEL — and a dot cannot say which way. Those cells pass
|
|
964
|
+
* an `icon` on the point, and it is drawn in place of the dot.
|
|
965
|
+
*
|
|
966
|
+
* THE GLYPH COMES FROM THE ICON LIBRARY, never from in here. `efficient-easel/icons`
|
|
967
|
+
* holds `direction-left`, `direction-right`, `direction-up` and `direction-down`
|
|
968
|
+
* for exactly this control. A shape drawn inside this component would be the
|
|
969
|
+
* second source of truth the whole package exists to prevent — the same rule
|
|
970
|
+
* that stops a panel drawing its own glyph one level up.
|
|
971
|
+
*
|
|
972
|
+
* It also holds no opinion about what a position MEANS. The host hands nine
|
|
973
|
+
* values and gets one back; whether that value becomes a CSS `object-position`
|
|
974
|
+
* or a grid anchor is the host's business, and keeping it that way is what lets
|
|
975
|
+
* one control answer both questions.
|
|
976
|
+
*
|
|
977
|
+
* Controlled and hook-free, like every primitive here, so a Next server
|
|
978
|
+
* component can import this module.
|
|
979
|
+
*/
|
|
980
|
+
declare function SettingsPlacement({ label, value, points, onChange, id, className, }: SettingsPlacementProps): react.JSX.Element;
|
|
981
|
+
|
|
982
|
+
/**
|
|
983
|
+
* Four corners of a box, any number of them lit — "which of these corners?".
|
|
984
|
+
*
|
|
985
|
+
* ── WHY IT IS NOT {@link SettingsPlacement} WITH FOUR CELLS ──────────────────
|
|
986
|
+
*
|
|
987
|
+
* The nine-square asks WHICH ONE and gets one answer back; a radiogroup, and
|
|
988
|
+
* pressing a second cell releases the first. This asks WHICH ONES and gets a
|
|
989
|
+
* set: a merchant rounding a stack of boxes turns on the top two of the box at
|
|
990
|
+
* the top and the bottom two of the box at the bottom, so the run reads as one
|
|
991
|
+
* shape. Those are different questions with different keyboards — a radiogroup
|
|
992
|
+
* moves with the arrows and admits exactly one, a set is toggled with Space and
|
|
993
|
+
* admits any number — so they are two controls rather than one control with a
|
|
994
|
+
* `multiple` flag that changes what its own ARIA means.
|
|
995
|
+
*
|
|
996
|
+
* They still look like siblings, because a merchant meeting the second one
|
|
997
|
+
* should already know how to press it: the same track, the same chip, the same
|
|
998
|
+
* "the chosen thing is the darkest thing here" reading the placement grid and
|
|
999
|
+
* the segmented strip already share.
|
|
1000
|
+
*
|
|
1001
|
+
* ── THE CELL DRAWS THE ANSWER, NOT A DOT ────────────────────────────────────
|
|
1002
|
+
*
|
|
1003
|
+
* The placement grid's cell holds a dot, because a POSITION has no shape of its
|
|
1004
|
+
* own to show. A corner does: each cell here holds a small square whose matching
|
|
1005
|
+
* corner is rounded when the corner is on, and square when it is off. So the
|
|
1006
|
+
* control is a picture of what the setting does rather than four labels a
|
|
1007
|
+
* merchant has to hold in their head — and it stays readable for someone who
|
|
1008
|
+
* cannot separate the accent from the ground, because the SHAPE carries the
|
|
1009
|
+
* state as well as the fill.
|
|
1010
|
+
*
|
|
1011
|
+
* ── WHAT IT DOES NOT DO ─────────────────────────────────────────────────────
|
|
1012
|
+
*
|
|
1013
|
+
* It renders the CONTROL only. The row around it — the glyph, the label, the
|
|
1014
|
+
* caption — is `SettingsField`'s, the same split `SettingsSwitch` and
|
|
1015
|
+
* `SettingsPlacement` make. `label` is the group's accessible name, never a
|
|
1016
|
+
* visible one.
|
|
1017
|
+
*
|
|
1018
|
+
* It also holds no opinion about what a corner MEANS. The host hands the four
|
|
1019
|
+
* corners it wants offered and gets a set back; whether that set becomes a
|
|
1020
|
+
* `border-radius` shorthand, four custom properties or a stored preset is the
|
|
1021
|
+
* host's business, and keeping it that way is what lets one control serve a
|
|
1022
|
+
* section box, a card and whatever asks next.
|
|
1023
|
+
*
|
|
1024
|
+
* ⚠️ IT HAS NO OPINION ABOUT "ALL FOUR" EITHER. An empty set is a real answer
|
|
1025
|
+
* (every corner square) and so is a full one; a host that treats absence as
|
|
1026
|
+
* "all four" resolves that BEFORE handing `value` in, exactly as the placement
|
|
1027
|
+
* grid requires its value already resolved. A control that invented the default
|
|
1028
|
+
* would be answering a question only the host can answer.
|
|
1029
|
+
*
|
|
1030
|
+
* Controlled and hook-free, like every primitive here, so a Next server
|
|
1031
|
+
* component can import this module.
|
|
1032
|
+
*/
|
|
1033
|
+
declare function SettingsCorners({ label, value, corners, onChange, id, className, }: SettingsCornersProps): react.JSX.Element;
|
|
1034
|
+
|
|
1035
|
+
/**
|
|
1036
|
+
* A FULL-WIDTH BUTTON UNDER A GROUP'S ROWS THAT COMMITS A CHANGE.
|
|
1037
|
+
*
|
|
1038
|
+
* The storefront editor's first caller is "Apply to all cards": a merchant has
|
|
1039
|
+
* styled one card and wants the other eleven to match. It sits at the bottom of
|
|
1040
|
+
* the group whose rows it copies, it is one press wide, and pressing it WRITES.
|
|
1041
|
+
*
|
|
1042
|
+
* ── WHY IT IS NOT `SettingsDrill` RENAMED ───────────────────────────────────
|
|
1043
|
+
*
|
|
1044
|
+
* A DRILL NAVIGATES. Its row is a name, a one-line preview and a chevron; it
|
|
1045
|
+
* commits nothing, and pressing it takes the panel down a level to the part's
|
|
1046
|
+
* own editor. The chevron is the promise: something opens, nothing changes, and
|
|
1047
|
+
* the way back is the way you came.
|
|
1048
|
+
*
|
|
1049
|
+
* THIS COMMITS AND STAYS PUT. No preview, because there is no value to preview —
|
|
1050
|
+
* it holds none. No chevron, because nothing opens. A disabled state that
|
|
1051
|
+
* explains ITSELF, which a drill has no need of: a row that only navigates is
|
|
1052
|
+
* rarely unavailable, and this one is unavailable often (every card already
|
|
1053
|
+
* matches, nothing to copy yet). And a CONFIRMATION state, which a drill has no
|
|
1054
|
+
* concept of at all — a doorway does not report back.
|
|
1055
|
+
*
|
|
1056
|
+
* Pointing a drill at this would ship a row that LOOKS LIKE A DOORWAY and is
|
|
1057
|
+
* actually a write. A merchant who has learnt that these rows open something
|
|
1058
|
+
* would press it to look, and eleven cards would change.
|
|
1059
|
+
*
|
|
1060
|
+
* ── THE PRESS IS ANNOUNCED, NOT JUST DRAWN ──────────────────────────────────
|
|
1061
|
+
*
|
|
1062
|
+
* The whole point of this button is that it rewrites things that are NOT ON
|
|
1063
|
+
* SCREEN — the other eleven cards, the sections below the fold. So the change
|
|
1064
|
+
* has to arrive as words: the `role="status" aria-live="polite"` region is the
|
|
1065
|
+
* shell's, and it is why `done` is a prop rather than a label swap the host
|
|
1066
|
+
* could do on its own. A host that only swapped the label would get the words
|
|
1067
|
+
* right and the announcement wrong, and nobody reviewing that diff would see the
|
|
1068
|
+
* hole — which is the kind of thing this package exists to hold once.
|
|
1069
|
+
*
|
|
1070
|
+
* The region is rendered EMPTY until `done`, and the announcement is the text
|
|
1071
|
+
* arriving in a region that was already there. A region that appears at the same
|
|
1072
|
+
* moment as its text is not reliably announced.
|
|
1073
|
+
*
|
|
1074
|
+
* ── AND THE NOTE IS WHY IT IS HERE RATHER THAN IN THE SHOP ──────────────────
|
|
1075
|
+
*
|
|
1076
|
+
* `note` is wired to the button by `aria-describedby`. `efficient-shop`'s own
|
|
1077
|
+
* `components/AddChildRow.tsx` explains its disabled button with a `title`, and
|
|
1078
|
+
* a `title` on a disabled button reaches no screen reader and no keyboard — the
|
|
1079
|
+
* tooltip wants a hover the control has stopped answering. The shop cannot patch
|
|
1080
|
+
* that from its own side while it draws its own button.
|
|
1081
|
+
*
|
|
1082
|
+
* Controlled and hook-free, like every primitive here, so a Next-16 server
|
|
1083
|
+
* component can import this module. The host owns the write, the `done` flag and
|
|
1084
|
+
* the timer that takes it back down.
|
|
1085
|
+
*/
|
|
1086
|
+
declare function SettingsAction({ label, onPress, disabled, note, done, doneLabel, icon, id, className, }: SettingsActionProps): react.JSX.Element;
|
|
1087
|
+
|
|
1088
|
+
export { SettingsAction, type SettingsActionBaseProps, type SettingsActionNote, type SettingsActionProps, type SettingsCornerPoint, SettingsCorners, type SettingsCornersProps, SettingsDrill, type SettingsDrillProps, SettingsField, type SettingsFieldProps, SettingsGroup, SettingsGroupHead, type SettingsGroupHeadProps, type SettingsGroupProps, SettingsGroupSub, type SettingsGroupSubProps, SettingsPlacement, type SettingsPlacementPoint, type SettingsPlacementProps, SettingsSegmented, type SettingsSegmentedFit, type SettingsSegmentedOption, type SettingsSegmentedProps, SettingsSwitch, type SettingsSwitchBaseProps, type SettingsSwitchLabel, type SettingsSwitchProps, type SettingsTabItem, SettingsTabs, type SettingsTabsProps };
|