efficient-easel 0.0.0 → 0.96.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.
Files changed (119) hide show
  1. package/LICENSE +10 -0
  2. package/README.md +723 -4
  3. package/dist/autosave/index.d.ts +101 -0
  4. package/dist/autosave/index.js +1 -0
  5. package/dist/blocks/index.d.ts +295 -0
  6. package/dist/blocks/index.js +0 -0
  7. package/dist/buckets-1vRlQceY.d.ts +13 -0
  8. package/dist/canvas/index.d.ts +194 -0
  9. package/dist/canvas/index.js +1 -0
  10. package/dist/chrome/chrome.css +570 -0
  11. package/dist/chrome/index.d.ts +219 -0
  12. package/dist/chrome/index.js +21 -0
  13. package/dist/chunk-2LUSK32B.js +1 -0
  14. package/dist/chunk-2OAZYSWJ.js +1 -0
  15. package/dist/chunk-67VZRQLG.js +13 -0
  16. package/dist/chunk-6JDII7N3.js +1 -0
  17. package/dist/chunk-6OPSJGAI.js +1 -0
  18. package/dist/chunk-6WY6ZXJG.js +1 -0
  19. package/dist/chunk-7A7O2JWQ.js +1 -0
  20. package/dist/chunk-BZ5ASXOL.js +1 -0
  21. package/dist/chunk-CERTFXCV.js +13 -0
  22. package/dist/chunk-EFTTSIMK.js +1 -0
  23. package/dist/chunk-FI23ALVJ.js +1 -0
  24. package/dist/chunk-FJE4IDBQ.js +1 -0
  25. package/dist/chunk-GGTPNGFJ.js +1 -0
  26. package/dist/chunk-H4B72XCJ.js +1 -0
  27. package/dist/chunk-H6DDIBQ4.js +1 -0
  28. package/dist/chunk-KDIVA6WD.js +1 -0
  29. package/dist/chunk-LPRQDDQO.js +1 -0
  30. package/dist/chunk-MCLBHENY.js +1 -0
  31. package/dist/chunk-MFIRYGHW.js +1 -0
  32. package/dist/chunk-NKB3KVAB.js +1 -0
  33. package/dist/chunk-O2NLQBFX.js +1 -0
  34. package/dist/chunk-OGZFDJ6G.js +1 -0
  35. package/dist/chunk-OWBO4WKA.js +0 -0
  36. package/dist/chunk-QB3BZIRA.js +5 -0
  37. package/dist/chunk-RE4LP46F.js +1 -0
  38. package/dist/chunk-STFMZJKU.js +14 -0
  39. package/dist/chunk-SUO5627P.js +1 -0
  40. package/dist/chunk-TDA4HH54.js +1 -0
  41. package/dist/chunk-WK3AFPXY.js +1 -0
  42. package/dist/chunk-YIMRTIBN.js +3 -0
  43. package/dist/chunk-YNZZYHA4.js +450 -0
  44. package/dist/chunk-ZAOM6PFH.js +1 -0
  45. package/dist/chunk-ZNCEOSXO.js +1 -0
  46. package/dist/copy/copy.css +282 -0
  47. package/dist/copy/index.d.ts +336 -0
  48. package/dist/copy/index.js +10 -0
  49. package/dist/data/index.d.ts +208 -0
  50. package/dist/data/index.js +1 -0
  51. package/dist/drag/drag.css +360 -0
  52. package/dist/drag/index.d.ts +379 -0
  53. package/dist/drag/index.js +18 -0
  54. package/dist/edit-lock/index.d.ts +354 -0
  55. package/dist/edit-lock/index.js +1 -0
  56. package/dist/editor/editor.css +58 -0
  57. package/dist/editor/index.d.ts +361 -0
  58. package/dist/editor/index.js +9 -0
  59. package/dist/fields/fields.css +207 -0
  60. package/dist/fields/index.d.ts +273 -0
  61. package/dist/fields/index.js +46 -0
  62. package/dist/gold/gold.css +196 -0
  63. package/dist/gold/index.d.ts +280 -0
  64. package/dist/gold/index.js +1 -0
  65. package/dist/icons/index.d.ts +474 -0
  66. package/dist/icons/index.js +1 -0
  67. package/dist/index.d.ts +6 -0
  68. package/dist/index.js +1 -0
  69. package/dist/markup/index.d.ts +452 -0
  70. package/dist/markup/index.js +1 -0
  71. package/dist/menu/index.d.ts +49 -0
  72. package/dist/menu/index.js +15 -0
  73. package/dist/menu/menu.css +254 -0
  74. package/dist/node-ops/index.d.ts +91 -0
  75. package/dist/node-ops/index.js +1 -0
  76. package/dist/panel/index.d.ts +1088 -0
  77. package/dist/panel/index.js +16 -0
  78. package/dist/panel/panel.css +1460 -0
  79. package/dist/pick/index.d.ts +153 -0
  80. package/dist/pick/index.js +14 -0
  81. package/dist/pick/pick.css +235 -0
  82. package/dist/plan-DtNw2VuI.d.ts +88 -0
  83. package/dist/rail/index.d.ts +145 -0
  84. package/dist/rail/index.js +1 -0
  85. package/dist/rail/rail.css +175 -0
  86. package/dist/render/index.d.ts +117 -0
  87. package/dist/render/index.js +5 -0
  88. package/dist/resolve-DpH8YQOA.d.ts +57 -0
  89. package/dist/shell/index.d.ts +159 -0
  90. package/dist/shell/index.js +1 -0
  91. package/dist/shell/shell.css +122 -0
  92. package/dist/strings/catalogue.d.ts +141 -0
  93. package/dist/strings/catalogue.js +1 -0
  94. package/dist/strings/index.d.ts +56 -0
  95. package/dist/strings/index.js +2 -0
  96. package/dist/style-clipboard/index.d.ts +184 -0
  97. package/dist/style-clipboard/index.js +1 -0
  98. package/dist/tree/index.d.ts +173 -0
  99. package/dist/tree/index.js +1 -0
  100. package/dist/tree/tree.css +618 -0
  101. package/dist/types-CEqUFH2j.d.ts +129 -0
  102. package/dist/types-CuNiSnX6.d.ts +204 -0
  103. package/dist/types-DNrn_w4V.d.ts +88 -0
  104. package/dist/types-DziKCwsN.d.ts +105 -0
  105. package/dist/types-_Vayx8ov.d.ts +153 -0
  106. package/dist/types-pGBXevBU.d.ts +131 -0
  107. package/dist/useNodeOps-6IAej9jy.d.ts +68 -0
  108. package/dist/watch-DmasQA61.d.ts +60 -0
  109. package/dist/wordFit-9B9nqyf3.d.ts +35 -0
  110. package/dist/words-B2YLaKEr.d.ts +30 -0
  111. package/dist/words-B87GIFWH.d.ts +86 -0
  112. package/dist/words-BK-npta-.d.ts +27 -0
  113. package/dist/words-CD3WADyb.d.ts +51 -0
  114. package/dist/words-CsRWXkY4.d.ts +129 -0
  115. package/dist/words-D9ChvbNC.d.ts +52 -0
  116. package/dist/words-DOhF-4D4.d.ts +35 -0
  117. package/dist/words-DagJdwIA.d.ts +78 -0
  118. package/dist/words-DfxEp_RH.d.ts +142 -0
  119. 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 };