diff --git a/docs/SUPPORTED-PROPS.md b/docs/SUPPORTED-PROPS.md index 3c7e9ddf..e2fe9ea1 100644 --- a/docs/SUPPORTED-PROPS.md +++ b/docs/SUPPORTED-PROPS.md @@ -32,21 +32,18 @@ these views has exactly one of these fates, and they are different promises: | --- | --- | --- | | **consumed** | the component, or its caller in the engine, reads it | nothing — we already own the behaviour | | **stripped** | removed at the DOM boundary by `src/core/components/domProps.js` | only the boundary moves | -| **forwarded** | handed to semantic-ui-react, which decides what happens | everything: this is the parity risk | +| ~~**forwarded**~~ | ~~handed to semantic-ui-react, which decides what happens~~ | **no longer a fate.** The exit completed, so nothing is handed anywhere but to code in this repository | | **dropped** | semantic-ui-react handled it; the in-house component deliberately does not | already happened — this is the semver record for that step | A checklist that mixed them would be full of props that never mattered. Each section below is split that way. -The forwarded set is tiered, because "supported" and "used" are different facts: - -- **tier 1 — exercised.** Used by a node in the tracked example corpus, by one of the - consumer metas audited in step 0, or generated unconditionally by the wrapper. A - regression here is a live bug. -- **tier 2 — published but unexercised.** Reachable through the wrapper API or by - passthrough, but no meta in either corpus uses it. These cannot be dropped *silently* — - they are documented propTypes — but reimplement-vs-deprecate is a decision for the step - PR, not an automatic obligation. +The forwarded set **was** tiered, because "supported" and "used" are different facts: tier 1 +meant a node in the tracked corpus or an audited consumer meta exercised it, tier 2 that it +was published but unexercised. Both are retired with the set itself — tiers ranked a PARITY +CHECKLIST, and there is no second implementation left to reach parity with. The distinction +they encoded did its job: every tier-2 prop on the dropdown was resolved as a deliberate +removal rather than a silent one, which is what the `Dropped` tables below record. ## Isolation invariant @@ -343,15 +340,15 @@ What each step owes beyond "the props above still work". - WHAT THE GATE CANNOT SAY, so the replacement is not judged on it: everything positional. jsdom reports 0×0 for every rect, so flip, the resulting placement-class change, shift, the arrow geometry, the 250 px wrap, clipping, stacking, painted style, real pointer travel and screen-reader announcement are all inexpressible. They are named one by one against the §9.5 Playwright item, which now blocks THIS step's completion rather than only F1's. - FREE CLEANUP, confirmed: the `TooltipPop` chain in `modules/form/utils.js` is dead at four sites — the import (line 8), `withForm`'s `Tooltip = TooltipPop` default parameter, the pass-through into `withFormSetup({… Tooltip})`, and the destructure that never uses it. Delete all four; do NOT touch line 7, which imports the in-house `Tooltip` as `ToolTip` and IS used by the validation-error tooltip. -### Step 3 — `Dropdown` +### Step 3 — `Dropdown` — SHIPPED -**Effort: L (unchanged size, different location).** +**Effort: L (came in at the L; the matrix was the cost, as predicted).** -- The L is NOT in `search`/`multiple`/`allowAdditions`/`clearable` — nothing uses them. It is in the keyboard/a11y matrix, the cascading flows, `upward`'s auto-flip, and the `.ui.selection.dropdown` CSS contract. -- Keep `displayName = 'Dropdown'` AND the named-vs-default export split: `modules/form/utils.js` branches on `InputComponent.displayName`, and only the named export carries it — `React.memo(...)` does not. -- Reproduce SUIR's aria shape: `role="listbox"` (or `combobox` under `search`), `aria-expanded`, `aria-disabled`, `tabIndex=-1` when disabled. SUIR renders no hidden native input, so the form binding is entirely react-final-form. -- Decide tier 2 explicitly: reimplement or deprecate. They are published propTypes/JSDoc, so they cannot be dropped silently — but they are not evidence for an L estimate either. -- Owed to this page before the swap: a measured `classContract` and `behaviourContract`, the way step 2 part 1 produced them for `TooltipPop`. Both fields are optional in the curation only because they have not been measured for `Dropdown` yet — an absent one means unmeasured, not "no contract", and `.ui.selection.dropdown` is known to be load-bearing. +- The L was NOT in `search`/`multiple`/`allowAdditions`/`clearable` — nothing used them, and they were REMOVED rather than reimplemented. It was in the keyboard/a11y matrix, which had to be built rather than ported: `Home`/`End`, `PageUp`/`PageDown` and typeahead were measured ABSENT from the library, so there was nothing to preserve and everything to write. +- MET: `displayName = 'Dropdown'` and the named-vs-default export split both survive — `modules/form/utils.js` still branches on `InputComponent.displayName`, and only the named export carries it. +- MET, and one addition: `role="listbox"`, `aria-expanded`, `aria-disabled` and `tabIndex=-1` when disabled are all emitted. `aria-disabled` was MISSING in the first draft and caught by the behavioural suite — a `role="listbox"` div cannot carry the native attribute, so being unavailable has to be said three ways. Added beyond the library: `aria-activedescendant`, which is how the keyboard cursor is announced now that moving no longer commits. +- DECIDED: tier 2 was resolved as REMOVAL, on the evidence that nothing in either corpus or the consumer-only record declares any of them. Not silently — the removed names are stripped at the boundary and warn once each in development, and the `Dropped` table below is the record. +- DELIVERED: both `classContract` and `behaviourContract` are measured and present above. The class contract is pinned token by token in `src/style/__tests__/css.dropdown-contract.test.js` (what each class is worth in scoped rules), and the behaviour contract is the WAI-ARIA listbox model — arrows move a cursor, Enter commits, Escape reports nothing. ## What this page does and does not guarantee @@ -368,9 +365,11 @@ test against the real `EXAMPLES` manifest. Not guaranteed, and deliberately so: -- **the forwarded set is open.** A rest spread cannot be closed by static analysis; a meta - may pass any semantic-ui-react prop. The tier-1 list is what was found, not a proof of - what is possible. +- **the rest spread is still open.** A rest spread cannot be closed by static analysis, so a + meta may pass a name nobody anticipated. What changed with the exit is where it lands: not + into semantic-ui-react, but onto a DOM element, or into a component's own dropped-prop + list where it is stripped and warned about once. The inventories below are what was found, + not a proof of what is possible. - **the emitted className strings.** The generator reads which props a component consumes, not what it composes them into. `ui table` surviving on the root is asserted by `src/core/components/__tests__/Table.test.js` and by the 38-example DOM baseline, not here. diff --git a/docs/UPGRADE-PLAN.md b/docs/UPGRADE-PLAN.md index 9b174cca..b8a9d852 100644 --- a/docs/UPGRADE-PLAN.md +++ b/docs/UPGRADE-PLAN.md @@ -46,7 +46,7 @@ Because `react`/`react-dom` are webpack **externals** and npm **peer dependencie | Bundler | webpack 5 (declared `^5.99`; + dev-server 5, CLI 6) | ✅ current | | Transpiler | Babel 7 (declared `^7.26`), `babel-loader` 10 | ✅ current | | Tests | Jest 30 + `jest-environment-jsdom` 30 | ✅ current, React-18-ready | -| Node | engines `>=22`, `.nvmrc` = 24 | ✅ current | +| Node | engines `>=18` (relaxed 2026-08-18, see §10), `.nvmrc` = 24 | ✅ current | | Lint | ESLint 8 + `eslint-config-react-app` 7 | ✅ `npm run lint:js` exits 0 errors / 0 warnings with `--max-warnings 0` and runs in CI; the 22 warnings were triaged in Phase 0.9 (one was a real defect, R18) and the 14 zero-reference devDependencies are gone | | Styling | LESS 3.13 (pinned for the semantic-ui-less + `less-plugin-functions` toolchain), PostCSS 8, stylelint 16 | ✅ works; LESS pin is a separate watch-item (§9.8) | | Types | Hand-written public API types in `src/library/types/`, emitted via `tsconfig.build.json` (declaration-only) | ✅ describes the direct callable UMD/CommonJS export; emitted declarations compile with `skipLibCheck: false` against locked `@types/react` 16/17/18 consumers using both interop-default and direct `import = require` (§2.6-1, Phase 0.6) | @@ -128,7 +128,7 @@ A second audit pass — against the working tree, the build configs, a full test | 4 | **Engine ↔ form-modules import cycle:** `form/utils.js:13–14` imports `errorsProcessing`, `clearErrorsMap`, `formsStorage` from `pages/main/*`, while `rules.js:3` imports `storedTouched`, `withForm` from `modules/form` — in addition to the known `Text.js:4` violation. | §9.3 step 2, §9.9-H5 | | 5 | **Module-global mutable state is wider than first catalogued:** besides `FIELD.FUNC`/`Active.translate`/`errorHandlerFunction` — `formsStorage` (`rules.js:199`, module-level `Map`), `errorsMap` (`rules.js:204`), `formInitialValues` + `storedTouched` (`form/utils.js:23–24`); and every instance renders a fixed-id `
` (`AppWrapper.js:17`) that the modal portal resolves via global `getElementById` (`Popup.js:88`) — first instance in the DOM wins. (`FIELD.METHODS`, named in an earlier review draft, does not exist.) | §9.3 step 3, R14 | | 6 | **Form-runtime hazards (StrictMode/longevity-relevant):** `form.subscribe()` runs on **every render** with the unsubscribe discarded (`form/utils.js:382`); `setState` during render in the Dropdown field branch (`form/utils.js:199–208`); a debounced handler lives on the **prototype**, sharing one timer across instances (`form/utils.js:571`); state is mutated in place via the documented-as-mutating `set()` before `setState` (`rules.js:1108/1150/1211/1215`); `AutoSave`'s debounce is never cancelled on unmount; `autoSubmit` creates a fresh `debounce(instance.submit)` on every render pass (`mapper.js:588`). (Checked and clean: `Slider` and `@withTimer` DO clean up — only an unmount-mid-drag edge remains.) | §9.3 step 4, §7 | -| 7 | **Published CSS leaks global styles.** `postcss.config.js` deliberately exempts `html`, `body`, `*` from prefixwrap, so the bundled semantic reset restyles the host page. **Figures corrected 2026-08-27:** the earlier "2× `html{`, 2× `body`, 1× `*{`" came from an anchored line-start grep, which cannot work — the published CSS is minified (38 lines, ~407 KB). A selector-level parse of `static/all.css` finds **13 unscoped occurrences across 11 rules** (`body` 6, `html` 5, `*` 2) and zero `.ui-render html`. The path was also stale: since §0.7 the real file is root `static/all.css` and `dist/static/all.css` is a 130-byte `@import` re-export. The standalone `scripts/build-css.js` uses **different** prefixwrap options, and the LESS-stage Jest test (`css.compilation.test.js` — not `css-contract.test.js`, which does not exist) runs **before** PostCSS. Now gated: `src/style/__tests__/css.pipeline.parity.test.js` pins the final CSS. | §9.9-H8, §9.5, F1 step 4 | +| 7 | ~~**Published CSS leaks global styles.**~~ **CLOSED 2026-09-15 (§9.9-H8).** `postcss.config.js` no longer exempts anything but `.ui-render-*`, so nothing reaches a host page: measured, the host `` computes browser defaults and the declarations that used to land on it are on `.ui-render` itself. The finding as it stood: it exempted `html`, `body`, `*` from prefixwrap, so the bundled semantic reset restyled the host page. **Figures corrected 2026-08-27:** the earlier "2× `html{`, 2× `body`, 1× `*{`" came from an anchored line-start grep, which cannot work — the published CSS is minified (38 lines, ~407 KB). A selector-level parse of `static/all.css` finds **13 unscoped occurrences across 11 rules** (`body` 6, `html` 5, `*` 2) and zero `.ui-render html`. The path was also stale: since §0.7 the real file is root `static/all.css` and `dist/static/all.css` is a 130-byte `@import` re-export. The standalone `scripts/build-css.js` uses **different** prefixwrap options, and the LESS-stage Jest test (`css.compilation.test.js` — not `css-contract.test.js`, which does not exist) runs **before** PostCSS. Now gated: `src/style/__tests__/css.pipeline.parity.test.js` pins the final CSS. | §9.9-H8, §9.5, F1 step 4 | | 8 | **Packaging is unreproducible and oversized.** *Resolved in Phase 0.7,* except the build divergence below. Was: 579 files / **11.6 MB unpacked**, every asset duplicated between `dist/static/*` and root `static/*` (two 407 KB copies of `all.css`), no `prepack`/`prepublishOnly`, no budgets. Now 295 files / **7.25 MB unpacked** (2.53 MB packed) with assets shipped once, `prepack` guarding version drift, and both budgets and a packed-tarball smoke in CI. Source maps ship deliberately (3.0 MB — owners' call, §10). `static/semantic.css` stays a 0-byte stub for compatibility. Still open: the library and watch builds disagree — watch emits `static/ui-render.css` (not the `all.css`/`font.css`/`semantic.css` set) and its `output.clean: true` wipes `dist/index.d.ts`. | §9.9-H7 | | 9 | **A clean checkout does not build.** `Examples.jsx:24–25` imports `src/demo/examples/input-integer_{meta,data}.json`, which exist only as untracked files in the current working tree (8 more untracked example JSONs are unreferenced). | Phase 0.8 | | 10 | **Babel scope correction:** the demo webpack config carries its **own inline presets** (`webpack.demo.config.mjs:37`, no explicit targets → browserslist applies there), so the root `targets: {node:'current'}` problem hits the **library and watch** builds (Jest is already env-split). Fixing the root config alone leaves the demo's duplicated preset setup in place. | Phase 0.5, §9.8 | @@ -186,7 +186,7 @@ Widen, never replace: - **npm consumers**: Phase 1 peer resolution accepts React 17 hosts; the Phase 2 target adds React 18. No breaking change for React 16.14 hosts. - **Install docs**: Phase 1 now recommends React 17 while documenting continued React 16.14 support; Phase 2 must update the recommended install commands again for React 18. - **Hybrid dependency model (§2.6-11):** npm hosts currently install `dependencies` (SUIR, the form stack, rc-picker, …) that are *also* bundled into the UMD — dead weight in host `node_modules` and an extra peer-resolution surface. Resolving this (trim `dependencies`, externalize more, or peerize) is an owners' decision — see the gates in §10. -- **`engines.node >= 22` ships to consumers** in the published manifest — on `engine-strict` hosts this fails installs even though Node is only a *build* requirement for this browser library. Decision gate in §10. +- ~~**`engines.node >= 22` ships to consumers**~~ — **DECIDED and shipped (2026-08-18): relaxed to `>=18`.** The manifest now reads `>=18`, describing consumption of the prebuilt bundle rather than building the repo; development still uses `.nvmrc`. The finding as it stood: on `engine-strict` hosts the `>=22` floor failed installs even though Node is only a *build* requirement for this browser library. --- @@ -827,7 +827,27 @@ The wrappers' external APIs are the contract; `mapper.js` and meta authors never One thing CI caught that a local check had missed: `lint:css` globs `src/style/**/*.less`, so the vendored files entered it and reported 2,295 violations of conventions upstream never followed. `.stylelintignore` excludes `src/style/vendor/` — generated third-party CSS is not where style decisions are made. The local check had been `npm run lint:css | tail -1`, which shows output and hides the exit code; gates are verified by exit code now. -- **Step 5 — cleanup + release.** The `semantic-ui-react` dependency is already gone (Step 3½); this step drops `semantic-ui-less`, finishes changelog + supported-prop documentation, and records the bundle delta (−30 KB+ per the source's own estimates: 27 KB Dropdown + 4 KB Table + Popup). **SEMVER — DECIDED (2026-09-11): this ships as a MINOR.** The maintainers' call, and it is recorded here so the earlier reasoning is not mistaken for the decision. What the plan said before: Step 0 found no unsupported passthrough props in the wild, so on that evidence a minor — with the tier-2 decision as what could turn it major, since dropping a published propType is breaking even when nothing observed uses it. Steps 2 and 3 then did drop published propTypes (19 tooltip names, and `search`/`multiple`/`allowAdditions` with their companions), and the analysis in those steps argued for a major on that basis. The owners weighed it against the evidence that nothing in either corpus or the consumer-only record declares any of them, and chose minor. `docs/SUPPORTED-PROPS.md` still carries the full `Dropped` inventory per component, which is where a consumer looks regardless of the version number. +- **Step 5 — cleanup + release — DONE EXCEPT THE RELEASE ITSELF (2026-09-15).** The release is deliberately deferred; everything else this step named is discharged. `semantic-ui-react` went at step 3½ and `semantic-ui-less` at **step 4**, not here — this step's own text said it would drop the latter, and the work simply landed one step earlier. + + **THE BUNDLE DELTA — MEASURED, and the estimate it replaces was low by 6.7×.** This step asked to "record the bundle delta (−30 KB+ per the source's own estimates: 27 KB Dropdown + 4 KB Table + Popup)". Those estimates came from comments in the source and had never been checked. Measured by building the library at `c754daf5` — the commit before step 1, where all three components still wrapped SUIR — and comparing with master: + + | artifact | before | after | saved | gzip saved | + |---|---|---|---|---| + | `dist/index.js` | 436,690 | 333,529 | **103,161** (−23.6%) | 33,486 | + | `static/all.css` | 407,288 | 307,785 | **99,503** (−24.4%) | 10,316 | + | **total** | 843,978 | 641,314 | **202,664 (−24.0%)** | **43,802** | + + Why the estimate was so far off: it counted only the JS weight of the three components and had no CSS term at all. Half the saving is stylesheet — 67 KB from deleting three unused Semantic modules at step 4, the rest from the reset and dropdown modules shrinking as they were vendored and scoped. The gzip column is the number that describes a real page load: **~44 KB less transferred, every time.** + + The baseline build also demonstrated what the exit removed, by failing: a clean `npm ci` at `c754daf5` could not build until `theme.config` was copied into `node_modules/semantic-ui-less/` by hand. That dependency is gone. + + **THE DOCUMENTATION HALF, discharged 2026-09-15.** The changelog and `docs/SUPPORTED-PROPS.md` were brought true rather than merely finished: three statements in the changelog were wrong, two of them written during this workstream — that the Semantic *styles* were still loaded (they were vendored at step 4), that the tooltip "has been in-house for two releases" (it has shipped in NONE — `git tag --contains` is empty for that commit), and stale tarball figures. The props page still carried a "Step 3 — `Dropdown`" section of unmet obligations that the same page discharged, and a live "forwarded to semantic-ui-react" outcome with its tier vocabulary. All corrected at the source, since that page is generated. + + Added while there, because a MINOR that removes published props has to be actionable: `docs/SUPPORTED-PROPS.md` and `docs/SUPPORTED-VIEWS.md` now ship IN the tarball (they were site-only), and the manifest gained `repository` and `bugs`. A consumer who installs the release can reach the record of what was removed without knowing a URL. + + **WHAT IS DEFERRED, deliberately: the release itself.** Both acts — `npm publish` and `npm run build && npm run deploy` — are the maintainers' call and are not taken here. `master` therefore still carries React 17+18 and the whole F1 exit unreleased, and the changelog's `### Unreleased` heading is accurate. + + **NOT part of step 5, and still open:** the §9.8 LESS-pin re-evaluation, whose trigger was "right after step 4" and which is now due; and `watch-lib`'s divergence from the published build (`yalc-watch` is how a consumer validates a pre-release, so it should be right before one is offered). **SEMVER — DECIDED (2026-09-11): this ships as a MINOR.** The maintainers' call, and it is recorded here so the earlier reasoning is not mistaken for the decision. What the plan said before: Step 0 found no unsupported passthrough props in the wild, so on that evidence a minor — with the tier-2 decision as what could turn it major, since dropping a published propType is breaking even when nothing observed uses it. Steps 2 and 3 then did drop published propTypes (19 tooltip names, and `search`/`multiple`/`allowAdditions` with their companions), and the analysis in those steps argued for a major on that basis. The owners weighed it against the evidence that nothing in either corpus or the consumer-only record declares any of them, and chose minor. `docs/SUPPORTED-PROPS.md` still carries the full `Dropped` inventory per component, which is where a consumer looks regardless of the version number. ##### F1.3 Sequencing & effort @@ -1003,8 +1023,8 @@ src/ | **4** | Hooks migration of leaf components; automatic JSX runtime; ~~SUIR passthrough-prop audit + lint guard (F1 step 0)~~ **shipped** (`docs/SUPPORTED-PROPS.md` + the `no-restricted-imports` guard); **housekeeping H1–H2 (dead code + docs truth)** | ongoing, per-component PRs | — | | **4-S** | **Structure (§9.9): demo isolation (H3); engine re-home `pages/main`+`ui-render` → `core/engine` (H4, pure `git mv` commits); layer-direction lint (H5)** | ~2–4 days, in a quiet window | **H4 before Phase 6** | | **TS** | **TypeScript migration track (§9.6): E0 infra after Phase 2 → utils+contract (E1) → components/modules (E2, rides 4/5a) → engine (E3, rides 6) → public-API switchover (E4) → `prop-types` dependency removed (E5); **go/no-go on the E2/E3 tail after E1** | continuous, per-PR | `tsc --noEmit` green throughout; E4 gated by the `dist/index.d.ts` golden diff | -| **5a** | **SUIR exit, JS side (F1 steps 1–3½): ~~`Table`~~ **shipped** → `TooltipPop` → `Dropdown`, then `semantic-ui-react` removed from `dependencies`**, shipped step by step | ~3–5 weeks | behavioral + visual/a11y suites per step | -| **5b** | **SUIR exit, CSS side (F1 steps 4–5): own the 5 LESS modules; drop `semantic-ui-less` + `theme.config`; LESS pipeline re-eval (§9.8)** | ~1–2 weeks | visual parity | +| **5a** | ✅ **DONE (2026-09-08).** SUIR exit, JS side (F1 steps 1–3½): `Table`, `TooltipPop`, `Dropdown` all in-house, then `semantic-ui-react` removed from `dependencies` | ~3–5 weeks | behavioral + visual/a11y suites per step | +| **5b** | ✅ **DONE except the release and the §9.8 re-eval (2026-09-15).** SUIR exit, CSS side (F1 steps 4–5). Not "own the 5 LESS modules" as written: three were measured unused and DELETED, and only two were vendored. `semantic-ui-less` + `theme.config` are gone. The LESS pipeline re-eval is now DUE — its trigger was "right after step 4" — and §9.8 records that the pin does not dissolve with the package | ~1–2 weeks | visual parity — met byte-for-byte | | **6** | Engine decomposition (`rules.js` / `form/utils.js`): extract → compose → hooks; **StrictMode-clean demo** as acceptance | largest single item; only after 3 | StrictMode clean | | **7** | Decision gate: ESM build; `moment` native adapter go/no-go (F2); form-stack major bump if not done earlier (F4); **React 19 go/no-go — earliest right after 5a (§8 fast path)** | decision + scoped work | — | @@ -1033,7 +1053,7 @@ Phases 3 and 4 can partially overlap. Tracks **5a/5b** (SUIR exit) and **6** (en | # | Risk | Likelihood | Impact | Mitigation | |---|---|---|---|---| | R1 | React 18 automatic batching changes form-flow behavior in `rules.js` (async `setState` sites) | Medium | High | Phase 0 tests *before* the bump; full example QA; `flushSync` as documented last resort | -| R2 | `semantic-ui-react` upstream becomes fully unmaintained before the exit completes (stuck on an unpatchable beta) | Medium | Medium | Planned full exit (§9.7-F1) caps the exposure; isolation + eslint guard until then (**both in place since step 0**); a 3-component surface keeps even an emergency exit bounded | +| ~~R2~~ | ~~`semantic-ui-react` upstream becomes fully unmaintained before the exit completes~~ — **RETIRED 2026-09-08: the exit completed.** The package is referenced by no file in `src` and is not a dependency, so upstream's state cannot reach this library | — | — | Closed by §9.7-F1 steps 1–3½ | | R3 | RTL 12→16 migration effort balloons (act warnings, async tests) | Medium | Medium | Migrate suite-by-suite; timebox; Phase 0 semantic tests protect behavior while plumbing changes | | R4 | Engine decomposition (§9.3) regresses undocumented meta behaviors | Medium | High | Contract snapshot suite (§9.5) is a hard prerequisite; extract-then-replace sequencing; per-step releases | | R5 | Hosts stuck on React 16 get broken by an inadvertent 17+-only API | Low | Medium | Keep 16.14 floor; React 16/17 CI mount smoke (§9.5) | @@ -1047,7 +1067,7 @@ Phases 3 and 4 can partially overlap. Tracks **5a/5b** (SUIR exit) and **6** (en | R13 | TS migration drifts the public API types (E4 switchover) or breaks a build pipeline (Babel strips types file-by-file) | Medium | Medium | Golden `dist/index.d.ts` diff gate; `isolatedModules` + CI `tsc --noEmit` from E0; probe files verify all four pipelines before any real conversion | | R14 | Module-global engine state (`FIELD.FUNC`, `Active.translate`, `errorHandlerFunction`, `formsStorage`, `errorsMap`, `storedTouched`, fixed-id popup root) — two `UIRender` instances on one page interfere **today**, and StrictMode double-invocation trips on the same writes | High (current behavior) | Medium–High | §9.3 step 3 (de-globalization incl. per-instance portal root); two-instance case added to the contract suite; until then, document the single-instance assumption for hosts | | R15 | Public declarations or package contents regress after the corrected baseline; the tarball remains oversized and unbudgeted | Low (was Medium) | High | **Closed.** Corrected golden d.ts, locked React 16/17/18 consumer matrix, fresh hosted checkout, the `prepack` version/build guard, asset deduplication, pack budgets with a duplicate-asset guard and the packed-tarball server-render smoke have all landed and run in CI | -| R16 | Published CSS restyles host pages (unscoped `html`/`body`/`*` reset in `all.css`) | High (current behavior) | Medium–High | H8 decision + unified prefixwrap; §9.5 final-CSS gate; implemented in F1 step 4 | +| ~~R16~~ | ~~Published CSS restyles host pages (unscoped `html`/`body`/`*` reset in `all.css`)~~ — **RETIRED 2026-09-15: §9.9-H8 shipped.** Nothing escapes `.ui-render`; measured before and after in Chrome, and `css.pipeline.parity.test.js` pins the counts at zero in both pipelines | — | — | Closed by the H8 decision | | R17 | Form-runtime leaks (per-render subscriptions, prototype-shared debounce, state mutation, uncancelled timers) degrade long-lived host sessions and multi-instance pages | Medium–High | Medium | §9.3 step 4 fixes (shippable before the full decomposition), gated by Phase 0 tests; R14 two-instance coverage | | R18 | Tolerated lint warnings hide real defects in paths no test exercises | Medium | Medium–High | The 0.9 triage confirmed it repeatedly. **Fixed, each with a test:** the `no-use-before-define` pair in `rules.js` was a temporal-dead-zone `ReferenceError` that silently swallowed any interpolated-template popup whose local data was the row array — 1920 green tests never reached it; the `no-useless-escape` line in `demo/services/axios.js` indexed a possibly-null `String.match`, so an unmatched server message threw a TypeError inside the rejection handler and replaced the real API error; and `jsx-a11y/alt-text` on `Image.js` was not merely a false positive — the justification for suppressing it ("`alt` is always set") rested on `fileNameWithoutExt(name)`, which has no guard, so `view: IMAGE` meta carrying `src` without `name` crashed the whole render. A `no-unused-vars` import additionally exposed a JsonView test whose name promised a click it never performed (renamed; real toggle coverage lives in `JsonView.behavior.test.js`). **Fixed in the follow-up pass, each with a test that fails beforehand:** `hasObjKeys(…, 'shallow')` OR-ed its loose comparison onto the object branch, so a reference comparison vetoed every object match the first clause had already accepted and no shallow object match could ever succeed — restructured so only non-object-like values compare loosely (its JSDoc also claimed the wrong default mode); `Dropdown`'s sanitize switch treated a `null` option value as an object (`typeof null === 'object'`) and rewrote it to the string `"null"`, which the cascading-reset effect could then never match; and `View` took a `ref` parameter its `React.memo` export can never deliver. **Method note:** two of the three fixed defects were found only because the warning was re-read adversarially after the obvious cleanup — a suppression comment that sounds convincing is not evidence. `lint:js --max-warnings 0` now stops the next one from being tolerated | @@ -1080,9 +1100,15 @@ rg -l "from 'semantic-ui-react'" src | grep -v "^src/core/components" # moment usage funnel check (§9.7-F2 — target: only the dateAdapter module remains) rg -n "from 'moment'" src -# SUIR exit progress (§9.7-F1 — target: both return nothing) +# SUIR exit progress (§9.7-F1 — target: all three return nothing) +# +# THE SECOND COMMAND USED TO BE `grep -v '^\s*//' … | grep '@import'`, and it was UNSATISFIABLE +# once the exit completed: it matched our own prose comments and the legitimate imports of the +# vendored CSS in `src/style/vendor/`. A gate that cannot pass is not a gate. What the step +# actually asserts is that nothing REACHES the packages — expressed directly below. rg -n "from 'semantic-ui-react'" src -grep -v '^\s*//' src/style/override/_semantic.less | grep '@import' || true +rg -n --glob '*.less' '@import[^;]*(semantic-ui-less|@\{libPath\})' src/style # .less only: the invariant is about LESS imports, and prose in .js tests names the package legitimately +node -e "const p=require('./package.json'); const bad=['semantic-ui-react','semantic-ui-less'].filter(d=>(p.dependencies||{})[d]||(p.devDependencies||{})[d]); if(bad.length){console.log(bad.join(' '));process.exit(1)}" # SUIR erosion guard + published prop surface (§9.7-F1 step 0) # The eslint override fails lint:js on an `import` outside src/core/components; the generated @@ -1168,7 +1194,7 @@ Every check this plan depends on, in one place. ✅ = already verified during th - ✅ Whole suite gating on React 16.14 (`react-16-floor`) and 17.0.2 (`react-17`); advisory on 19; packed artifact smoked on 16.14/17 - ✅ Markup-independent behavioral contract layer (layer 2) — the layer that gates F1, where the DOM changes intentionally: 4 suites / 104 tests over one shared harness, pinned as literals rather than snapshots so `-u` cannot bless a change; roles, ARIA state, accessible names, visible text and form values only. Tabs and upload expose no interactive role at all, so their keyboard contract stays with the Playwright suite below, and no manifest example enables table sorting or row expansion, so neither has a markup-independent gate yet - ✅ JSON Schema for meta + dev-mode validation, the error-boundary/`onError` repair (§9.4, §2.6-3) and the config-channel repair (§2.6-2) -- ☐ Visual/a11y regression suite (mandatory for F1); ~~note the 57 dangling `aria-describedby` references the DOM baseline records will trip it~~ — fixed 2026-08-27: the attribute is now conditional on the message element existing, and the DOM contract asserts that every reference resolves +- ☑ Visual/a11y regression suite (mandatory for F1) — SHIPPED at step 2 part 2 and gating the CI `browser` job (41 Chromium tests); ~~note the 57 dangling `aria-describedby` references the DOM baseline records will trip it~~ — fixed 2026-08-27: the attribute is now conditional on the message element existing, and the DOM contract asserts that every reference resolves ### Phase 1 — React 17 @@ -1223,20 +1249,20 @@ Every check this plan depends on, in one place. ✅ = already verified during th - ☑ **Step 2 part 3:** the review was run a THIRD time and every candidate now has a verdict — **28 upheld, 27 refuted, 0 unverified** of 55, with all 171 agents completing. Two of the four findings I had verified myself were refuted on evidence better than mine, and one of my own characterisations was wrong: I had recorded the `pointer-events: none` selection loss as *predating* this step. It does not — `git show master:…tooltip.less` has no such declaration, so this branch introduced it. But it is not the regression I called it either: `.app__slider` already sets `user-select: none` (`slider.less:20`), so the Slider value text was never mouse-selectable, and the handle drags on `pointerdown`, so a press inside the value bubble used to MOVE the slider — the declaration is an improvement there. The `Upload` bubble is a single word. Verified all three claims directly - ☑ **Step 2 part 3:** the two CSS pins the third pass argued about are fixed on the agreed FACTS rather than on the vote. `CORNER_AXIS_DECLARATIONS` pinned `top` alone while its own comment promised that deleting any declaration as "redundant" would fail there; it now covers `transform` too — the least obviously load-bearing one, because `.tooltip.left`/`.right` set their own `transform` at equal specificity and later in source order, so `transform: none` on a corner is what stops the bubble shifting a full width left and half its height up. Verified by deleting it: the compiled rule loses the property and the test fails naming it. And `.tooltip.show { animation-delay: 0s }` is now labelled a CASCADE LOSER — measured `0s` with and without the class, because `.fade-in`'s shorthand resets it — so the pin cannot be read as proof the declaration does anything - ☑ **Step 2 part 3:** `behaviourContract` was stored for in-house components and rendered only for wrappers, so the tooltip's whole open/close/dismiss contract was missing from `docs/SUPPORTED-PROPS.md` — the one document that actually reaches a consumer. It renders now, and **SC 1.4.13's Hoverable clause is named there as a known non-conformance** instead of being left for a reader to derive: the bubble cannot be hovered, Dismissable and Persistent are met, and it is kept because the alternative measured worse -- ☐ **Step 2 part 3:** the accessibility trade is **recorded, not resolved** — 1.4.13 wants hovered content hoverable and this bubble cannot be, because `pointer-events: none` is what stops it swallowing the pointer and flickering; and a touch-only device now has no way to see a tooltip at all. Both are consequences of decisions the maintainers took, and both are written down in `docs/SUPPORTED-PROPS.md`, `e2e/reference.js` and §9.7-F1 step 2 rather than left for a reader to discover -- ☐ **Step 2 part 3 — OBLIGATION 1: the 500 ms open delay survives.** The inline component reveals through the CSS rule `*:hover > &` (`style/components/tooltip.less`), which has no delay, so a naive convergence drops it. It is a deliberate UX decision, stated in `TooltipPop.js` — "avoiding an accidental popup when the user is already familiar with the UI" — and pinned to the millisecond in three places (`[I] hover opens after our 500 ms delay` in `e2e/corpus.tooltip.pw.js`, `TooltipPop.behavior.test.js`, and the 499/500 boundary in `UIRender.overlay-behavior.test.js`). Measured: the simulated convergence fails all of them. Keep it in JavaScript, or `transition-delay` if the reveal stays CSS — either way it must remain gated -- ☐ **Step 2 part 3 — OBLIGATION 2: click-to-open survives.** SUIR's `on: ['click', 'hover']` gives the tooltip a click path the CSS rule has no equivalent for. Measured as failing under the simulated convergence (`[R] click opens with no delay, a second click closes`, `e2e/harness.tooltip.pw.js`). Decide deliberately: reimplement, or drop it and say so in the changelog — but not by omission -- ☐ **Step 2 part 3 — OBLIGATION 3: Escape and click-outside still dismiss.** This is accessibility, not polish: a pointer-only dismissal path leaves keyboard users with no way to close the bubble. Both are pinned (`[I] a click outside closes it; Escape closes it`, and `[I] Escape closes an open bubble from an unrelated native input` — the latter is only answerable in a browser, since jsdom has no native focus semantics). Measured: both fail under the simulated convergence, because the inline component has no JavaScript at all -- ☐ **Step 2 part 3 — HAZARD: the bubble must not intercept pointer events.** An inline bubble positioned over its trigger swallows the pointer, so the trigger cannot be hovered — Playwright reported ` intercepts pointer events` and retried until timeout. `pointer-events: none` is therefore a requirement of the inline shape. `tooltip.less:12` hints at the same hazard from the other direction (`display: none` is used because `visibility: hidden` "will trigger hover"), and the reason `Slider`/`Upload` never hit it is that their bubbles do not cover an active area. Adding it fixed exactly one of the five simulated failures, so it is necessary and not sufficient -- ☐ **Step 2 part 2 (NEW, from the browser leg):** `tooltip.less`'s corner placements fixed, or the vocabulary explicitly restricted — `.tooltip.left`/`.tooltip.right` set `top: 50%` at the same specificity as `.tooltip.top`/`.tooltip.bottom`, so `top left`/`top right` land ON the host and `bottom left`/`bottom right` degrade to `left`/`right`. `top left` is the only placement `TooltipPop` uses, so converging on the in-house `Tooltip` without this ships a bubble sitting on its own trigger -- ☐ **Step 2 part 2:** `omitProps` applied (the boundary leaks `view`/`index`/`symbol` onto the bubble today, pinned as a defect); `on` decided; the 45-prop passthrough decided; the `items` trigger shape decided; the dead 4-site `TooltipPop` chain in `modules/form/utils.js` deleted -- ☐ **Steps 2–3:** per component — behavioral contract layer + example QA green (full-DOM snapshots regenerated deliberately); emitted classNames keep existing LESS working -- ☐ **Step 3:** Dropdown keyboard/a11y matrix verified (WAI-ARIA combobox pattern), incl. cascading-Select flows from `rules.js` -- ☐ **Step 3:** replacement keeps `displayName = 'Dropdown'`, or the form-adapter branch (`form/utils.js:193,199`) is refactored in the same PR -- ☐ **Step 3½:** `semantic-ui-react` absent from `dependencies` (this is the 5a exit and the React 19 unblock) +- ☑ **Step 2 part 3:** the accessibility trade is **recorded, not resolved** — 1.4.13 wants hovered content hoverable and this bubble cannot be, because `pointer-events: none` is what stops it swallowing the pointer and flickering; and a touch-only device now has no way to see a tooltip at all. Both are consequences of decisions the maintainers took, and both are written down in `docs/SUPPORTED-PROPS.md`, `e2e/reference.js` and §9.7-F1 step 2 rather than left for a reader to discover +- ☑ **Step 2 part 3 — OBLIGATION 1: the 500 ms open delay survives.** MET — `TooltipPop.js` keeps it in JavaScript and three suites pin it. The inline component reveals through the CSS rule `*:hover > &` (`style/components/tooltip.less`), which has no delay, so a naive convergence drops it. It is a deliberate UX decision, stated in `TooltipPop.js` — "avoiding an accidental popup when the user is already familiar with the UI" — and pinned to the millisecond in three places (`[I] hover opens after our 500 ms delay` in `e2e/corpus.tooltip.pw.js`, `TooltipPop.behavior.test.js`, and the 499/500 boundary in `UIRender.overlay-behavior.test.js`). Measured: the simulated convergence fails all of them. Keep it in JavaScript, or `transition-delay` if the reveal stays CSS — either way it must remain gated +- ☑ **Step 2 part 3 — OBLIGATION 2: click-to-open survives.** DECIDED THE OTHER WAY and recorded: `on` was DROPPED on the maintainers' instruction, with focus-open added so a keyboard still reaches the content. Documented in `docs/SUPPORTED-PROPS.md` under `dropped.on` — not by omission. SUIR's `on: ['click', 'hover']` gives the tooltip a click path the CSS rule has no equivalent for. Measured as failing under the simulated convergence (`[R] click opens with no delay, a second click closes`, `e2e/harness.tooltip.pw.js`). Decide deliberately: reimplement, or drop it and say so in the changelog — but not by omission +- ☑ **Step 2 part 3 — OBLIGATION 3: Escape and click-outside still dismiss.** MET — both implemented in `TooltipPop.js` and pinned in jsdom and in Chrome. This is accessibility, not polish: a pointer-only dismissal path leaves keyboard users with no way to close the bubble. Both are pinned (`[I] a click outside closes it; Escape closes it`, and `[I] Escape closes an open bubble from an unrelated native input` — the latter is only answerable in a browser, since jsdom has no native focus semantics). Measured: both fail under the simulated convergence, because the inline component has no JavaScript at all +- ☑ **Step 2 part 3 — HAZARD: the bubble must not intercept pointer events.** HANDLED — `pointer-events: none` is in `tooltip.less` and `css.tooltip-contract.test.js` pins it, with the 1.4.13 consequence recorded rather than hidden. An inline bubble positioned over its trigger swallows the pointer, so the trigger cannot be hovered — Playwright reported ` intercepts pointer events` and retried until timeout. `pointer-events: none` is therefore a requirement of the inline shape. `tooltip.less:12` hints at the same hazard from the other direction (`display: none` is used because `visibility: hidden` "will trigger hover"), and the reason `Slider`/`Upload` never hit it is that their bubbles do not cover an active area. Adding it fixed exactly one of the five simulated failures, so it is necessary and not sufficient +- ☑ **Step 2 part 2 (NEW, from the browser leg):** `tooltip.less`'s corner placements FIXED — all eight placements land off their host, verified in Chrome. `tooltip.less`'s corner placements fixed, or the vocabulary explicitly restricted — `.tooltip.left`/`.tooltip.right` set `top: 50%` at the same specificity as `.tooltip.top`/`.tooltip.bottom`, so `top left`/`top right` land ON the host and `bottom left`/`bottom right` degrade to `left`/`right`. `top left` is the only placement `TooltipPop` uses, so converging on the in-house `Tooltip` without this ships a bubble sitting on its own trigger +- ☑ **Step 2 part 2:** all five discharged — `omitProps` applied (the boundary leaks `view`/`index`/`symbol` onto the bubble today, pinned as a defect); `on` decided; the 45-prop passthrough decided; the `items` trigger shape decided; the dead 4-site `TooltipPop` chain in `modules/form/utils.js` deleted +- ☑ **Steps 2–3:** per component — behavioral contract layer + example QA green (full-DOM snapshots regenerated deliberately); emitted classNames keep existing LESS working +- ☑ **Step 3:** Dropdown keyboard/a11y matrix verified, incl. cascading-Select flows from `rules.js`. NOTE the deviation from this line as written: the control adopted the WAI-ARIA **listbox** pattern, not combobox — it stays a `role="listbox"`, and `e2e/reference.js` keeps the four remaining combobox attributes as an inventory rather than a debt +- ☑ **Step 3:** replacement keeps `displayName = 'Dropdown'`, or the form-adapter branch (`form/utils.js:193,199`) is refactored in the same PR +- ☑ **Step 3½:** `semantic-ui-react` absent from `dependencies` (this is the 5a exit and the React 19 unblock) - 🔶 Visual/keyboard/a11y suite green per step (mandatory, §9.5) — **the suite now EXISTS** (`playwright.config.js` + `e2e/`, gating CI job `browser`), shipped by Step 2 part 2 and **built for Step 2**. Green for Step 2: all ten named jsdom gaps closed except real assistive technology, which no browser runner can close. **Still NOT met for Step 1** and deliberately not retro-credited — its `.ui.table` padding contract was verified by reading the compiled CSS and by class-string assertions, not by rendered pixels, and stays that way. **Not met for Step 3**, which owns its own `Dropdown` keyboard/a11y matrix; what it inherits is the harness, the probes and a measured starting state (`e2e/reference.js` → `DROPDOWN`: role census, `aria-expanded` behaviour, options present when closed, and the five missing combobox ARIA attributes as a defect inventory). What the suite can never see is enumerated in §9.5 — real AT, cross-browser, real OS pointer/touch, pixels, the published artifact, the 16/17 peer ends, and the `tabs`/`upload` keyboard contracts -- ☐ **Step 4:** pixel parity of extracted CSS vs current compiled output; MIT attribution for vendored semantic CSS in place; H8 scoping decision implemented. (`javascriptEnabled` was listed here and has been REMOVED as a criterion: it is required by our own `_variables.less:23`, not by semantic, so this step cannot satisfy it — see §9.7-F1 step 4 and R8.) -- ☐ **Step 5:** Appendix B "SUIR exit progress" greps return nothing +- ☑ **Step 4:** MET — parity proved twice (the 285-rule gate, and `static/all.css` byte-identical by sha256). pixel parity of extracted CSS vs current compiled output; MIT attribution for vendored semantic CSS in place; H8 scoping decision implemented. (`javascriptEnabled` was listed here and has been REMOVED as a criterion: it is required by our own `_variables.less:23`, not by semantic, so this step cannot satisfy it — see §9.7-F1 step 4 and R8.) +- ☑ **Step 5:** Appendix B "SUIR exit progress" greps return nothing — **after fixing the gate**, which was unsatisfiable as written (its second command matched our own comments and the vendored imports). All three now return nothing ### F2 — moment adapter (only if the gate opens) @@ -1246,7 +1272,7 @@ Every check this plan depends on, in one place. ✅ = already verified during th ### H1/H6 — deletions & duplicate reconciliation (gate before each `rm`) - ☐ Orphan component names grepped across consumer metas as `view` values -- ☐ `override/_policy.less` / `_classic.less` confirmed unreferenced +- ☑ `override/_policy.less` / `_classic.less` confirmed unreferenced — and DELETED at step 4 with 87 other dead files, the set determined by the LESS import graph rather than by grep - ☒ `less-plugin-functions`: removed in a branch → all four pipelines still build → devDep dropped (§9.8). **Attempted and CLOSED as not-doable-as-written (2026-09-11):** the build fails without it at `_variables.less:264` on `round()`. Re-opening this means replacing those nine call sites first. - ☐ Pack `Tabs`/`TabList` audited for orphan status before reconciling the duplicates (H6) @@ -1277,13 +1303,13 @@ Every check this plan depends on, in one place. ✅ = already verified during th - ☐ **Host consumption mode confirmed with host teams (bundler vs script-tag)** — the only item requiring a human answer; it decides how much F3 (ESM) matters and settles the §10 script-tag gate - ☑ `semantic-ui-react` absent from `dependencies` (F1 step 3½ / Phase 5a) — shipped; `npm prune` is part of it, or it stays resolvable locally - ☐ `defaultProps` fixed (Phase 2) and automatic JSX runtime enabled (Phase 4) -- ☐ `@testing-library/react` ≥16.1 in place (the first RTL with `react ^19` peers); `react@19` in the dev/CI matrix; full regression green; peers widened additively (`^16.14 || ^17 || ^18 || ^19`) +- ☑ `@testing-library/react` ≥16.1 in place — `^16.3.2` (the first RTL with `react ^19` peers); `react@19` in the dev/CI matrix; full regression green; peers widened additively (`^16.14 || ^17 || ^18 || ^19`) - ☐ `@types/react@19` pinned for `gen-ts` - ☐ rc-picker / react-refresh / eslint-plugin-react-hooks behave on 19 in the matrix ### Ongoing invariants (commands in Appendix B) -- ☐ SUIR isolation grep: nothing outside the components pack (until exit), then nothing at all +- ☑ SUIR isolation grep: **nothing at all** — the exit completed, so the stronger half of this line is what holds now. Nothing outside the components pack (until exit), then nothing at all - ☐ Alias grep stays zero - ☐ After the F2 seam: only the `dateAdapter` module imports `moment` - ☐ React 16/17 compatibility stays in the CI matrix while the peer floor includes them — the full suite diff --git a/package.json b/package.json index 39c3df9e..1097c708 100644 --- a/package.json +++ b/package.json @@ -3,6 +3,13 @@ "version": "0.34.3", "description": "UI generation based on JSON schemas", "homepage": "https://eisgroup.github.io/ui-render/", + "repository": { + "type": "git", + "url": "git+https://github.com/eisgroup/ui-render.git" + }, + "bugs": { + "url": "https://github.com/eisgroup/ui-render/issues" + }, "license": "Apache-2.0", "engines": { "node": ">=18" @@ -16,7 +23,9 @@ "package.json", "LICENSE", "THIRD-PARTY-NOTICES.md", - "README.md" + "README.md", + "docs/SUPPORTED-PROPS.md", + "docs/SUPPORTED-VIEWS.md" ], "scripts": { "start": "webpack serve --mode development --config webpack.demo.config.mjs", diff --git a/scripts/generate-wrapper-prop-reference.js b/scripts/generate-wrapper-prop-reference.js index 9343e975..2fd44465 100644 --- a/scripts/generate-wrapper-prop-reference.js +++ b/scripts/generate-wrapper-prop-reference.js @@ -1007,21 +1007,18 @@ function renderMarkdown (reference) { '| --- | --- | --- |', '| **consumed** | the component, or its caller in the engine, reads it | nothing — we already own the behaviour |', '| **stripped** | removed at the DOM boundary by `' + DOM_PROPS_FILE + '` | only the boundary moves |', - '| **forwarded** | handed to semantic-ui-react, which decides what happens | everything: this is the parity risk |', + '| ~~**forwarded**~~ | ~~handed to semantic-ui-react, which decides what happens~~ | **no longer a fate.** The exit completed, so nothing is handed anywhere but to code in this repository |', '| **dropped** | semantic-ui-react handled it; the in-house component deliberately does not | already happened — this is the semver record for that step |', '', 'A checklist that mixed them would be full of props that never mattered. Each section below', 'is split that way.', '', - 'The forwarded set is tiered, because "supported" and "used" are different facts:', - '', - '- **tier 1 — exercised.** Used by a node in the tracked example corpus, by one of the', - ' consumer metas audited in step 0, or generated unconditionally by the wrapper. A', - ' regression here is a live bug.', - '- **tier 2 — published but unexercised.** Reachable through the wrapper API or by', - ' passthrough, but no meta in either corpus uses it. These cannot be dropped *silently* —', - ' they are documented propTypes — but reimplement-vs-deprecate is a decision for the step', - ' PR, not an automatic obligation.', + 'The forwarded set **was** tiered, because "supported" and "used" are different facts: tier 1', + 'meant a node in the tracked corpus or an audited consumer meta exercised it, tier 2 that it', + 'was published but unexercised. Both are retired with the set itself — tiers ranked a PARITY', + 'CHECKLIST, and there is no second implementation left to reach parity with. The distinction', + 'they encoded did its job: every tier-2 prop on the dropdown was resolved as a deliberate', + 'removal rather than a silent one, which is what the `Dropped` tables below record.', '', '## Isolation invariant', '', @@ -1126,9 +1123,11 @@ function renderMarkdown (reference) { '', 'Not guaranteed, and deliberately so:', '', - '- **the forwarded set is open.** A rest spread cannot be closed by static analysis; a meta', - ' may pass any semantic-ui-react prop. The tier-1 list is what was found, not a proof of', - ' what is possible.', + '- **the rest spread is still open.** A rest spread cannot be closed by static analysis, so a', + ' meta may pass a name nobody anticipated. What changed with the exit is where it lands: not', + ' into semantic-ui-react, but onto a DOM element, or into a component\'s own dropped-prop', + ' list where it is stripped and warned about once. The inventories below are what was found,', + ' not a proof of what is possible.', '- **the emitted className strings.** The generator reads which props a component consumes,', ' not what it composes them into. `ui table` surviving on the root is asserted by', ' `src/core/components/__tests__/Table.test.js` and by the 38-example DOM baseline, not here.', diff --git a/scripts/wrapper-prop-curation.js b/scripts/wrapper-prop-curation.js index d56d5e2b..459bcafa 100644 --- a/scripts/wrapper-prop-curation.js +++ b/scripts/wrapper-prop-curation.js @@ -797,14 +797,14 @@ const STEP_OBLIGATIONS = [ ], }, { - step: 'Step 3 — `Dropdown`', - effort: 'L (unchanged size, different location)', + step: 'Step 3 — `Dropdown` — SHIPPED', + effort: 'L (came in at the L; the matrix was the cost, as predicted)', items: [ - 'The L is NOT in `search`/`multiple`/`allowAdditions`/`clearable` — nothing uses them. It is in the keyboard/a11y matrix, the cascading flows, `upward`\'s auto-flip, and the `.ui.selection.dropdown` CSS contract.', - 'Keep `displayName = \'Dropdown\'` AND the named-vs-default export split: `modules/form/utils.js` branches on `InputComponent.displayName`, and only the named export carries it — `React.memo(...)` does not.', - 'Reproduce SUIR\'s aria shape: `role="listbox"` (or `combobox` under `search`), `aria-expanded`, `aria-disabled`, `tabIndex=-1` when disabled. SUIR renders no hidden native input, so the form binding is entirely react-final-form.', - 'Decide tier 2 explicitly: reimplement or deprecate. They are published propTypes/JSDoc, so they cannot be dropped silently — but they are not evidence for an L estimate either.', - 'Owed to this page before the swap: a measured `classContract` and `behaviourContract`, the way step 2 part 1 produced them for `TooltipPop`. Both fields are optional in the curation only because they have not been measured for `Dropdown` yet — an absent one means unmeasured, not "no contract", and `.ui.selection.dropdown` is known to be load-bearing.', + 'The L was NOT in `search`/`multiple`/`allowAdditions`/`clearable` — nothing used them, and they were REMOVED rather than reimplemented. It was in the keyboard/a11y matrix, which had to be built rather than ported: `Home`/`End`, `PageUp`/`PageDown` and typeahead were measured ABSENT from the library, so there was nothing to preserve and everything to write.', + 'MET: `displayName = \'Dropdown\'` and the named-vs-default export split both survive — `modules/form/utils.js` still branches on `InputComponent.displayName`, and only the named export carries it.', + 'MET, and one addition: `role="listbox"`, `aria-expanded`, `aria-disabled` and `tabIndex=-1` when disabled are all emitted. `aria-disabled` was MISSING in the first draft and caught by the behavioural suite — a `role="listbox"` div cannot carry the native attribute, so being unavailable has to be said three ways. Added beyond the library: `aria-activedescendant`, which is how the keyboard cursor is announced now that moving no longer commits.', + 'DECIDED: tier 2 was resolved as REMOVAL, on the evidence that nothing in either corpus or the consumer-only record declares any of them. Not silently — the removed names are stripped at the boundary and warn once each in development, and the `Dropped` table below is the record.', + 'DELIVERED: both `classContract` and `behaviourContract` are measured and present above. The class contract is pinned token by token in `src/style/__tests__/css.dropdown-contract.test.js` (what each class is worth in scoped rules), and the behaviour contract is the WAI-ARIA listbox model — arrows move a cursor, Enter commits, Escape reports nothing.', ], }, ] diff --git a/src/demo/markdowns/changelog.md b/src/demo/markdowns/changelog.md index 24baf041..8325226b 100644 --- a/src/demo/markdowns/changelog.md +++ b/src/demo/markdowns/changelog.md @@ -112,6 +112,14 @@ does. In the demo example set this removes 57 dangling references; no other rendered output changes. +#### Documentation + +- **The prop reference now ships with the package.** `docs/SUPPORTED-PROPS.md` and + `docs/SUPPORTED-VIEWS.md` are in the npm tarball, not just on the site. This release removes + published props, and the list of what went and why should be reachable from an installed copy + rather than only from a URL. The manifest also gained `repository` and `bugs`, so npm links to + the source and to somewhere you can tell us the removals hurt. + #### Dependencies - **Semantic UI is gone entirely — and nothing about how anything looks has changed.** The last @@ -145,15 +153,17 @@ It is green today, which is the first real evidence, but the claim comes with the release that makes it. - Nothing about the rendered output or the meta contract changes here. The Semantic **styles** are - still loaded (that is a separate package, `semantic-ui-less`, and a separate step), which is why - components still emit class names like `ui selection dropdown`. + Nothing about the rendered output or the meta contract changes here. The Semantic **styles** were + still loaded when this entry was first written — that was a separate package, `semantic-ui-less`, + and a separate step. It has since been done too (see *Dependencies* above): the two stylesheets + still in use were moved into this repository and that package is gone as well. Components keep + emitting class names like `ui selection dropdown` because our own CSS now selects on them. #### Styling - **The stylesheet is 67 KB smaller, and nothing about how anything looks has changed.** Three Semantic UI CSS modules were being compiled into every build for components this library no - longer renders: the popup (our tooltip has been in-house for two releases), the label (it styled + longer renders: the popup (our tooltip is in-house as of this same unreleased set), the label (it styled multi-select chips, and multi-select is gone), and the menu (it was kept "for the Pagination component", which turned out to be untrue — pagination has always been styled by our own CSS). @@ -258,6 +268,20 @@ hoverable in practice). The tooltip also gained `role="tooltip"` and `aria-describedby`, and it is now positioned next to its trigger — it was rendering at the document origin and throwing on every open. See `docs/SUPPORTED-PROPS.md` for the per-prop record. + + **If you pass an object `tooltip` attribute, its accepted surface narrowed from 45 names to 13.** + Those 45 were never a designed API — they were whatever `semantic-ui-react`'s `Popup` and `Portal` + happened to accept, reachable because the attribute was spread straight through. The 13 that + remain are the ones with a defined meaning here; of the rest, the 18 that were both reachable and + plausible warn once each in development rather than being ignored silently, and the remainder had + no observable effect to lose. + + **One accessibility trade, stated rather than buried:** the bubble is not hoverable, which is a + known non-conformance with WCAG SC 1.4.13 ("Content on Hover or Focus"). The other two parts of + that criterion are met — Escape dismisses it, and it stays while the pointer rests on the trigger. + It cannot be made hoverable without `pointer-events` on the bubble, and that was measured to make + the trigger itself unusable. The trade is recorded here, in `docs/SUPPORTED-PROPS.md` and in the + plan rather than left for you to discover. - **The equivalent list for `Table` is settled**: `celled`, `textAlign` and `as` were dropped (see "Table rendering" above), while `striped` and `inverted` were kept. @@ -284,7 +308,7 @@ paths resolve to `/static/images/` — and `dist/static/all.css` and `font.css` became one-line `@import` re-exports of it. Both import paths keep working; `semantic.css` remains an empty stub in both places. -- The tarball dropped from 579 files / 11.6 MB unpacked to 295 files / 7.25 MB (2.53 MB packed). +- The tarball dropped from 579 files / 11.6 MB unpacked to **299 files / 6.7 MB (2.5 MB packed)**, remeasured 2026-09-15 after the Semantic exit completed. Source maps continue to ship for host debugging. #### Tests and CI