feat(ui): add the d2e component library and the Histoire explorer - #3204
Open
khairul-syazwan wants to merge 33 commits into
Open
feat(ui): add the d2e component library and the Histoire explorer#3204khairul-syazwan wants to merge 33 commits into
khairul-syazwan wants to merge 33 commits into
Conversation
(cherry picked from commit 8f22ee68dd96ffd43f8105289599c311770b62e8)
(cherry picked from commit 2e095dec72fc364af0dcb1bfdc9d51e14d999596)
(cherry picked from commit e5a8dbb430e21d58c4c389cfa234ead2ae242631)
(cherry picked from commit 59aadc48cdf285d91eeabdc3e7369687e14fd01b)
…ollisions (cherry picked from commit 75414bea71e8840a6bc35d7318003d409f3067c2)
Library half of the original formatting commit. The application half goes with the dialog changes.
(cherry picked from commit 977b92b633cc47993eafd32780fcfad74e72ee8b)
(cherry picked from commit 268c408fa58447db64aac3493d7454b0a8f27bb8)
(cherry picked from commit 50d4ae4682c4b2c3273b3bb246373301b23a396b)
(cherry picked from commit 2bf1177dc6b87f17a66fb79248fe9ce587dfad58)
(cherry picked from commit 7f5bedd16d14b7b6b8fbdcf56a681c3f06bd7045)
The token generator added `px` to each number. This made `--d2e-font-*-weight` and the ratio `--d2e-font-*-line-height` values invalid, for example `font-weight: 600px` and `line-height: 1.2px`. The browser ignored both declarations. The effect was not visible in the dialogs, because each affected element holds one line of text. It was visible in D2eExplorationCard, where the metadata rows collapsed to 1.5 px and printed on top of each other. Keep `weight`, `lineHeight`, `opacity` and `zIndex` unitless. Add a test that fails if a weight or a ratio line height gets a unit again. (cherry picked from commit ca03e1f38574680e87d73a6038774b208e75cb8d)
The explorer was an untracked proof of concept. Track it, so that each developer can start it with the same dependency versions. Histoire needs vite 7. `plugins/ui/package.json` pins vite to 6.4.2 with an override, to hold libs/react-notebook at vite 6. Bun applies an override to all packages in the workspace. The workspace globs are `libs/*` and `apps/*`, which match one level only, so this directory is outside the workspace. It uses npm and its own lockfile, and it can therefore use vite 7. Also: - Align `vue` with apps/vue-mri-ui-lib at 3.5.17. `vuetify` already agreed at 3.12.0. The explorer must show what the application renders. - Add `.nvmrc` and `engine-strict=true`, because the explorer needs Node 20.19 or later and a default shell Node can be older. - Keep the lockfile in git. `plugins/ui/.gitignore` ignores each `package-lock.json`, so the explorer negates that rule. - Add `.prettierignore`, to keep `prettier --write .` away from the lockfile and the build output. (cherry picked from commit d849a464dfeb988e61d20c136a11cff7039c3217)
`histoire.config.ts` and `histoire.setup.ts` were in the library root, and the `ui:dev` and `ui:build` scripts started them. They cannot run, because histoire needs vite 7 and the workspace override pins vite 6. The working copies are now in `explorer/`. Two configurations, where one of them cannot run, is a trap for the next reader. This also removes an undeclared dependency problem: the deleted `histoire.setup.ts` imported `@mdi/font`, `@fontsource-variable/ibm-plex-sans` and `sass`, which this package never declared. They resolved only through bun hoisting. The explorer declares all three. (cherry picked from commit 59ede28af997fa5cb6b1e518cc699a3281adb204)
Give the library a README, and give the explorer a README with one command sequence that includes the Node version step. Record why the explorer is outside the workspace, and the two rules that keep it working. (cherry picked from commit 65560a77dcee6921c47006d506600c8ca19306aa)
Two properties are load-bearing and silent when they break: - The workspace globs match one level only. A deeper glob pulls the explorer into the workspace, where the vite 6 override stops Histoire. - The explorer must use the vue and vuetify versions of the application, or it shows something the application does not render. Assert both against the three package.json files. The test reads JSON only; it mounts no component. (cherry picked from commit 346ff0f147ae47caac80806d71c671aed12a4e01)
`@histoire/plugin-vue@1.0.0-beta.1` needs `vue ^3.5.26`, and apps/vue-mri-ui-lib pins `vue 3.5.17`. An exact match makes `npm ci` fail with ERESOLVE. Pin 3.5.26 and record the constraint in the README. The parity test already compares the major and minor version only, so it still holds. (cherry picked from commit 9e9bbbbc04f97c12b4ded963acc567264680e4c5)
Three gaps found by checking the library against the Figma design system (file lpbqxd8B0OkRKH9kJoCCEa) through the component explorer. **Modal scale.** The Figma variables are Modal/S 540, Modal/L 900 and Modal/XL 1200. There is no Modal/M, and 600 appears nowhere in the design system, but `tokens.ts` carried `md: 600` and D2eDialog defaulted to it. Drop the invented size, add a `size` prop over the real scale, and default to Modal/S — the rename, delete and materialize frames are all Modal/S. `maxWidth` stays as an escape hatch. A test locks the scale. **Button icons.** The Figma Button set has a `Sub category` axis of Default / Icon front / Icon back. D2eButton had none of it. Its `icon` prop forwarded to `v-btn`'s `icon`, which makes an icon-only round button and drops the label, so the "with icon" story rendered no label at all. Replace it with `prependIcon` and `appendIcon`. Use D2eIconButton when an icon-only control is wanted. **Scrim dismissal.** MODAL CLOSE BEHAVIOR (2106:162) says clicking the overlay must not dismiss a modal, for every modal type. The dialog was dismissable. `v-dialog` is now always persistent, and Escape is handled directly so informational modals still close on it. `closeOnEscape` turns that off for long forms and multi-step flows, which the same frame says must raise a confirm-discard step instead. The `persistent` prop is gone: the overlay never dismisses now, so the only remaining axis is Escape.
The single "sizes" story showed one dialog at one size, so the scale was not demonstrable — the sizes looked like they did nothing. Give each size its own variant. The `size` prop was in fact working; the story sandbox was fixed at 720 px, so Modal/L (900) and Modal/XL (1200) both clamped to the pane and rendered identically. Setting `responsive-disabled` on the story lets the preview scale with the window: S renders at 540 everywhere, L reaches 900 and XL reaches 1200 on a wide window, and both clamp below that — the same thing a real browser does. Each variant states its max-width. Also turn off the Vuetify ripple on D2eButton, D2eIconButton and the dialog close button. The Figma button set defines discrete Hover, Focused and Pressed states and no ripple animation.
… build The patient-analytics sub-plugin is installed in CI with `npm install --workspaces=false` inside apps/vue-mri-ui-lib, which resolves @d2e/ui from the registry and 404s on the private package. Alias @d2e/ui to libs/d2e-ui/src/index.ts and @d2e/ui/tokens.css to libs/d2e-ui/src/tokens/tokens.css in the three vite configs and in tsconfig, and drop the dependency. This keeps the source-export model and works for both bun and npm.
khairul-syazwan
requested review from
LSriragavan,
SantanM,
brandantck,
hengxian-jiang,
jerome-ng,
maggie-li-yd,
p-hoffmann and
suwarnoong
as code owners
August 26, 2026 03:03
khairul-syazwan
temporarily deployed
to
github
September 1, 2026 08:51 — with
GitHub Actions
Inactive
khairul-syazwan
temporarily deployed
to
github
September 1, 2026 08:51 — with
GitHub Actions
Inactive
khairul-syazwan
temporarily deployed
to
github
September 1, 2026 08:51 — with
GitHub Actions
Inactive
khairul-syazwan
temporarily deployed
to
github
September 1, 2026 08:51 — with
GitHub Actions
Inactive
khairul-syazwan
temporarily deployed
to
github
September 1, 2026 08:51 — with
GitHub Actions
Inactive
khairul-syazwan
temporarily deployed
to
github
September 1, 2026 08:51 — with
GitHub Actions
Inactive
The shared library carried application-specific naming. `@d2e/ui` is meant to serve the portal once it moves to Vue, and `mri-brand` reads as MRI-only to any other consumer. mri-brand -> brand mri-brand-hover -> brand-hover mri-contrast -> brand-contrast mri-info -> brand-info Values are unchanged. Deliberately **not** renamed to `primary`. The original plan gives two conditions for that, and only one is met. The Figma extraction is done, but Bootstrap 4.6.1 is still imported inside `.mri-app-vue-container` (styles/style.scss:11), so `primary` would still collide: Bootstrap's scoped `.bg-primary`/`.text-primary` beat Vuetify's, and Bootstrap 4 defines no `--bs-primary-rgb`, so the declaration is invalid at computed-value time and the button renders transparent. `brand` is collision-free because Bootstrap's `$theme-colors` has no such entry. Rename to `primary` once Bootstrap leaves the portal scope. Nothing outside the library consumed these keys — no `color="mri-brand"` props and no `bg-mri-*` utility classes. The `--color-mri-brand` CSS custom property in the application palette is a different namespace and is untouched. Verified in the explorer: primary renders rgb(0, 0, 128), secondary and ghost keep navy text, danger stays rgb(163, 41, 61).
khairul-syazwan
temporarily deployed
to
github
September 2, 2026 01:19 — with
GitHub Actions
Inactive
khairul-syazwan
temporarily deployed
to
github
September 2, 2026 01:19 — with
GitHub Actions
Inactive
khairul-syazwan
temporarily deployed
to
github
September 2, 2026 01:19 — with
GitHub Actions
Inactive
khairul-syazwan
temporarily deployed
to
github
September 2, 2026 01:19 — with
GitHub Actions
Inactive
khairul-syazwan
temporarily deployed
to
github
September 2, 2026 01:19 — with
GitHub Actions
Inactive
khairul-syazwan
temporarily deployed
to
github
September 2, 2026 01:19 — with
GitHub Actions
Inactive
The portal is moving to Vue and will share these components, so the library needs a real contract rather than a source export that every consumer compiles itself. - Build with vite lib mode into `dist/index.js` + `dist/index.css`, types via `vue-tsc`. Not the rollup/babel chain libs/portal-components uses: that is rollup 2 plus six babel packages for React/TSX, and vite handles Vue SFCs natively. - `vue` and `vuetify` stay external as peer dependencies. - Components now import the Vuetify pieces they use, so the package works without the consumer running vite-plugin-vuetify. - Entry points move to dist; `./tokens.css` stays on source because it is generated from tokens.ts and guarded by tokens:check. - `scripts/verify-dist.mjs` checks the artifact in CI. Two things this surfaced: Adding the Vuetify imports broke three unit tests, which imported their constant maps from the `.vue` files and so began loading Vuetify's CSS under vitest's node environment. Moved those maps into plain modules (`buttonVariants`, `iconButtonSizes`, `statusChipVariants`, `explorationCardStatus`), following the existing `dialogSizes.ts` pattern. The public API is unchanged; tests no longer touch Vuetify. The obvious smoke test — importing dist in plain node — cannot work, because vuetify's ESM pulls in .css. `verify:dist` checks statically instead, and asserts *positively* that vue and vuetify are still imported: when a peer gets bundled it stops appearing as an import, so looking only for unexpected imports misses the case that matters. Verified by dropping vuetify from `external` — the check fails on both the missing import and a 197 kB bundle against the 150 kB ceiling. Verified: 25 lib tests, tokens:check, dist verify, all three vue-mri build variants, explorer 8 stories / 30 variants, and every component still renders in the browser with no console errors.
`verify-dist.mjs` checked 13 names while `src/index.ts` exports 17 values. The check is a subset test, so the four constant maps this branch added — VARIANT_MAP, SIZE_MAP, STATUS_CHIP_VARIANT_MAP and ICON_BUTTON_SIZE_MAP — went unguarded. Dropping or renaming any of them would not have failed the build. The guard now reports 17 exports. The README header still said the application uses the source and there is no build step, directly above the new section on consuming the built package. Say what is true of both: the application reads source through vite aliases, and every other consumer uses the artifact. Found in review of #3251.
khairul-syazwan
temporarily deployed
to
github
September 2, 2026 03:28 — with
GitHub Actions
Inactive
khairul-syazwan
temporarily deployed
to
github
September 2, 2026 03:28 — with
GitHub Actions
Inactive
khairul-syazwan
temporarily deployed
to
github
September 2, 2026 03:28 — with
GitHub Actions
Inactive
khairul-syazwan
temporarily deployed
to
github
September 2, 2026 03:28 — with
GitHub Actions
Inactive
khairul-syazwan
temporarily deployed
to
github
September 2, 2026 03:28 — with
GitHub Actions
Inactive
khairul-syazwan
temporarily deployed
to
github
September 2, 2026 03:29 — with
GitHub Actions
Inactive
7 tasks
- D2eDialog: card is now a flex column with a capped max-height and a scrollable body, so a long form or short viewport keeps the header/ actions visible instead of clipping content. - D2eTextField: disable attribute inheritance so a caller's listeners (blur, click, ...) aren't applied twice — once via forwardAttrs, once via Vue's default inheritance onto the same root. - D2eIconButton: require icon and ariaLabel. There's no content slot, so a call missing either produced a blank, unlabeled control. - D2eMenu: implement the roving-tabindex ARIA menu keyboard model (Arrow Up/Down, Home/End) the role="menu"/"menuitem" markup already implied but didn't back. - D2eExplorationCard: derive the checkbox's default accessible name from the exploration name instead of a shared "Select exploration" label. The other six comments on that review were already resolved by later commits on this branch (main.ts's theme class was removed outright, the two story docs and explorer/.gitignore's lockfile were fixed in 87be952 and 10aaf0a, and D2eExplorationCard's truncation/tooltip and "By:" prefix were fixed rebuilding it against the Figma design). Left the missing D2eDialog behavior tests out — the fix would require mounting the component in vitest, which the repo's testing policy rules out; that belongs in Playwright once the dialog is wired into an app.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Stack 1 of 4 for the D2E data-exploration redesign (#3116).
Adds the component library, packages it as a built artifact, and adds a tracked Histoire explorer. Replaces the legacy Atlas theme and wires the application to the design tokens.
Scope:
libs/d2e-uicomponent library (D2eDialog, D2eButton, D2eTextField, D2eStatusChip, D2eIconButton, D2eMenu, D2eCard, D2eToolbar, D2eExplorationCard) and design tokens.libs/d2e-ui/explorer.@d2e/uiinvue-mri-ui-libthrough a source-export alias so the CI atlas build resolves the library without publishing the private package.dist/index.jsanddist/index.css, types viavue-tsc, withvueandvuetifyexternal. Components import their own Vuetify pieces. Constant maps moved out of the SFCs into plain modules so tests do not load Vuetify.scripts/verify-dist.mjsguards the artifact in CI.#3251 was folded in rather than reviewed separately: it was 25 files and +323/-121 against this PR's +7448, and 8 of those files are components this PR introduces. Reviewed apart, those SFCs had to be read twice — once with the constant maps inline, then again with the maps extracted. The final state is clearer read once. The branch was fast-forwarded, not force-pushed, so no commit was rewritten.
Notes:
@d2e/uitests pass (25), app unit tests pass (906) with one inheritedapp-segmented-buttonfailure that passes in CI, the app and atlas builds pass,verify:distreports 17 exports with peers external, and the explorer builds and serves on port 6006.