@brightspot/ui 5.0.3-css-bloat.1 → 5.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/custom-elements.json +1203 -1203
- package/dist/storybook/assets/{ActionBar.stories-mJT7v57p.js → ActionBar.stories-cyX9vc6C.js} +1 -1
- package/dist/storybook/assets/{ActionItem.stories-BlFf4xEA.js → ActionItem.stories-Bs-Kxp5J.js} +1 -1
- package/dist/storybook/assets/{Avatar.stories--_L9Vbdp.js → Avatar.stories-B1Uee53f.js} +1 -1
- package/dist/storybook/assets/{AvatarGroup.stories-C51JkHGo.js → AvatarGroup.stories-W2EtKQBu.js} +1 -1
- package/dist/storybook/assets/{Badge.stories-Cb7xiaXg.js → Badge.stories-BotNIO18.js} +1 -1
- package/dist/storybook/assets/{Button-CUSnc1o5.js → Button-YTBnP55L.js} +1 -1
- package/dist/storybook/assets/{Button.stories-DYN1XkuS.js → Button.stories-B-X7_d_i.js} +1 -1
- package/dist/storybook/assets/{ButtonGroup.stories-DG8aRtzR.js → ButtonGroup.stories-BM-pxfK2.js} +1 -1
- package/dist/storybook/assets/{Celebrate.stories-DTEESbu8.js → Celebrate.stories-D9EJwzxo.js} +1 -1
- package/dist/storybook/assets/{Checkbox.stories-C9YH29GV.js → Checkbox.stories-f5VLVSw5.js} +1 -1
- package/dist/storybook/assets/{CircularProgress.stories-DdYXxjoB.js → CircularProgress.stories-BI9e372u.js} +1 -1
- package/dist/storybook/assets/{ClipboardMixin.stories-CmZELLpR.js → ClipboardMixin.stories-CsyJDNxc.js} +1 -1
- package/dist/storybook/assets/{Color-6BZIO3FS-o42Z8gTi.js → Color-6BZIO3FS-ClVOLIJG.js} +1 -1
- package/dist/storybook/assets/{Colors.stories-DjuerTf9.js → Colors.stories-hUYBvymM.js} +1 -1
- package/dist/storybook/assets/{CombinedEffects.stories-C_AMq9k4.js → CombinedEffects.stories-DkokyKCS.js} +1 -1
- package/dist/storybook/assets/{ComponentStatesMixin-kk-5wcvM.js → ComponentStatesMixin-C4I_rtgt.js} +1 -1
- package/dist/storybook/assets/{ComponentStatesMixin.stories-cy7-TcbF.js → ComponentStatesMixin.stories-BeLCYevK.js} +1 -1
- package/dist/storybook/assets/{CopyToClipboard.stories-nM41pfEH.js → CopyToClipboard.stories-DN9oagz-.js} +1 -1
- package/dist/storybook/assets/{Debounce.stories-B8jBQli0.js → Debounce.stories-CtNQAJxO.js} +1 -1
- package/dist/storybook/assets/{DocsRenderer-LL677BLK-wNqfOPga.js → DocsRenderer-LL677BLK-Bx1Fds2q.js} +3 -3
- package/dist/storybook/assets/{Dropdown.stories-D_TlsBqs.js → Dropdown.stories-B862-mco.js} +1 -1
- package/dist/storybook/assets/{EmptyState.stories-BI98FUIg.js → EmptyState.stories-Im3Vr4ZL.js} +1 -1
- package/dist/storybook/assets/{Events.stories-BoH9pXaI.js → Events.stories-B0tluV0t.js} +1 -1
- package/dist/storybook/assets/{Heading.stories-BmjIILOl.js → Heading.stories-6CzGqAAc.js} +1 -1
- package/dist/storybook/assets/{HueRipple.stories-DOeusr2J.js → HueRipple.stories-DaQiDn9K.js} +1 -1
- package/dist/storybook/assets/{Icon.stories-DUSkJwNE.js → Icon.stories-CFkYO_7w.js} +1 -1
- package/dist/storybook/assets/{IconButton.stories-Bj5mC5rr.js → IconButton.stories-DwBTqvTi.js} +1 -1
- package/dist/storybook/assets/{LinearProgress.stories-BtVWYb-l.js → LinearProgress.stories-Coxmgjmo.js} +1 -1
- package/dist/storybook/assets/{Pagination.stories-tspgl3pY.js → Pagination.stories-CYrKX5iI.js} +1 -1
- package/dist/storybook/assets/{Popover.stories-CBZaxHo7.js → Popover.stories-BkGiUOfu.js} +1 -1
- package/dist/storybook/assets/{ReadyMixin-DH37_O0o.js → ReadyMixin-CP6tQ4FB.js} +1 -1
- package/dist/storybook/assets/{RovingTabindexMixin.stories-C1QArA31.js → RovingTabindexMixin.stories-CzkPw8Nl.js} +1 -1
- package/dist/storybook/assets/{Rtc.stories-zWf-zb6K.js → Rtc.stories-CVch488H.js} +1 -1
- package/dist/storybook/assets/{ScrollShadow.stories-CtVTMAY-.js → ScrollShadow.stories-BGh-Irt7.js} +1 -1
- package/dist/storybook/assets/{Switch.stories-BGFvlrF1.js → Switch.stories-DPfP0QVK.js} +1 -1
- package/dist/storybook/assets/{Tab.stories-fFHdA-0z.js → Tab.stories-CBcuRcDB.js} +1 -1
- package/dist/storybook/assets/{Tabs.stories-DUxUf-8D.js → Tabs.stories-CDOBjYbs.js} +1 -1
- package/dist/storybook/assets/{Throttle.stories-BUBzkB-y.js → Throttle.stories-Bqyul0aW.js} +1 -1
- package/dist/storybook/assets/{Tooltip.stories-BRZ9x5GZ.js → Tooltip.stories-B9dohX1h.js} +1 -1
- package/dist/storybook/assets/{Upload.stories-9PNQJTXv.js → Upload.stories-C7dq2Wdk.js} +1 -1
- package/dist/storybook/assets/{UploadItem.stories-Dg1Osr5_.js → UploadItem.stories-35zsIKTv.js} +1 -1
- package/dist/storybook/assets/{Welcome.stories-BbidbZpj.js → Welcome.stories-BuD3fpke.js} +1 -1
- package/dist/storybook/assets/{Widget.stories-B5MKOa3i.js → Widget.stories-D2UYzfyE.js} +1 -1
- package/dist/storybook/assets/{WithTooltip-65CFNBJE-DEoC8S9g.js → WithTooltip-65CFNBJE-DGiY8cz9.js} +1 -1
- package/dist/storybook/assets/{blocks-BzEtLpfg.js → blocks-YjKl5E55.js} +5 -5
- package/dist/storybook/assets/{formatter-EIJCOSYU-Dpapfc1g.js → formatter-EIJCOSYU-XYAiuXAN.js} +1 -1
- package/dist/storybook/assets/if-defined-f_e-RnGa.js +1 -0
- package/dist/storybook/assets/{iframe-Bi6noyvR.js → iframe-CufEXQ5F.js} +4 -4
- package/dist/storybook/assets/{index-B6rDQTSl.js → index-DbEDIsEB.js} +1 -1
- package/dist/storybook/assets/{onFind-eyMIAO26.js → onFind-C7Wi8jr6.js} +1 -1
- package/dist/storybook/assets/{onFind.stories-CPJuRapJ.js → onFind.stories-UpwJxFqR.js} +1 -1
- package/dist/storybook/assets/{onRemove.stories-Dbwmibgi.js → onRemove.stories-BMwQGBCl.js} +1 -1
- package/dist/storybook/assets/{onVisible.stories-C_V2xOaA.js → onVisible.stories-axSo0Zv3.js} +1 -1
- package/dist/storybook/assets/{style-map-D3mT-6Qa.js → style-map-BkaK9546.js} +1 -1
- package/dist/storybook/assets/{syntaxhighlighter-ED5Y7EFY-BJK43TGw.js → syntaxhighlighter-ED5Y7EFY-CDDZTVRn.js} +1 -1
- package/dist/storybook/iframe.html +1 -1
- package/dist/storybook/project.json +1 -1
- package/package.json +1 -1
- package/src/legacy/tool-ui/src/ContentInputGroup.css +9 -4
- package/src/legacy/tool-ui/src/ContentSummary.css +2 -0
- package/src/legacy/tool-ui/src/Conversation.ts +54 -10
- package/src/legacy/tool-ui/src/Toggleable/index.ts +45 -29
- package/src/legacy/tool-ui/src/Widget.css +1 -2
- package/src/legacy/tool-ui/src/main/webapp/dist/{6216.cf8adc1990ee1111f065.js → 6216.2d532054fcb8e634f96b.js} +1 -1
- package/src/legacy/tool-ui/src/main/webapp/dist/{RTEProseMirror.e8521581e28e90ef6f30.js → RTEProseMirror.945bd28778b1a3e937c7.js} +2 -2
- package/src/legacy/tool-ui/src/main/webapp/dist/{v4.ab96a7dd75fa7af9c970.js → v4.c5bcef50efdfa2d2e35f.js} +4 -4
- package/src/legacy/tool-ui/src/main/webapp/dist/{v5.e48c1d8f41b5e088bef0.js → v5.86effb9bf858ae7b0640.js} +4 -4
- package/src/legacy/tool-ui/src/main/webapp/dist/v5.f60c05ef0c9b89aae888.css +5 -0
- package/src/legacy/tool-ui/src/main/webapp/v4/rte/plugins/table_manager/views/ProseMirror-table.less +28 -1
- package/dist/storybook/assets/if-defined-CzvBkd64.js +0 -1
- package/docs/adr/0001-monolith-css-delivery.md +0 -20
- package/docs/adr/0002-complete-component-preset.md +0 -26
- package/docs/adr/0003-plugin-selector-isolation.md +0 -24
- package/docs/adr/0004-dynamic-icon-names-route-through-runtime-injectors.md +0 -50
- package/docs/adr/0005-rtl-conditional-overlay.md +0 -44
- package/docs/adr/0006-editor-has-runtime-perf.md +0 -33
- package/src/legacy/tool-ui/src/main/webapp/dist/v5.58cad1e6193f2106d271.css +0 -5
- /package/src/legacy/tool-ui/src/main/webapp/dist/{RTEProseMirror.e8521581e28e90ef6f30.js.LICENSE.txt → RTEProseMirror.945bd28778b1a3e937c7.js.LICENSE.txt} +0 -0
- /package/src/legacy/tool-ui/src/main/webapp/dist/{v4.ab96a7dd75fa7af9c970.js.LICENSE.txt → v4.c5bcef50efdfa2d2e35f.js.LICENSE.txt} +0 -0
- /package/src/legacy/tool-ui/src/main/webapp/dist/{v5.e48c1d8f41b5e088bef0.js.LICENSE.txt → v5.86effb9bf858ae7b0640.js.LICENSE.txt} +0 -0
package/src/legacy/tool-ui/src/main/webapp/v4/rte/plugins/table_manager/views/ProseMirror-table.less
CHANGED
|
@@ -126,12 +126,39 @@
|
|
|
126
126
|
}
|
|
127
127
|
|
|
128
128
|
.ProsemirrorEnhancementMenu-container-button:is(
|
|
129
|
-
.edit,
|
|
130
129
|
.arrow_upward,
|
|
131
130
|
.arrow_downward,
|
|
132
131
|
.open_with
|
|
133
132
|
) {
|
|
134
133
|
display: none;
|
|
135
134
|
}
|
|
135
|
+
|
|
136
|
+
.Enhancement.readOnly {
|
|
137
|
+
align-items: center;
|
|
138
|
+
display: flex;
|
|
139
|
+
flex-wrap: wrap;
|
|
140
|
+
overflow: hidden;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
.Enhancement-label {
|
|
144
|
+
min-width: 0;
|
|
145
|
+
overflow: hidden;
|
|
146
|
+
text-overflow: ellipsis;
|
|
147
|
+
white-space: nowrap;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
.ProsemirrorEnhancementMenu {
|
|
151
|
+
margin-left: auto;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
.ProsemirrorEnhancementMenu-container.is-block {
|
|
155
|
+
position: static;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
.Enhancement-preview,
|
|
159
|
+
.Enhancement-initialBody {
|
|
160
|
+
flex: 1 1 100%;
|
|
161
|
+
min-width: 0;
|
|
162
|
+
}
|
|
136
163
|
}
|
|
137
164
|
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{E as r}from"./iframe-Bi6noyvR.js";const m=o=>o??r;export{m as o};
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
status: accepted
|
|
3
|
-
---
|
|
4
|
-
|
|
5
|
-
# Monolith CSS delivery; reject per-component CSS
|
|
6
|
-
|
|
7
|
-
Every consumer of `@brightspot/ui` renders **inside a CMS page** where the host-built Monolith (`v5.<hash>.css`) is already present, and the library's components are Light-DOM class-appliers that ship no CSS of their own. We therefore keep the **host-built Monolith as the single CSS delivery vehicle** for all consumers: a consumer "picks and chooses" purely by importing the component **JS** it needs, and the corresponding `btu-*` CSS is materialized once, centrally, in the Monolith. The work to make this correct is (1) **de-bloat** the Monolith by decoupling cross-referencing plugins (Option A) and (2) **complete** it by registering the plugins consumers use (e.g. tooltip/popover) plus a safelist for runtime-written classes.
|
|
8
|
-
|
|
9
|
-
## Considered options
|
|
10
|
-
|
|
11
|
-
- **Direction 1 — Precompiled per-component CSS (self-contained components).** The library compiles a static `.css` per component and ships it next to the JS, so consumers bundle exactly what they import. **Rejected:** every consumer is in-CMS, so the Monolith is already on the page — bundling component CSS only **duplicates** what's already served (confirmed: platform-automation ships ~0 `btu-*` CSS today and relies on the Monolith).
|
|
12
|
-
- **Direction 2 — Per-consumer Tailwind (JIT).** Add a Tailwind/PostCSS pipeline to `@brightspot/ui-builder` so each consumer compiles the classes it uses. **Rejected:** forces every consumer to adopt Tailwind (abandoning the deliberate vanilla-Vite simplicity), re-ships overlapping `btu-*` + utility CSS per consumer, and reintroduces cross-multiplication per-consumer.
|
|
13
|
-
- **Direction 3 — Keep and fix the Monolith.** **Chosen.**
|
|
14
|
-
|
|
15
|
-
## Consequences
|
|
16
|
-
|
|
17
|
-
- Components intentionally ship **no** styling; they are inert without a Monolith on the page. "Pick-and-choose" is a JS-import concern, not a CSS one.
|
|
18
|
-
- **Appearance theming** stays runtime-overridable via `--btu-theme-*` custom properties; **structural theming** (scales, new color families, variants) remains a compile-time concern owned centrally, not by consumers.
|
|
19
|
-
- A consumer that needs a class the Monolith doesn't contain (today: tooltip/popover) **cannot** fix it locally — the fix must land where the Monolith is built (host CMS config and/or the published preset). This is the root cause of the tooltip-styles-missing defect.
|
|
20
|
-
- If a true **standalone** (non-CMS) consumer ever appears, this decision must be revisited — Direction 1 would come back on the table for that consumer class.
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
status: accepted
|
|
3
|
-
---
|
|
4
|
-
|
|
5
|
-
# Published preset registers the complete component set
|
|
6
|
-
|
|
7
|
-
The published `@brightspot/ui` preset (`src/tailwind.config.ts`) registers the **full** component-plugin set plus an explicit `safelist` for runtime-written classes (tooltip/popover position/offset/no-arrow), rather than the minimal `pagination` + `empty-state` list it carried historically. Consumers — including the host CMS — extend the preset and get every component's CSS without maintaining their own per-plugin registration.
|
|
8
|
-
|
|
9
|
-
### Cost (measured)
|
|
10
|
-
|
|
11
|
-
**Host impact (the only number that matters for production):** switching the host CMS from its current 10-plugin list to the complete preset adds **≈ +16.9 KB** to the v5.css component layer (≈ +25–34 KB after postcss-rtl). tooltip+popover specifically account for **≈ +7.9 KB** of that, closing the headline tooltip-missing defect. The host's v5.css already ships the heaviest plugins (`button`, `badge`, `icon`, `heading`, `loader`, `scroll-shadow`, `contrast`, `ring-contrast`, `theme`, `container-queries`) — Phase 3 only newly adds the lighter components (action-bar, avatar, button-group, checkbox, dropdown, popover, switch, tabs, tooltip, upload).
|
|
12
|
-
|
|
13
|
-
---
|
|
14
|
-
|
|
15
|
-
*Footnote — preset-intrinsic cost:* the standalone preset's component layer grows from ~2.7 KB (bare 2-plugin) to ~1.36 MB (complete). This is **not** the host impact (no production consumer compiles the bare preset — in-CMS consumers rely on the host Monolith per [ADR 0001](./0001-monolith-css-delivery.md)); it's an intrinsic-cost figure. The preset's broad color/size `safelist` patterns force-emit every *registered* component's runtime-applied variants (e.g. `btu-button-primary`, `btu-badge-lg`), so unused components do **not** purge to zero. The original rationale on this page claimed "addComponents is content-purged, so a complete preset costs ~0 for unused components" — that is incorrect when safelist patterns match the component's variants (which they do for most components). The reason completion is cheap *for the host* is that the host already registers the heavy plugins, not purging.
|
|
16
|
-
|
|
17
|
-
## Why
|
|
18
|
-
|
|
19
|
-
- Consumers previously hand-maintained their own `plugins[]` list (the host registered ~10 itself), which drifted out of sync. The omission of `tooltip`/`popover` there is exactly why those classes were missing from the Monolith — the tooltip-styles-missing defect. A complete preset removes the per-consumer registration step and the whole class of "forgot to register X" bugs.
|
|
20
|
-
- Runtime-written classes (e.g. `TooltipMixin`'s POSITION_CLASSES/OFFSET_CLASSES) are invisible to content scanning, so they must be safelisted in the preset itself — consumers can't be expected to rediscover them.
|
|
21
|
-
|
|
22
|
-
## Consequences
|
|
23
|
-
|
|
24
|
-
- The host `cms/tool-ui` config can drop its manual `plugins[]` list and rely on `presets: [require('@brightspot/ui')]`. Before removing it, preset parity must cover everything the host registered (`theme`, `contrast`, `ring-contrast`, `@tailwindcss/container-queries`).
|
|
25
|
-
- **Gated on [ADR 0003](./0003-plugin-selector-isolation.md).** Registering cross-referencing plugins (button-group, pagination, dropdown, upload) into the shipped preset is only safe once their foreign `.btu-*` selectors are decoupled — otherwise the `@apply` cross-multiplication detonates (≈ +2.75 MB for button-group alone). Sequence: **isolate first, then complete the preset.**
|
|
26
|
-
- See [ADR 0001](./0001-monolith-css-delivery.md): the preset feeds the host-built Monolith, which is the single CSS delivery vehicle for all (in-CMS) consumers.
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
status: accepted
|
|
3
|
-
---
|
|
4
|
-
|
|
5
|
-
# Plugin selectors must not reference foreign component classes
|
|
6
|
-
|
|
7
|
-
A `tailwind-plugin-<name>.ts` may only use `.btu-*` class tokens from **its own namespace** in its selectors. Referencing another component's `.btu-*` class — bare, in a combinator, or wrapped in `:where()`/`:is()` — is forbidden. To affect another component, use (a) the inner element / custom-element **tag** selector, (b) a CSS custom property the other component consumes, or (c) the component's own scoped class.
|
|
8
|
-
|
|
9
|
-
## Why
|
|
10
|
-
|
|
11
|
-
Tailwind's `@apply` resolver clones every rule whose selector contains a class token, once per `@apply <that-class>` site in the compiled CSS. So `.btu-pagination-controls > btu-icon-button > .btu-button { … }` is duplicated once per `@apply btu-button` site — ~178 in the host build — then doubled by postcss-rtl and fanned by variants → ≈ 917 KB of cross-multiplied rules for pagination's 7 selectors alone. The token's presence in the selector is the sole trigger; `:where()`/`:is()` do **not** help (verified — the walker matches the token regardless of wrapping).
|
|
12
|
-
|
|
13
|
-
## We enforce; we do not auto-fix
|
|
14
|
-
|
|
15
|
-
There is no safe library-side "magic" that neutralizes a stencil: the clone fires in the **host's** Tailwind pass (not the library's), the only internal skip-flag (`raws.tailwind.layer`) also removes the class from the apply cache and so breaks the legitimate `@apply btu-button` reuse path, and a codemod can't infer which element a foreign selector intended to target. So the rule is enforced, not corrected:
|
|
16
|
-
|
|
17
|
-
- **`no-foreign-btu-class`** grep rule in `scripts/check-conventions.mjs` (pre-commit + CI) — flags any foreign `.btu-*` token in a plugin selector. Ownership is resolved by longest-matching plugin name (`btu-button` → `button`, `btu-button-group` → `button-group`).
|
|
18
|
-
- **Flat-slope regression test** — compiles the preset against N = 0/10/100 `@apply btu-button` sites and fails if output is not independent of N. Catches reintroductions the grep can't see (e.g. selectors built from variables).
|
|
19
|
-
|
|
20
|
-
## Considered and rejected
|
|
21
|
-
|
|
22
|
-
`:where()`/`:is()` wrapping (token still clones), static CSS (re-multiplies unless injected *after* Tailwind — no host slot exists in the "ship raw CSS for the consumer's pipeline" model), `@scope` (Baseline Dec-2025 floor; the donut form still needs the element selector to reach the button), inverted ownership (selector still carries the token), Shadow DOM / `::part` (contradicts [ADR 0001](./0001-monolith-css-delivery.md)), patching `@apply` behavior (wrong repo; breaks the reuse API).
|
|
23
|
-
|
|
24
|
-
This rule is the gate that makes [ADR 0002](./0002-complete-component-preset.md) safe and keeps Monolith growth linear (~3–6 KB per new component). Authoring guidance: `.ai/LESSONS-PLUGIN-ISOLATION.md`; complementary "compose, don't reinvent" rule: `.ai/LESSONS-BUTTON-REUSE.md` §3.
|
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
status: accepted
|
|
3
|
-
---
|
|
4
|
-
|
|
5
|
-
# Dynamic icon names route through runtime injectors, not per-name CSS
|
|
6
|
-
|
|
7
|
-
Every Brightspot path that renders a Lucide icon whose name is **chosen at runtime** (editor selections via `@ToolUi.IconName`, plugin code calling `createNavAreaHeader(..., "chart-pie")`, Material-to-Lucide compat lookups for `data-icon-button-name` elements, ProseMirror toolbar buttons, the `<btu-icon symbol="…">` web component itself) resolves the name in **JavaScript** and applies the SVG via **inline CSS custom properties** on the host element. None of these paths reads the Preset's per-name `.btu-icon-via-mask-NAME` rules. Therefore the Preset's **Safelist** must not force-emit those per-name rules — doing so ships ~1.18 MB of `v5.css` that the browser fetches, parses, and never consults.
|
|
8
|
-
|
|
9
|
-
## Why
|
|
10
|
-
|
|
11
|
-
The icon Component Plugin (`tailwind-plugin-icon.ts`) generates 1,792 per-name rules — one per Lucide icon — each inlining the entire SVG as a URL-encoded data URL. Until this decision they were force-emitted to v5.css by two redundant safelist patterns (`/btu-icon-via-mask-.+/` and `/^btu-icon(-.*)?$/`). Measured: 2,638 emitted rules (postcss-rtl doubles the directional ones), 1,182,531 bytes, **24.9% of v5.css**.
|
|
12
|
-
|
|
13
|
-
Every actual rendering path bypasses these rules:
|
|
14
|
-
|
|
15
|
-
| Dynamic path | Resolution mechanism | CSS slot the SVG lands in |
|
|
16
|
-
| --- | --- | --- |
|
|
17
|
-
| `<btu-icon symbol="X">` (Nav-rail, content widgets) | `Icon.ts` calls `getIcon(name)`, inlines the SVG as Light-DOM markup with a `<mask>` `<defs>`, sets `style="--mask-url: url(#X)"` | `--mask-url` (highest-priority slot in the base rule's `var()` chain) |
|
|
18
|
-
| `<button data-icon-button-name="X">` (Right-rail / IconButton) | `v5.ts:applyIconProperties` (an `onFind`-registered host injector) maps Material → Lucide, calls `getIcon(name)`, sets `style="--compat-icon-via-mask: url(…)"` | `--compat-icon-via-mask` (third slot in the base rule's `var()` chain) |
|
|
19
|
-
| ProseMirror toolbar (RTE-blacklisted) | `resolveIconCompat.js` fires a `btu-resolve-icon` custom event that re-enters `applyIconProperties` from outside the RTE blacklist | same as above |
|
|
20
|
-
| `@apply btu-icon-via-mask-NAME` in developer CSS | Tailwind's `@apply` resolver copies the rule body from the plugin's internal registry into the consumer's selector at build time | the consumer's own rule (per-name source rule is not required in output) |
|
|
21
|
-
| `<i class="btu-icon-via-mask-NAME">` in scanned source | Tailwind's content scanner emits the per-name source rule | the per-name source rule itself (~6 names total today) |
|
|
22
|
-
|
|
23
|
-
The base rule the runtime injectors target is unchanged:
|
|
24
|
-
|
|
25
|
-
```css
|
|
26
|
-
.btu-icon-via-mask {
|
|
27
|
-
mask: var(--mask-url, var(--icon-svg, var(--compat-icon-via-mask))) center / contain no-repeat;
|
|
28
|
-
…shared properties…
|
|
29
|
-
}
|
|
30
|
-
```
|
|
31
|
-
|
|
32
|
-
The base rule survives any safelist edit that excludes only `btu-icon-via-mask-X` (X non-empty). The runtime injectors continue to work because they consume the **base** rule via inline custom properties — they never depended on the per-name rules.
|
|
33
|
-
|
|
34
|
-
The remaining ~1,786 names exist in v5.css purely as safelist artifacts. They are dead bytes.
|
|
35
|
-
|
|
36
|
-
## Consequences
|
|
37
|
-
|
|
38
|
-
- The Preset's safelist explicitly excludes the per-name via-mask namespace. Concretely, no safelist pattern may match a class of the form `btu-icon-via-mask-X` for any X — enforced by a convention guard in `scripts/check-conventions.mjs` alongside the existing `no-foreign-btu-class` and `safelist-runtime-classes` rules.
|
|
39
|
-
- The Component Plugin remains unchanged. It continues to register all 1,792 per-name rules into Tailwind's internal registry so that **Cross-multiplication** for `@apply btu-icon-via-mask-NAME` in developer CSS keeps working (Tailwind copies the rule body into the consumer regardless of whether the source class survives output purging).
|
|
40
|
-
- The ~6 statically-discoverable raw-class names (`eye`, `eye-closed`, `file-text`, `sparkles`, `upload`, `zap` as of this writing) continue to emit per-name rules because Tailwind's content scanner finds them. New static-class names self-emit on first use; no maintenance step.
|
|
41
|
-
- A new raw-class name added by a downstream consumer in **non-scanned content** (e.g. a hand-built `<i class="btu-icon-via-mask-some-name">` injected at runtime by JS or by a template engine Tailwind doesn't scan) will **not** render its mask, because no rule will match. The supported alternatives are:
|
|
42
|
-
- Use `<btu-icon symbol="some-name">` (preferred — already runtime-resolved).
|
|
43
|
-
- Use the `data-icon` / `data-icon-name` / `data-icon-button-name` pattern (handled by `applyIconProperties`).
|
|
44
|
-
- Add the class to `additional-tailwind-classes.txt` so Tailwind scans it.
|
|
45
|
-
- A byte-budget regression test in `test/css-bloat/` asserts the aggregate size of per-name mask rules in v5.css stays under 20 KB — leaving headroom for the natural growth of statically-discoverable names while catching any reintroduction of force-emission.
|
|
46
|
-
- This ADR is the load-bearing premise for the Lever #1 safelist change in [`../../plans/lever-1-icon-emission-prd.md`](../../plans/lever-1-icon-emission-prd.md). It is reachable through any of the runtime-injector files (`src/legacy/tool-ui/src/v5.ts`, `src/legacy/tool-ui/src/main/webapp/v4/dom/resolveIconCompat.js`, `src/components/icon/Icon.ts`, `src/LucideDynamicLoader.ts`).
|
|
47
|
-
|
|
48
|
-
If a future requirement appears where dynamic icon names *must* render through the raw class form without scanning and without using `data-icon*` attributes, this ADR is the place to revisit. Two follow-up paths are sketched in the Lever #1 PRD's design discussion: (a) plugin refactor splitting shared behavior onto the base class so a library-side runtime scanner can complete the rule from inline style, and (b) per-element `<style>`-block injection. Neither is needed under the current consumer contract; both would be reopened only if the contract changed.
|
|
49
|
-
|
|
50
|
-
See also: [ADR 0001](./0001-monolith-css-delivery.md) (the Monolith is the single CSS delivery vehicle), [ADR 0002](./0002-complete-component-preset.md) (the Preset registers the complete component set; safelist patterns govern which variants force-emit), [ADR 0003](./0003-plugin-selector-isolation.md) (cross-referencing plugin selectors are forbidden).
|
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
status: accepted
|
|
3
|
-
---
|
|
4
|
-
|
|
5
|
-
# RTL ships as a conditional overlay, not doubled into the Monolith
|
|
6
|
-
|
|
7
|
-
Brightspot resolves text direction as a per-user setting server-side (`Localization.isCurrentUserRtl()`), emitted once into the `<html dir>` attribute. The Tool UI **Monolith** (`v5.css`), however, ships **both** directions to **every** user: `postcss-rtl` doubles each directional rule into `[dir="ltr"]` and `[dir="rtl"]` halves (plus bare-`[dir]` scoping), so ~25% of the post-Lever-#1 bundle is direction-prefixed and roughly half of that is dead weight on any given page. We therefore **split direction out of the Monolith**: all users load a direction-neutral base `v5.css`; RTL users *additionally* load a small `[dir="rtl"]`-only overlay `v5-rtl.css`, gated by the same `Localization.isCurrentUserRtl()` call that drives `<html dir>`. This **refines [ADR 0001](./0001-monolith-css-delivery.md)**: the Monolith stays the single styling vehicle, but materializes as a base + conditional overlay pair rather than one doubled bundle.
|
|
8
|
-
|
|
9
|
-
## How the split is produced
|
|
10
|
-
|
|
11
|
-
The base and overlay are a **clean partition of the same source CSS**, produced by one paren-aware direction-filter run with opposite predicates:
|
|
12
|
-
|
|
13
|
-
| Bundle | Pipeline | Keeps |
|
|
14
|
-
| --- | --- | --- |
|
|
15
|
-
| `v5.css` (base) | postcss **without** postcss-rtlcss, then **strip-`[dir=rtl]`** | every rule except `[dir="rtl"]`-scoped parts; physical properties stay at their LTR-default values, unscoped |
|
|
16
|
-
| `v5-rtl.css` (overlay) | postcss **with** `postcss-rtlcss` `mode: 'override'`, then **keep-only-`[dir=rtl]`** (which also prunes the at-rule wrappers it empties) | only `[dir="rtl"]`-scoped rules — the mirrored overrides, the authored `[dir="rtl"]` rules, and Tailwind `rtl:`-variant output |
|
|
17
|
-
|
|
18
|
-
`base ⊎ overlay` reconstructs correct rendering for both users with **no gap and no overlap**: an LTR user gets the base alone; an RTL user gets the base (LTR-default physical props) plus the overlay's `[dir="rtl"]` overrides, which win on specificity regardless of load order.
|
|
19
|
-
|
|
20
|
-
The two filters are **one module, two predicates**, sharing a top-level comma splitter that respects `(`/`[` nesting — so `.a, [dir=rtl] .b { … }` is partitioned, not mangled.
|
|
21
|
-
|
|
22
|
-
Each stage runs as an **isolated postcss pass** (postcss-rtlcss → filter → cssnano), not one chained pipeline: postcss-rtlcss must fully finish adding `[dir=rtl]` overrides before keep-rtl runs, and keep-rtl must finish before cssnano — otherwise cssnano's empty-discard races keep-rtl's rule removal and the emptied `@media`/`@container` wrappers survive (measured: ~46 KB of husks, ~32% of the overlay). The filter therefore prunes empty at-rules in its own `OnceExit` rather than relying on cssnano.
|
|
23
|
-
|
|
24
|
-
## Why postcss-rtlcss `override` + keep-rtl (verified empirically)
|
|
25
|
-
|
|
26
|
-
`postcss-rtlcss` v6 `mode: 'override'` was confirmed (2026-06-02, sandbox `/private/tmp/rtlcss-verify`) to emit **no** `[dir="ltr"]` halves and **no** bare-`[dir]` scaffolding — but it **keeps every unscoped original rule** alongside the `[dir="rtl"]` overrides. Override-alone is therefore not a lean overlay (it would carry a full copy of the base). The keep-rtl filter drops those originals, leaving the `[dir="rtl"]`-only overlay.
|
|
27
|
-
|
|
28
|
-
Override mode also emits the explicit cascade-resets the overlay needs (e.g. `[dir="rtl"] .x { border-left: none; border-right: … }`). This is what makes it beat the considered alternatives:
|
|
29
|
-
|
|
30
|
-
- **`mode: 'combined'`** — reproduces postcss-rtl's doubling (`[dir=ltr]` + `[dir=rtl]` halves). Rejected.
|
|
31
|
-
- **`mode: 'diff'`** — emits flipped declarations **unscoped** (relying on load order, not specificity) and **silently drops authored `[dir="rtl"]` rules**. Rejected.
|
|
32
|
-
- **"postcss-rtl extraction"** (filter postcss-rtl's doubled output down to its `[dir=rtl]` half) — the originally-feared fallback. Unnecessary, and *less correct*: postcss-rtl puts the cascade-resets in the `[dir=ltr]` half, so the extracted `[dir=rtl]` half would be missing them.
|
|
33
|
-
- **Parallel model** (RTL users load `v5-rtl.css` *instead of* `v5.css`) — two large bundles, lower edge-cache hit rate, no free graceful degradation. Rejected in favor of the overlay.
|
|
34
|
-
|
|
35
|
-
## Consequences
|
|
36
|
-
|
|
37
|
-
- A **CI overlay-integrity assertion** guards the partition every build: the base must contain zero `[dir="rtl"]` rules; the overlay must be non-empty and contain only `[dir="rtl"]` rules (zero unscoped / `[dir="ltr"]` / bare-`[dir]`). This catches a strip-rtl leak into the base, a silently no-op'd postcss-rtlcss (empty overlay), and a keep-rtl leak of originals into the overlay — each diagnostically. Stronger than a bare "byte counts differ" check.
|
|
38
|
-
- **Graceful degradation is free — and better than worst-case.** The base is built on CSS **logical properties**, which respond to the server-set `<html dir="rtl">` natively (no `[dir=rtl]` rules needed). So if a deployment is missing `v5-rtl.css`, an RTL user still gets **mostly-correct RTL** — only the physical-property elements (the overlay's job) stay unflipped — never a blank page or a fully-LTR layout. No runtime fallback logic. (Validated live 2026-06-02: at `dir=rtl` the base alone mirrored the bulk of the Dashboard and Content Edit pages; the overlay then corrected 16 and 46 physical-property elements respectively, including reversing the RTE toolbar.)
|
|
39
|
-
- **Measured impact (2026-06-02, same shared compile, decimal MB):** LTR base 2.63 MB — **−12.8%** vs the 3.02 MB doubled build (which cross-checks against the shipped container artifact ≈ 3.01 MB); RTL base+overlay 2.73 MB — **−9.7%**; overlay **95 KB** (beats the ~130–140 KB projection after empty-wrapper pruning). The overlay's size is effectively a **proxy for physical-property debt** in the legacy CSS: it shrinks toward zero as source migrates from physical (`padding-left`) to logical (`padding-inline-start`) properties, at which point native `dir` handling would leave the overlay nearly empty.
|
|
40
|
-
- The direction-filter is the **deep module** of Lever #2, golden-tested in both directions independent of the build orchestration.
|
|
41
|
-
- **`v4.css` is out of scope** and keeps `postcss-rtl` until v4 is separately retired (it also has hand-authored `[dir='rtl']` Less rules that need their own audit).
|
|
42
|
-
- The change surface is entirely Host-CMS-consumer-side (`build-css.mjs` + a conditional second `<link>` in `DariHtmlToolPageUtils`); the **Preset** is unchanged, so other `@brightspot/ui` consumers are insulated.
|
|
43
|
-
|
|
44
|
-
See also: [ADR 0001](./0001-monolith-css-delivery.md) (Monolith as single delivery vehicle — refined here), and the full Lever #2 design this records the decision for: [`../../plans/lever-2-rtl-overlay-prd.md`](../../plans/lever-2-rtl-overlay-prd.md).
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
status: accepted
|
|
3
|
-
---
|
|
4
|
-
|
|
5
|
-
# Editor-scoped `:has()` is the v5 runtime-perf hazard; reduce it
|
|
6
|
-
|
|
7
|
-
The v5 CMS UI feels frozen during editing while v4 is smooth, on the same page/DOM/machine. The cause is **`:has()` selector invalidation**: v5's stylesheet authors ~782 distinct `:has()` selectors (993 occurrences) vs v4's 41, and the browser re-tests `:has()` on every DOM mutation and every change to a class/attribute used as a `:has()` argument — which the editor JS does constantly. Each such interaction costs **~250 ms in v5 vs ~0–2 ms in v4**; expanding a Subscriptions item makes it ~16× worse by bringing the `:has()`-dense `ContentForm` subtree live. **Decision: treat the editor-subtree `:has()` as a runtime-perf hazard and reduce the ~160 rules scoped to `.CIG`/`.RCIG`/`.EIG`/`ContentForm`** (rewrite to `:focus-within`, JS/server-toggled classes, `:where()`/sibling selectors), preserving the structural `display:none` hides. Full evidence and rewrite recipe: [`plans/v5-css-has-runtime-perf-investigation.md`](../../plans/v5-css-has-runtime-perf-investigation.md).
|
|
8
|
-
|
|
9
|
-
## Considered options
|
|
10
|
-
|
|
11
|
-
All measured via live A/B (v5 `jpencola` vs v4 `joe`, same page), median forced-reflow cost per post-expand interaction:
|
|
12
|
-
|
|
13
|
-
- **Do nothing.** Stock v5 ≈ 507 ms/interaction (v4 ≈ 1 ms). **Rejected** — this is the reported "frozen" UX.
|
|
14
|
-
- **Rewrite only `:has(:focus)` / `:has(.is-*)` (the dynamic patterns, 149 rules).** 507 ms → ~375 ms (**1.4×**). **Rejected** — necessary mechanism but far short; cost is the aggregate `:has()` count in scope, not one category.
|
|
15
|
-
- **Remove all `:has()` (802 rules).** ~21 ms (**~25×**). **Rejected as the fix** — strips structural `display:none` hides and component styling system-wide; proves the lever but breaks layout/behavior.
|
|
16
|
-
- **Reduce the ~160 editor-scoped `:has()` (`.CIG`/`.RCIG`/`.EIG`/`ContentForm`).** ~22 ms (**~25×**, = full benefit) while leaving the other 768 untouched. **Chosen** — 16 % of the rules recover ~96 % of the speed, on a bounded surface (the editor CSS files).
|
|
17
|
-
|
|
18
|
-
## Consequences
|
|
19
|
-
|
|
20
|
-
- The work is a **design-system `:has()` diet** in `src/legacy/tool-ui/src/` editor CSS (`ContentEdit.css`, `ContentInputGroup.css`, `ContentForm.css`, `RepeatableContentInputGroup/Selector.css`, `Widget.css`, …), not a one-line change. Behavior-sensitive: `:has(:focus)`→`:focus-within` is equivalent, but structural `:has(> child)` and `:has(.is-*)` rewrites must keep the same visual result (especially `display:none` hides) or layout regresses.
|
|
21
|
-
- Distinct from byte-bloat work (ADR 0002 and the bloat investigation): reducing `:has()` also trims ~185 KB, but the **runtime** win comes specifically from cutting the editor-subtree `:has()` count — size reduction alone (e.g. icon emission) would not fix the jank.
|
|
22
|
-
- `:has()` becomes a reviewable cost in the editor surface: new `:has()` in CIG/RCIG/ContentForm CSS should be justified or replaced with a class, since the cost scales with how many `:has()` cover an interacted subtree.
|
|
23
|
-
- The lever was first proven via live in-container CSS surgery (the proxy); the shipped fix is the source rewrite below, rebuilt through the real compilers and re-measured on the same page.
|
|
24
|
-
|
|
25
|
-
## Outcome (implemented 2026-06-02, branch `fix/v5-css-bloat`)
|
|
26
|
-
|
|
27
|
-
Behavior-preserving rewrite of the 42 editor-scoped `:has()` source rules: `:has(:focus…)` → `:focus-within`; `:has(+ .CIG-row)` / error-border → `:not(:last-child)`; one inert `transition-delay` rule dropped; everything structural (`:has(> child)`, `:has(~ sibling)`, `:has(.is-*)`) → a runtime class set by a new self-contained module `src/legacy/tool-ui/src/editorHasCompat.ts` — an idempotent, debounced sweep (`querySelectorAll` + `classList.toggle`) driven by ready + the editor's `brightspot-content-state-change` event + a 120 ms `MutationObserver`, **not** Blink's per-mutation `:has()` re-test. (FormFilter marks `.is-filterResultAncestor` on result ancestors in JS, replacing `:has(.is-filterResult)`.)
|
|
28
|
-
|
|
29
|
-
- **Built-Monolith count (PostCSS walk):** total `:has(` **993 → 765**; editor-scoped (`.CIG/.RCIG/.EIG/ContentForm`) **223 → 3**. The 3 residuals are the self-contradictory `RCIG-list:empty:has(ol,ul)` ContentForm placeholders (`:empty` + `:has(children)` → never match), absent from normal content-edit surfaces.
|
|
30
|
-
- **Runtime verdict — human A/B, per the [microbenchmark pitfall](../../plans/v5-css-has-runtime-perf-investigation.md):** editor went from janky to "a lot better / acceptable." Synthetic forced-reflow probes do **not** reproduce the jank on Chrome 148 and must not be used to confirm it.
|
|
31
|
-
- **Confirmed by Chrome DevTools *Selector stats* under real editor churn (sorted by invalidation count):** **no rewritten editor `:has()` appears** — `has-formContent` et al. are plain classes now, so they don't invalidate. The only editor-scoped `:has()` in the trace are the 3 inert ContentForm `:empty:has` residuals, at ~748 invalidations but **~0.1 ms each** (`:empty` + `:has(children)` bails instantly — confirmed harmless). The top invalidators are all sub-1.1 ms: `:hover`/`:focus-within` recalc (the pattern this rewrite adopts) and tiny-scope out-of-scope page-chrome `:has()` (calendar / preview / page-header). The per-interaction `:has()` invalidation storm is gone; cost is now ordinary recalc.
|
|
32
|
-
- **Correctness:** for each runtime class, `querySelectorAll('.has-X')` equals the original `:has()` selector element-for-element — 11 classes confirmed EQUAL across Users / Blog Post / Dashboard, the `has-cig` setter and the live `MutationObserver` path confirmed by injection, no console errors. Found and fixed one gap during verification: `DashboardWidget`'s `form:has(.CIG)` → `form.has-cig` rewrite had no setter (added).
|
|
33
|
-
- **Follow-up (out of scope, file separately):** the single most expensive selector in the trace, `.is-imagePreview:not(:has(.ImageEditor-groups.is-focus-active))` (~46 ms, 0 invalidations), is a pre-existing ImageEditor `:has()` outside the `.CIG/.RCIG/.EIG/ContentForm` scope; 7 ImageEditor-related `:has()` remain as a candidate next pass.
|