@transtyle/exporter-bootstrap 0.1.0-alpha.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.
@@ -0,0 +1,665 @@
1
+ /**
2
+ * AL1.2 (docs/plan/bootstrap-component-tier.md): the binding cross-walk —
3
+ * which meaning each in-inventory Bootstrap variable reads. Data only; the
4
+ * emission semantics are AL1.3's. Completeness is enforced by
5
+ * scripts/check-bootstrap-surface.mjs: every in-inventory variable must be
6
+ * bound here (emit), explicitly classified (drop), or mechanically covered
7
+ * (chained / aliases-global / Bootstrap `null`-family marker).
8
+ *
9
+ * Recipe forms in `emit`:
10
+ * { comp: 'button.padding-x' } — component-tier slot (COMPONENT_CATALOG)
11
+ * { sem: 'space.2' } — exporter-private semantic binding
12
+ * { sem: ..., cls: 'approximated', note } — inexact binding, honestly classed
13
+ * { trans: { duration, easing } } — transition shorthand recipe:
14
+ * AL1.3 keeps Bootstrap's default property list and substitutes timing —
15
+ * always classed `approximated` (one duration/easing for a compound value).
16
+ *
17
+ * `drop` entries: { cls: 'dropped' | 'unsupported', note } per
18
+ * validation-and-coverage.md's class directions — `dropped` = has no token
19
+ * meaning / deliberately not driven; `unsupported` = a real themable slot the
20
+ * IR has no vocabulary for yet (catalog-growth data, by construction).
21
+ *
22
+ * Mechanical classes (computed in coverageForVariable, not listed per name):
23
+ * chained — the value references other `$` variables; Bootstrap's own
24
+ * `!default` expression chains it from roots we do drive
25
+ * (the plan's "bind the root decision" rule).
26
+ * follows-global — aliases a global var(--bs-*) custom property, which the
27
+ * semantic tier drives today.
28
+ * inherits-driven — a cascade no-op marker on an *inherited* property whose
29
+ * effective value still comes from something we drive (see
30
+ * INHERITS_DRIVEN). Real coverage, exactly as PrimeNG's
31
+ * `inherited` reason is.
32
+ * inherit-default — the remaining `null`/`inherit`/`transparent`/`currentcolor`
33
+ * no-op markers; setting them would change cascade
34
+ * semantics, not theme values.
35
+ *
36
+ * Recipe `part`: 'alpha' | 'opaque' for colors; a composite member name
37
+ * ('fontSize' | 'fontWeight' | 'lineHeight') for `semantic.type.role.*`.
38
+ */
39
+
40
+ // Shared honest notes (referenced by many entries — keep the wording in one place).
41
+ const N_PARAM =
42
+ 'Bootstrap state/subtle-color derivation knob (shade/tint/scale %). Transtyle derives those result colors directly (grid -hover/-active cells, tint/outline prominences); the percentage itself has no token meaning — the derived colors reach the target via the CSS-variable path (AL1.4).';
43
+ const N_ASSET =
44
+ 'embedded SVG asset (icon/glyph) — the IR has no asset/icon vocabulary yet. AL2 growth signal: component icon slots.';
45
+ const N_OPACITY =
46
+ 'opacity value with no shared meaning: AL2 promoted `semantic.opacity.disabled` (the one both reference targets needed) but this slot is a different concept — a veil strength, a shimmer range, or a glyph-specific alpha. Growth signal only if a second target needs the identical one.';
47
+ // Probed against PrimeNG's real inventory in proposal 0004 — the earlier wording
48
+ // ("the IR has no component-size vocabulary yet") promised a vocabulary the
49
+ // evidence says nobody needs. Of the ten concepts in this bucket, six are
50
+ // one-sided (PrimeNG hard-codes what Bootstrap tokenizes: spinner, popover,
51
+ // modal ladder, offcanvas) or false friends ($toast-max-width 350px vs
52
+ // PrimeNG's fixed `width: 22rem`; $dropdown-min-width is the menu, PrimeNG's
53
+ // `dropdown.width` is the trigger), and two more share the concept while
54
+ // disagreeing on its architecture. A note that names a growth signal that isn't
55
+ // there is the coverage-report version of over-claiming.
56
+ const N_BESPOKE =
57
+ 'bespoke component geometry with no shared meaning — measured against the second reference target (proposal 0004), this is a slot PrimeNG either hard-codes or models differently, not a gap in the IR. Not a growth signal.';
58
+ // The two shapes proposal 0004 separated out of N_BESPOKE, because they are
59
+ // genuinely different situations and a single note flattened them.
60
+ const N_ARCH_DISAGREE =
61
+ 'both reference targets have this concept and model it differently, so no single authored value satisfies both: Bootstrap sizes it in `em` (proportional to local font size, one dimension), PrimeNG in absolute `rem` with independent width/height and an sm/md/lg ladder. The disagreement is the finding (proposal 0004) — same shape as the size ladder AL2 rejected.';
62
+ const N_ICON =
63
+ 'component icon size. The strongest open catalog-growth signal, and still one-sided: PrimeNG models this systematically (50 `icon.size` slots across 31 families, with a shared value set), Bootstrap has three ad-hoc ones with no common root. Promoting today would export one target’s architecture into the catalog — revisit when a third target arrives with a systematic icon-size concept (proposal 0004).';
64
+ const N_STRUCT =
65
+ 'structural/behavioral option (layout, cursor, transform, ordering, animation mechanics), not a theme value.';
66
+ const N_FILTER = 'CSS filter trick — presentation mechanics, not a token meaning.';
67
+ const N_EM =
68
+ 'em-relative in Bootstrap (proportional to local font size); bound to the nearest rem rung — proportional intent is approximated, not preserved.';
69
+
70
+ const T_FAST = { trans: { duration: 'duration.fast', easing: 'easing.standard' } };
71
+
72
+ export const DESCRIPTORS = {
73
+ // ---- content-ish micro components -------------------------------------
74
+ caret: {
75
+ drop: { 'caret-width': { cls: 'dropped', note: N_STRUCT + ' (caret glyph geometry)' } },
76
+ },
77
+ hr: { drop: { 'hr-opacity': { cls: 'unsupported', note: N_OPACITY } } },
78
+ legend: {
79
+ emit: {
80
+ 'legend-margin-bottom': { sem: 'space.2' },
81
+ 'legend-font-size': {
82
+ sem: 'type.size.xl',
83
+ cls: 'approximated',
84
+ note: '1.5rem vs the 1.563rem scale rung — nearest meaning',
85
+ },
86
+ // Bootstrap nulls the weight (inherit body 400), which is incoherent with
87
+ // the 1.5rem size it does set: a legend is a fieldset's title. Bound to
88
+ // the title role's weight so size and weight come from the same meaning.
89
+ 'legend-font-weight': { sem: 'type.role.title.md', part: 'fontWeight' },
90
+ },
91
+ },
92
+ mark: { emit: { 'mark-padding': { sem: 'space.1', cls: 'approximated', note: N_EM } } },
93
+ kbd: {
94
+ emit: {
95
+ 'kbd-padding-y': {
96
+ sem: 'space.1',
97
+ cls: 'approximated',
98
+ note: '.1875rem — sub-rung; nearest meaning',
99
+ },
100
+ 'kbd-padding-x': {
101
+ sem: 'space.1',
102
+ cls: 'approximated',
103
+ note: '.375rem — between rungs; nearest meaning',
104
+ },
105
+ },
106
+ },
107
+ pre: {},
108
+ code: {},
109
+ dt: {},
110
+ figure: {},
111
+ blockquote: {},
112
+ thumbnail: { emit: { 'thumbnail-padding': { sem: 'space.1' } } },
113
+ placeholder: {
114
+ drop: {
115
+ 'placeholder-opacity-max': { cls: 'unsupported', note: N_OPACITY },
116
+ 'placeholder-opacity-min': { cls: 'unsupported', note: N_OPACITY },
117
+ },
118
+ },
119
+
120
+ // ---- table --------------------------------------------------------------
121
+ table: {
122
+ emit: {
123
+ 'table-cell-padding-y': { sem: 'space.2' },
124
+ 'table-cell-padding-x': { sem: 'space.2' },
125
+ 'table-cell-padding-y-sm': { sem: 'space.1' },
126
+ 'table-cell-padding-x-sm': { sem: 'space.1' },
127
+ },
128
+ drop: {
129
+ 'table-cell-vertical-align': { cls: 'dropped', note: N_STRUCT },
130
+ 'table-th-font-weight': {
131
+ cls: 'dropped',
132
+ note: 'Bootstrap null marker with no non-null meaning to bind',
133
+ },
134
+ 'table-striped-bg-factor': { cls: 'dropped', note: N_PARAM },
135
+ 'table-active-bg-factor': { cls: 'dropped', note: N_PARAM },
136
+ 'table-hover-bg-factor': { cls: 'dropped', note: N_PARAM },
137
+ 'table-border-factor': { cls: 'dropped', note: N_PARAM },
138
+ 'table-bg-scale': { cls: 'dropped', note: N_PARAM },
139
+ 'table-striped-order': { cls: 'dropped', note: N_STRUCT },
140
+ 'table-striped-columns-order': { cls: 'dropped', note: N_STRUCT },
141
+ },
142
+ },
143
+
144
+ // ---- buttons + the shared input-btn root --------------------------------
145
+ input: {
146
+ emit: {
147
+ // AL2 RESOLVED the AL1.2 contested call: Bootstrap's shared root now
148
+ // binds to the promoted `component.control.*` slots — the shared
149
+ // interactive-control geometry — and `$btn-padding-*` below binds to
150
+ // `component.button.*`, which defaults from control. That reproduces
151
+ // Bootstrap's own two-level model exactly ($btn-padding-y defaults to
152
+ // $input-btn-padding-y), so authoring `control.*` moves buttons and
153
+ // fields together while authoring `button.*` moves only buttons.
154
+ 'input-btn-padding-y': { comp: 'control.padding-y' },
155
+ 'input-btn-padding-x': { comp: 'control.padding-x' },
156
+ // The sm/lg ladder stays exporter-private: both targets have one, but
157
+ // they disagree on the rungs (PrimeNG sm-x = space.3, Bootstrap = space.2),
158
+ // so there is no convergent value to promote — proposal 0003 §deferred.
159
+ 'input-btn-padding-y-sm': { sem: 'space.1' },
160
+ 'input-btn-padding-x-sm': { sem: 'space.2' },
161
+ 'input-btn-padding-y-lg': { sem: 'space.2' },
162
+ 'input-btn-padding-x-lg': { sem: 'space.4' },
163
+ 'input-border-radius': { comp: 'control.radius' },
164
+ 'input-transition': T_FAST,
165
+ },
166
+ },
167
+ btn: {
168
+ emit: {
169
+ // Review finding (2026-07-23): $btn-border-radius defaults to the global
170
+ // var(--bs-border-radius) — without an explicit binding, an authored
171
+ // component.button.radius (the catalog's flagship slot) would be
172
+ // silently ignored on Bootstrap. Bound; unauthored builds emit the
173
+ // resolved default (≡ what the chain produced) — explicit, not drifted.
174
+ 'btn-border-radius': { comp: 'button.radius' },
175
+ // AL2: Bootstrap chains these from the shared $input-btn-* root by
176
+ // default; binding them explicitly to the button layer is what makes
177
+ // per-component authoring reach buttons without moving form fields.
178
+ 'btn-padding-x': { comp: 'button.padding-x' },
179
+ 'btn-padding-y': { comp: 'button.padding-y' },
180
+ 'btn-disabled-opacity': {
181
+ sem: 'opacity.disabled',
182
+ cls: 'approximated',
183
+ note: "Bootstrap's own default is .65; the promoted catalog slot is the shared disabled-state opacity (AL2)",
184
+ },
185
+ 'btn-transition': T_FAST,
186
+ },
187
+ drop: {
188
+ 'btn-hover-bg-shade-amount': { cls: 'dropped', note: N_PARAM },
189
+ 'btn-hover-bg-tint-amount': { cls: 'dropped', note: N_PARAM },
190
+ 'btn-hover-border-shade-amount': { cls: 'dropped', note: N_PARAM },
191
+ 'btn-hover-border-tint-amount': { cls: 'dropped', note: N_PARAM },
192
+ 'btn-active-bg-shade-amount': { cls: 'dropped', note: N_PARAM },
193
+ 'btn-active-bg-tint-amount': { cls: 'dropped', note: N_PARAM },
194
+ 'btn-active-border-shade-amount': { cls: 'dropped', note: N_PARAM },
195
+ 'btn-active-border-tint-amount': { cls: 'dropped', note: N_PARAM },
196
+ },
197
+ },
198
+ 'btn-close': {
199
+ drop: {
200
+ 'btn-close-width': { cls: 'unsupported', note: N_ICON + ' (close glyph)' },
201
+ 'btn-close-padding-x': { cls: 'unsupported', note: N_BESPOKE + ' ' + N_EM },
202
+ 'btn-close-bg': { cls: 'unsupported', note: N_ASSET },
203
+ 'btn-close-opacity': { cls: 'unsupported', note: N_OPACITY },
204
+ 'btn-close-hover-opacity': { cls: 'unsupported', note: N_OPACITY },
205
+ 'btn-close-focus-opacity': { cls: 'unsupported', note: N_OPACITY },
206
+ 'btn-close-disabled-opacity': {
207
+ cls: 'unsupported',
208
+ note: 'the same concept as `semantic.opacity.disabled`, but not bindable to it: Bootstrap composes this against the glyph\'s own resting alpha ($btn-close-opacity: .5), so writing the catalog value (0.6) here would make the disabled close button MORE visible than the enabled one. Needs a compositional recipe (catalog factor x the target\'s resting alpha) the cross-walk has no form for yet — a real AL2-style growth signal, unlike the other -opacity rows in this family.',
209
+ },
210
+ 'btn-close-white-filter': { cls: 'dropped', note: N_FILTER },
211
+ },
212
+ },
213
+
214
+ // ---- forms ---------------------------------------------------------------
215
+ 'form-text': { emit: { 'form-text-margin-top': { sem: 'space.1' } } },
216
+ 'form-label': {
217
+ emit: {
218
+ 'form-label-margin-bottom': { sem: 'space.2' },
219
+ // Bootstrap leaves these `null` (labels inherit body type). The IR's type
220
+ // roles say a label is its own role — and Bootstrap has the exact slots
221
+ // to say so, so this is a binding, not an invention.
222
+ 'form-label-font-size': { sem: 'type.role.label.md', part: 'fontSize' },
223
+ 'form-label-font-weight': { sem: 'type.role.label.md', part: 'fontWeight' },
224
+ },
225
+ },
226
+ 'form-color': { drop: { 'form-color-width': { cls: 'unsupported', note: N_BESPOKE } } },
227
+ 'form-check': {
228
+ emit: {
229
+ 'form-check-margin-bottom': {
230
+ sem: 'space.1',
231
+ cls: 'approximated',
232
+ note: '.125rem — sub-rung; nearest meaning',
233
+ },
234
+ 'form-check-input-border-radius': { sem: 'radius.sm', cls: 'approximated', note: N_EM },
235
+ 'form-check-inline-margin-end': { sem: 'space.4' },
236
+ 'form-check-input-disabled-opacity': {
237
+ sem: 'opacity.disabled',
238
+ cls: 'approximated',
239
+ note: "Bootstrap's own default is .5; unified onto the shared disabled-state opacity (AL2)",
240
+ },
241
+ },
242
+ drop: {
243
+ 'form-check-input-width': {
244
+ cls: 'unsupported',
245
+ note: N_ARCH_DISAGREE + ' (checkbox/radio box)',
246
+ },
247
+ 'form-check-input-active-filter': { cls: 'dropped', note: N_FILTER },
248
+ 'form-check-radio-border-radius': {
249
+ cls: 'dropped',
250
+ note: N_STRUCT + ' (50% is the radio shape identity, not a radius token)',
251
+ },
252
+ 'form-check-input-checked-bg-image': { cls: 'unsupported', note: N_ASSET },
253
+ 'form-check-radio-checked-bg-image': { cls: 'unsupported', note: N_ASSET },
254
+ 'form-check-input-indeterminate-bg-image': { cls: 'unsupported', note: N_ASSET },
255
+ },
256
+ },
257
+ 'form-switch': {
258
+ emit: { 'form-switch-transition': T_FAST },
259
+ drop: {
260
+ 'form-switch-width': { cls: 'unsupported', note: N_ARCH_DISAGREE + ' (switch track)' },
261
+ 'form-switch-bg-image': { cls: 'unsupported', note: N_ASSET },
262
+ 'form-switch-focus-bg-image': { cls: 'unsupported', note: N_ASSET },
263
+ 'form-switch-checked-bg-image': { cls: 'unsupported', note: N_ASSET },
264
+ 'form-switch-checked-bg-position': { cls: 'dropped', note: N_STRUCT },
265
+ },
266
+ },
267
+ 'form-select': {
268
+ drop: {
269
+ 'form-select-bg-size': { cls: 'dropped', note: N_STRUCT + ' (indicator geometry)' },
270
+ 'form-select-indicator': { cls: 'unsupported', note: N_ASSET },
271
+ },
272
+ },
273
+ 'form-range': {
274
+ emit: {
275
+ 'form-range-track-height': { sem: 'space.2' },
276
+ 'form-range-track-border-radius': {
277
+ sem: 'radius.full',
278
+ cls: 'approximated',
279
+ note: 'Bootstrap says 1rem; the intent (pill track) maps to radius.full',
280
+ },
281
+ 'form-range-thumb-border-radius': {
282
+ sem: 'radius.full',
283
+ cls: 'approximated',
284
+ note: 'pill/circular thumb intent',
285
+ },
286
+ 'form-range-thumb-transition': T_FAST,
287
+ },
288
+ drop: {
289
+ 'form-range-track-width': { cls: 'dropped', note: N_STRUCT },
290
+ 'form-range-track-cursor': { cls: 'dropped', note: N_STRUCT },
291
+ 'form-range-thumb-width': { cls: 'unsupported', note: N_BESPOKE + ' (thumb size)' },
292
+ 'form-range-thumb-border': { cls: 'dropped', note: N_STRUCT },
293
+ },
294
+ },
295
+ 'form-floating': {
296
+ emit: {
297
+ 'form-floating-line-height': { sem: 'type.leading.tight' },
298
+ 'form-floating-padding-y': { sem: 'space.4' },
299
+ 'form-floating-transition': {
300
+ trans: { duration: 'duration.fast', easing: 'easing.standard' },
301
+ },
302
+ },
303
+ drop: {
304
+ 'form-floating-input-padding-t': {
305
+ cls: 'dropped',
306
+ note: N_STRUCT + ' (float-label mechanics: padding split is coupled to the transform math)',
307
+ },
308
+ 'form-floating-input-padding-b': {
309
+ cls: 'dropped',
310
+ note: N_STRUCT + ' (float-label mechanics)',
311
+ },
312
+ 'form-floating-label-height': { cls: 'dropped', note: N_STRUCT + ' (float-label mechanics)' },
313
+ 'form-floating-label-opacity': { cls: 'unsupported', note: N_OPACITY },
314
+ 'form-floating-label-transform': { cls: 'dropped', note: N_STRUCT },
315
+ },
316
+ },
317
+ 'form-feedback': {
318
+ drop: {
319
+ 'form-feedback-icon-valid': { cls: 'unsupported', note: N_ASSET },
320
+ 'form-feedback-icon-invalid': { cls: 'unsupported', note: N_ASSET },
321
+ },
322
+ },
323
+ 'form-valid': {},
324
+ 'form-invalid': {},
325
+ 'form-validation': {},
326
+ 'form-file': {},
327
+ 'input-group': {},
328
+
329
+ // ---- navigation -----------------------------------------------------------
330
+ 'nav-link': {
331
+ emit: {
332
+ 'nav-link-padding-y': { sem: 'space.2' },
333
+ 'nav-link-padding-x': { sem: 'space.4' },
334
+ 'nav-link-transition': T_FAST,
335
+ },
336
+ },
337
+ nav: {
338
+ emit: {
339
+ 'nav-underline-gap': { sem: 'space.4' },
340
+ 'nav-underline-border-width': {
341
+ sem: 'border-width.medium',
342
+ cls: 'approximated',
343
+ note: 'medium rung (2px) ≡ Bootstrap’s .125rem at a 16px root; unit semantics differ (px vs rem-relative)',
344
+ },
345
+ },
346
+ },
347
+ navbar: {
348
+ emit: {
349
+ 'navbar-nav-link-padding-x': { sem: 'space.2' },
350
+ 'navbar-brand-margin-end': { sem: 'space.4' },
351
+ 'navbar-toggler-padding-y': { sem: 'space.1' },
352
+ 'navbar-toggler-padding-x': { sem: 'space.3' },
353
+ 'navbar-toggler-transition': T_FAST,
354
+ },
355
+ drop: {
356
+ 'navbar-light-toggler-icon-bg': { cls: 'unsupported', note: N_ASSET },
357
+ 'navbar-dark-toggler-icon-bg': { cls: 'unsupported', note: N_ASSET },
358
+ },
359
+ },
360
+ breadcrumb: {
361
+ emit: {
362
+ 'breadcrumb-padding-y': { sem: 'space.0' },
363
+ 'breadcrumb-padding-x': { sem: 'space.0' },
364
+ 'breadcrumb-item-padding-x': { sem: 'space.2' },
365
+ 'breadcrumb-margin-bottom': { sem: 'space.4' },
366
+ },
367
+ drop: { 'breadcrumb-divider': { cls: 'dropped', note: N_STRUCT + ' (divider glyph)' } },
368
+ },
369
+ pagination: {
370
+ emit: {
371
+ 'pagination-padding-y': {
372
+ sem: 'space.2',
373
+ cls: 'approximated',
374
+ note: '.375rem — between rungs; matches the button/field padding intent',
375
+ },
376
+ 'pagination-padding-x': { sem: 'space.3' },
377
+ 'pagination-padding-y-sm': { sem: 'space.1' },
378
+ 'pagination-padding-x-sm': { sem: 'space.2' },
379
+ 'pagination-padding-y-lg': { sem: 'space.3' },
380
+ 'pagination-padding-x-lg': { sem: 'space.6' },
381
+ 'pagination-transition': T_FAST,
382
+ },
383
+ drop: {
384
+ 'pagination-focus-outline': {
385
+ cls: 'dropped',
386
+ note: N_STRUCT + ' (focus handled by focus-ring, driven semantically)',
387
+ },
388
+ },
389
+ },
390
+
391
+ // ---- surfaces / overlays ---------------------------------------------------
392
+ card: {},
393
+ accordion: {
394
+ emit: {
395
+ 'accordion-padding-y': { sem: 'space.4' },
396
+ 'accordion-padding-x': { sem: 'space.5' },
397
+ 'accordion-icon-transition': {
398
+ trans: { duration: 'duration.normal', easing: 'easing.standard' },
399
+ },
400
+ },
401
+ drop: {
402
+ 'accordion-icon-width': { cls: 'unsupported', note: N_ICON + ' (chevron)' },
403
+ 'accordion-icon-transform': { cls: 'dropped', note: N_STRUCT },
404
+ 'accordion-button-icon': { cls: 'unsupported', note: N_ASSET },
405
+ 'accordion-button-active-icon': { cls: 'unsupported', note: N_ASSET },
406
+ },
407
+ },
408
+ dropdown: {
409
+ emit: {
410
+ 'dropdown-padding-x': { sem: 'space.0' },
411
+ 'dropdown-padding-y': { sem: 'space.2' },
412
+ 'dropdown-spacer': {
413
+ sem: 'space.1',
414
+ cls: 'approximated',
415
+ note: '.125rem — sub-rung; nearest meaning',
416
+ },
417
+ },
418
+ drop: { 'dropdown-min-width': { cls: 'unsupported', note: N_BESPOKE } },
419
+ },
420
+ modal: {
421
+ emit: {
422
+ 'modal-footer-margin-between': { sem: 'space.2' },
423
+ 'modal-dialog-margin': { sem: 'space.2' },
424
+ 'modal-dialog-margin-y-sm-up': {
425
+ sem: 'space.6',
426
+ cls: 'approximated',
427
+ note: '1.75rem — between rungs; nearest meaning',
428
+ },
429
+ 'modal-transition': { trans: { duration: 'duration.normal', easing: 'easing.enter' } },
430
+ // Overlay pass (proposal 0003): Bootstrap splits the veil into a colour
431
+ // and an opacity; the IR's `scrim` already carries BOTH (its value has an
432
+ // alpha channel), so this needs no new vocabulary — just the two halves
433
+ // read out separately. `$offcanvas-backdrop-*` chains from these, so the
434
+ // offcanvas veil follows for free. Was `unsupported` on the claim that
435
+ // "scrim covers the color, not the alpha" — that claim was wrong.
436
+ 'modal-backdrop-bg': { sem: 'color.scrim', part: 'opaque' },
437
+ 'modal-backdrop-opacity': { sem: 'color.scrim', part: 'alpha' },
438
+ },
439
+ drop: {
440
+ 'modal-sm': { cls: 'unsupported', note: N_BESPOKE + ' (dialog width steps)' },
441
+ 'modal-md': { cls: 'unsupported', note: N_BESPOKE + ' (dialog width steps)' },
442
+ 'modal-lg': { cls: 'unsupported', note: N_BESPOKE + ' (dialog width steps)' },
443
+ 'modal-xl': { cls: 'unsupported', note: N_BESPOKE + ' (dialog width steps)' },
444
+ 'modal-fade-transform': { cls: 'dropped', note: N_STRUCT },
445
+ 'modal-show-transform': { cls: 'dropped', note: N_STRUCT },
446
+ 'modal-scale-transform': { cls: 'dropped', note: N_STRUCT },
447
+ },
448
+ },
449
+ offcanvas: {
450
+ emit: {
451
+ 'offcanvas-transition-duration': {
452
+ sem: 'duration.normal',
453
+ cls: 'approximated',
454
+ note: '.3s vs 250ms — nearest rung',
455
+ },
456
+ },
457
+ drop: {
458
+ 'offcanvas-horizontal-width': { cls: 'unsupported', note: N_BESPOKE + ' (panel width)' },
459
+ 'offcanvas-vertical-height': { cls: 'unsupported', note: N_BESPOKE + ' (panel height)' },
460
+ },
461
+ },
462
+ tooltip: {
463
+ emit: {
464
+ // Promoted by proposal 0004 on the strongest two-target evidence yet:
465
+ // PrimeNG constrains the same element the same way at the same measure
466
+ // (`tooltip.root.maxWidth: 12.5rem` — exactly Bootstrap's 200px), and it
467
+ // is one of only two `maxWidth` slots in its whole 2759-slot surface.
468
+ // The catalog slot has no default, so this only emits when authored;
469
+ // otherwise Bootstrap's own 200px stands (resolveEmits' absent-source
470
+ // path, AL5).
471
+ 'tooltip-max-width': { comp: 'tooltip.max-width' },
472
+ },
473
+ drop: {
474
+ 'tooltip-opacity': { cls: 'unsupported', note: N_OPACITY },
475
+ 'tooltip-arrow-width': { cls: 'dropped', note: N_STRUCT + ' (arrow geometry)' },
476
+ 'tooltip-arrow-height': { cls: 'dropped', note: N_STRUCT + ' (arrow geometry)' },
477
+ },
478
+ },
479
+ popover: {
480
+ emit: { 'popover-header-padding-y': { sem: 'space.2' } },
481
+ drop: {
482
+ 'popover-max-width': { cls: 'unsupported', note: N_BESPOKE },
483
+ 'popover-arrow-width': { cls: 'dropped', note: N_STRUCT + ' (arrow geometry)' },
484
+ 'popover-arrow-height': { cls: 'dropped', note: N_STRUCT + ' (arrow geometry)' },
485
+ },
486
+ },
487
+ toast: {
488
+ emit: {
489
+ 'toast-padding-x': { sem: 'space.3' },
490
+ 'toast-padding-y': { sem: 'space.2' },
491
+ 'toast-font-size': {
492
+ sem: 'type.size.sm',
493
+ cls: 'approximated',
494
+ note: '.875rem vs the 0.8rem scale rung — nearest meaning',
495
+ },
496
+ },
497
+ drop: { 'toast-max-width': { cls: 'unsupported', note: N_BESPOKE } },
498
+ },
499
+
500
+ // ---- feedback / status -------------------------------------------------------
501
+ alert: {
502
+ emit: { 'alert-margin-bottom': { sem: 'space.4' } },
503
+ drop: {
504
+ 'alert-bg-scale': { cls: 'dropped', note: N_PARAM },
505
+ 'alert-border-scale': { cls: 'dropped', note: N_PARAM },
506
+ 'alert-color-scale': { cls: 'dropped', note: N_PARAM },
507
+ },
508
+ },
509
+ 'list-group': {
510
+ drop: {
511
+ 'list-group-item-bg-scale': { cls: 'dropped', note: N_PARAM },
512
+ 'list-group-item-color-scale': { cls: 'dropped', note: N_PARAM },
513
+ },
514
+ },
515
+ badge: {
516
+ emit: {
517
+ 'badge-font-size': { sem: 'type.size.xs', cls: 'approximated', note: N_EM },
518
+ 'badge-padding-y': { sem: 'space.1', cls: 'approximated', note: N_EM },
519
+ 'badge-padding-x': { sem: 'space.2', cls: 'approximated', note: N_EM },
520
+ },
521
+ },
522
+ progress: {
523
+ emit: {
524
+ 'progress-height': { sem: 'space.4' },
525
+ 'progress-bar-transition': {
526
+ trans: { duration: 'duration.slower', easing: 'easing.standard' },
527
+ },
528
+ },
529
+ drop: {
530
+ 'progress-bar-animation-timing': {
531
+ cls: 'dropped',
532
+ note: N_STRUCT + ' (stripe animation mechanics)',
533
+ },
534
+ },
535
+ },
536
+ spinner: {
537
+ emit: {
538
+ 'spinner-animation-speed': {
539
+ sem: 'duration.slower',
540
+ cls: 'approximated',
541
+ note: '.75s rotation period vs 600ms — nearest rung',
542
+ },
543
+ },
544
+ drop: {
545
+ 'spinner-width': { cls: 'unsupported', note: N_BESPOKE },
546
+ 'spinner-width-sm': { cls: 'unsupported', note: N_BESPOKE },
547
+ 'spinner-border-width': {
548
+ cls: 'unsupported',
549
+ note: N_BESPOKE + ' (em-relative ring thickness)',
550
+ },
551
+ 'spinner-border-width-sm': {
552
+ cls: 'unsupported',
553
+ note: N_BESPOKE + ' (em-relative ring thickness)',
554
+ },
555
+ 'spinner-vertical-align': { cls: 'dropped', note: N_STRUCT },
556
+ },
557
+ },
558
+ carousel: {
559
+ emit: {
560
+ 'carousel-caption-padding-y': { sem: 'space.5' },
561
+ 'carousel-caption-spacer': { sem: 'space.5' },
562
+ 'carousel-control-transition': T_FAST,
563
+ 'carousel-indicator-transition': {
564
+ trans: { duration: 'duration.slower', easing: 'easing.standard' },
565
+ },
566
+ 'carousel-transition-duration': { sem: 'duration.slower' },
567
+ },
568
+ drop: {
569
+ 'carousel-control-width': { cls: 'dropped', note: N_STRUCT },
570
+ 'carousel-control-opacity': { cls: 'unsupported', note: N_OPACITY },
571
+ 'carousel-control-hover-opacity': { cls: 'unsupported', note: N_OPACITY },
572
+ 'carousel-indicator-width': { cls: 'unsupported', note: N_ARCH_DISAGREE + ' (PrimeNG has indicator width+height too, but 28x8 rounded vs Bootstrap 30x3 hairline — one value would give two differently-wrong carousels)' },
573
+ 'carousel-indicator-height': {
574
+ cls: 'unsupported',
575
+ note: N_ARCH_DISAGREE + ' (indicator, height axis)',
576
+ },
577
+ 'carousel-indicator-hit-area-height': {
578
+ cls: 'dropped',
579
+ note: N_STRUCT + ' (touch target mechanics)',
580
+ },
581
+ 'carousel-indicator-spacer': {
582
+ cls: 'unsupported',
583
+ note: N_BESPOKE + ' (gap between indicators — PrimeNG has no counterpart at all)',
584
+ },
585
+ 'carousel-indicator-opacity': { cls: 'unsupported', note: N_OPACITY },
586
+ 'carousel-indicator-active-opacity': { cls: 'unsupported', note: N_OPACITY },
587
+ 'carousel-caption-width': { cls: 'dropped', note: N_STRUCT },
588
+ 'carousel-control-icon-width': { cls: 'unsupported', note: N_ICON + ' (prev/next control)' },
589
+ 'carousel-control-prev-icon-bg': { cls: 'unsupported', note: N_ASSET },
590
+ 'carousel-control-next-icon-bg': { cls: 'unsupported', note: N_ASSET },
591
+ 'carousel-dark-control-icon-filter': { cls: 'dropped', note: N_FILTER },
592
+ },
593
+ },
594
+ };
595
+
596
+ /** Bootstrap's cascade no-op markers — mechanically classified. */
597
+ const INHERIT_MARKERS = new Set(['null', 'inherit', 'transparent', 'currentcolor']);
598
+
599
+ /**
600
+ * The Bootstrap counterpart of PrimeNG's `inherited` reason (AL3,
601
+ * surface-coverage.js): a slot left at a cascade no-op whose EFFECTIVE value
602
+ * still comes from something this exporter drives. PrimeNG counts those as real
603
+ * coverage (`derived`) because that is how PrimeNG is designed to be themed;
604
+ * Bootstrap's `null` markers on inherited CSS properties work identically, and
605
+ * classifying them as `dropped` under-reported the same mechanism on the other
606
+ * target. Splitting them is a correction, not an inflation.
607
+ *
608
+ * Membership is per-name and deliberately conservative — the split is a real
609
+ * judgment (does this property inherit at all, and is the ancestor value one we
610
+ * drive?), so it is not inferred from the marker. Anything not listed here keeps
611
+ * falling through to `inherit-default` (dropped), which is the safe direction:
612
+ * a new upstream `null` variable is never silently claimed as covered.
613
+ *
614
+ * The two rejected shapes, for the record:
615
+ * - non-inherited properties (box-shadow, background, border-radius, height,
616
+ * margin, transition, filter): `null` means "no declaration", so nothing
617
+ * reaches them — those stay dropped.
618
+ * - inherited-but-structural (white-space, cursor): the value that reaches
619
+ * them is real, but it is not a theme value.
620
+ */
621
+ const INHERITS_DRIVEN = {
622
+ 'hr-color': 'the body text color (driven) — the divider is currentColor at $hr-opacity',
623
+ 'hr-border-color': 'currentColor via $hr-color, so it follows the driven body color per mode',
624
+ 'input-btn-font-family': 'the driven body font stack',
625
+ 'form-text-font-style': 'the driven body type',
626
+ 'form-text-font-weight': 'the driven body type',
627
+ 'form-label-font-style': 'the driven body type',
628
+ 'form-label-color': 'the driven body text color',
629
+ 'input-disabled-color': 'the driven $input-color (Bootstrap keeps the enabled color when disabled)',
630
+ 'form-check-label-color': 'the driven body text color',
631
+ 'form-select-disabled-color': 'the driven $form-select-color',
632
+ 'nav-link-font-size': 'the driven body type',
633
+ 'nav-link-font-weight': 'the driven body type',
634
+ 'card-title-color': 'the driven card/body text color',
635
+ 'card-subtitle-color': 'the driven card/body text color',
636
+ 'card-cap-color': 'the driven card/body text color',
637
+ 'card-color': 'the driven body text color',
638
+ 'toast-color': 'the driven body text color',
639
+ 'pre-color': 'the driven body text color',
640
+ 'breadcrumb-font-size': 'the driven body type',
641
+ 'form-feedback-tooltip-line-height': 'the driven body line height',
642
+ 'tooltip-arrow-color': 'the driven --bs-tooltip-bg (Bootstrap points the arrow at the bubble color)',
643
+ 'table-group-separator-color': 'currentColor — the driven table text color',
644
+ };
645
+
646
+ /**
647
+ * Classify one inventory variable (scope === 'component').
648
+ * Returns { emit } | { drop } | { mech: 'chained'|'follows-global'|'inherit-default' } | null.
649
+ * null = unclassified — check-bootstrap-surface.mjs fails the build on it.
650
+ */
651
+ export function coverageForVariable(v) {
652
+ const d = DESCRIPTORS[v.family] ?? {};
653
+ const emit = d.emit?.[v.name];
654
+ const drop = d.drop?.[v.name];
655
+ if (emit && drop) return { emit, drop };
656
+ if (emit) return { emit };
657
+ if (drop) return { drop };
658
+ if (v.refs.length > 0) return { mech: 'chained' };
659
+ if (v.aliasesGlobalCssVar) return { mech: 'follows-global' };
660
+ if (INHERIT_MARKERS.has(v.value)) {
661
+ const from = INHERITS_DRIVEN[v.name];
662
+ return from ? { mech: 'inherits-driven', from } : { mech: 'inherit-default' };
663
+ }
664
+ return null;
665
+ }