Skip to content

feat!: rebuild ui-common on Astryx (0.2.0) - #53

Merged
yomybaby merged 89 commits into
mainfrom
feat/astryx-foundation
Oct 2, 2026
Merged

yomybaby merged 89 commits into
mainfrom
feat/astryx-foundation

Conversation

@yomybaby

@yomybaby yomybaby commented Sep 29, 2026 •

Copy link
Copy Markdown
Member

Summary

0.2 turns @lablup/ui-common from a set of its own components on a --token-* contract into Lablup's thin layer over Astryx 0.6.2.

  • Generated 1:1 mirror of @astryxdesign/core 0.6.2. Every core subpath exists under the same name (@lablup/ui-common/Button, @lablup/ui-common/theme/tokens.stylex, @lablup/ui-common/astryx.css, @lablup/ui-common/locales/<locale>.json); lab is @lablup/ui-common/lab, the neutral theme @lablup/ui-common/theme/neutral. scripts/gen-exports.mjs writes the surface and a drift test guards it. Astryx stays external and exact-pinned (core, theme-neutral, cli as dependencies; lab as an optional exact peer).
  • Exclusions and forks (exports.exclude.json): Dialog is replaced by Modal, AlertDialog by AlertModal. ComplexSelector and lab's Drawer and Tour ship as fixed forks under Astryx's own names and import paths, so consumers get the fixes without pnpm patches.
  • Lablup brand as an Astryx theme: theme/lablup (source), theme/lablup/built, theme/lablup/theme.css.
  • The --token-* contract is retired. legacy-tokens.css is a deprecated bridge that declares all 122 0.1 names inside @layer ui-common. Custom properties follow Astryx naming with no --uic- prefix (alpha.12).
  • i18n catalog on Astryx's translator: @lablup/ui-common/i18n-catalog plus ui-common-locales/<locale>.json; built-in strings are uic.<Component>.<key> resolved through Astryx InternationalizationProvider, translated in 20 locales besides English.
  • Components
    • Removed (replaced by Astryx through the mirror): Badge, BaseCard, Button, DataTable, Drawer (0.1), EmptyState, ProgressBar, Select, Skeleton (base shape), StatusTag, Tabs, Tooltip.
    • Rebuilt on Astryx with their 0.1 props: PageHeader, PageLayout, StatCard, ErrorState, SmoothHeight, DigitPopIn, the Skeleton composites.
    • Added: Modal, AlertModal, DeleteConfirmModal, ConfirmPopover, StepNumberInput, CountBadge, DoubleBadge, BooleanToken, DoubleToken, TokenList, TokenRow, IconWithTooltip, ImageWithFallback, OverlayScrollbar, NotificationStack, NotificationItem, SelectionLabel, UncontrolledInput, BoardItemTitle, Statistic, DividedRow, UnitGrid, ColorPicker, BulkEditFormItem, BulkErrorModal, DataGrid, ProgressWithLabel, TextHighlighter, CountdownBorder, ListBanner, PagedSelector — moved in from backend.ai-ui with Astryx-shaped props.
  • Form engine: @lablup/ui-common/Form (Form, Form.Item, Form.List, Form.useForm, validation rules, FormConfigProvider); DOM hooks are .uic-form-item* and data-uic-field-id.
  • DataGrid: Astryx-shaped table (data, idKey, renderCell, sort, page/totalItems, columnSettings with keyboard-reorderable drag handles, csvExport, expansion).
  • ui-common CLI (bin/ui-common.mjs): wraps the Astryx CLI with ui-common import paths, plus upgrade (0.1 → 0.2 codemods driven by migration/0.1-to-0.2.json, then Astryx's own codemods), agents (writes the UI-COMMON agent block) and sync-astryx (the maintainer's Astryx bump).

Decisions

  • lablup/ui-common #40 — dependency form, theme, cascade layer.
  • lablup/ui-common #41 — admission rule; consumers never import @astryxdesign/* directly.
  • lablup/ui-common #42 — the ui-common CLI.
  • lablup/ui-common #52 — the upgrade tool.
  • Jira: FR-4046 (map), FR-4088.
  • Consumer side: backend.ai-webui ADR 0009 (docs/adr/0009-ui-common-as-the-single-entry-point-to-astryx.md), in the webui counterpart PR.

Breaking changes for existing consumers

Every change is listed in CHANGELOG.md (0.2.0-alpha.0 through alpha.14) and, in machine-readable form, in migration/0.1-to-0.2.json. Consumers migrate with:

pnpm dlx @lablup/ui-common-cli@next upgrade --from 0.1

Headline breaks: React 19 only; @stylexjs/stylex ^0.19 is a new peer; the root barrel is Astryx's; the 0.1 components listed above are gone; --token-* is deprecated (bridged by legacy-tokens.css); class names moved to uic-.

Reviewer guide

Commits are in order (git log --oneline --reverse origin/main..feat/astryx-foundation, 73 commits):

# Range Group
1–8 660f885..18b0a53 Foundation: Astryx 0.6.2 dependency, generated export mirror, Lablup theme, ui-common.css + legacy-tokens.css, i18n catalog, Astryx CLI integration, fixture, README/CONTRIBUTING
9–14 d97161e..30be71e Components: rebuild the kept ones, remove the replaced ones, add Modal, ko-KR/ja-JP catalog
15–23 872ba80..3be542c CLI and upgrade: codemods and runner, the ui-common bin, migration map (includes two branch merges and alpha.1)
24–40 65f5fb2..b311ea6 Moves from backend.ai-ui by cluster: free leaves, i18n leaves, AlertModal/DeleteConfirmModal/StepNumberInput, AlertDialog hidden, BoardItemTitle/Statistic/DividedRow/TokenList/TokenRow/NotificationItem, UnitGrid/ColorPicker (alpha.2–alpha.6)
41–52 67c3fe3..161c31c Review fixes: codemod scope/SCSS/specifier fixes, upgrade write safety and --dry-run, Modal makes the page behind it inert and sets aria-modal, focus return (alpha.7–alpha.8)
53–54 02d5af6..95c4ae6 Form engine and BulkEditFormItem (alpha.9)
55–56 d42eccc..479c39f DataGrid, BulkErrorModal and five more components (alpha.10)
57–63 85abfd7..12e83e9 Review fixes: catalog button labels, DataGridSettingsModal keyboard reorder and announcements, select-all skips disabled rows, row index for getCellProps, Ant Design Icons licence text (alpha.11)
64–65 1017f34..f34cbf2 Drop the --uic- prefix from custom properties (alpha.12)
66–70 48bb2cf..45ca089 DataGrid flex-item fix, DoubleToken endContent, name-by-name lab mirror, ComplexSelector/Drawer/Tour forks (alpha.13)
71–73 fde887f..95f178a Drawer Escape through core's layer-dismissal stack, PagedSelector (alpha.14)

Skim only — generated: src/astryx/** (export mirror), the exports map in package.json, theme/lablup/built output, and the fork *.styles.ts files written by sync-forks. They are reproduced by the generators and guarded by drift tests; review the generators and exports.exclude.json instead.

Verification

pnpm run verify (typecheck, lint, format, boundary check, theme check, tests, build, pack check, integration check) on 95f178a:

 Test Files  73 passed (73)
      Tests  1338 passed | 2 skipped (1340)

Merge / publish conditions

  • Merged ahead of the webui cut-over as 0.2.0-alpha.15, published under the next dist-tag (both @lablup/ui-common and the new @lablup/ui-common-cli). latest/alpha keep pointing at the 0.1 line, and every 0.1 consumer pins an exact 0.1.0-alpha.N, so nothing upgrades implicitly.
  • 0.2.0 (moving latest) still waits for the webui cut-over (FR-4099).
  • Pre-merge hardening for 0.1 consumers landed via fix: harden 0.2 for existing 0.1 consumers (layer order, CLI package, upgrade gaps) #54; ready-gate review fixes are listed in the agent-review comment.

Follow-ups

Other consumers — backend.ai-go, continuum-hub, mlxcel, @lablup/ui-ai, @lablup/ui-charts — run ui-common upgrade --from 0.1 in their own repositories after 0.2.0 is published.

Pin @astryxdesign/core, theme-neutral and cli exactly as dependencies. Declare @astryxdesign/lab as an optional exact peer and @stylexjs/stylex ^0.19 as a peer. Raise the React peer floor to 19, which Astryx requires. pnpm-workspace.yaml carries the dev-install settings for the lab canary and the Astryx install scripts.
scripts/gen-exports.mjs mirrors every subpath of @astryxdesign/core 1:1 (lab under ./lab, theme-neutral under ./theme/neutral) as one-line re-export files in src/astryx, writes the root barrel as explicit named re-exports of core's root plus the customs in exports.customs.json, and writes package.json#exports. exports.exclude.json is the only way to hide a subpath; Dialog, docs.mjs and groups.doc.mjs are excluded. src/exports.test.ts regenerates in memory and byte-compares with the committed output, so every Astryx bump fails until the generator is re-run.

BREAKING CHANGE: the root barrel now exports Astryx's Badge, Button, EmptyState, ProgressBar, Skeleton and Tooltip; the old customs of those names stay at @lablup/ui-common/components/<Name> until 0.3. @lablup/ui-common/hooks is now the Astryx hooks mirror; usePrefersReducedMotion moves to the root barrel only.

check:pack now also fails when a packed module imports a package that is neither a dependency nor a peer, and when an Astryx locale catalog is not mirrored.
@lablup/ui-common/theme/lablup is a defineTheme source that extends neutral, seeds color.accent with #FF7A00/#DC6B03, carries the 0.1 status hues (info as the theme-local --uic-color-info, since Astryx has no info token) and names the 0.1 font family. astryx theme build compiles it into the committed theme/lablup/built JS and theme.css; pnpm run theme:check (part of verify) fails when they are stale.

The public-boundary disclosure scan no longer reads the theme/lablup path as a repository reference.
legacy-tokens.css declares all 122 0.1 --token-* names at :root inside @layer ui-common, each as the matching Astryx var() where one exists and as the old base.css literal otherwise, so libraries that only read tokens keep resolving. It is removed in 0.3. ui-common.css is the package's global sheet in @layer ui-common; today it carries the scrollbar rules from base.css, rewritten against Astryx tokens. A test asserts the 122-name coverage, the layering, and that every var() names a real Astryx token.

styles/base.css and styles/themes/*.css stay exported and are marked deprecated.
Custom components will resolve built-in strings through Astryx's InternationalizationProvider: an internal useUicTranslator() walks the provider's overrides and messages for the locale chain, like Astryx does, and falls back to the English defaultMessage in ui-common's catalog instead of the raw key. Keys are uic.<Component>.<key>; English lives in code and translations in src/i18n/locales/<astryx-locale>.json.

Consumers get @lablup/ui-common/i18n-catalog (uiCommonMessages, mergeMessages, uiCommonCatalog) and @lablup/ui-common/ui-common-locales/<locale>.json, with en.json written from the code catalog at build time. Astryx's own catalogs stay a 1:1 mirror at ./locales/*.json. The catalog is empty until the first component moves its strings in.

intl-messageformat becomes a direct dependency, on the same range core uses, for formatting the English fallback.
astryx.integration.mjs ships in the tarball with a ui-common reference topic (astryx docs ui-common) and four agentDocs lines, including "Use Modal, not Dialog". A consumer that depends on @lablup/ui-common picks it up without an astryx.config entry. There is no components root yet: the CLI requires each component doc to sit beside a same-stem .tsx, and the tarball ships no source.

pnpm run check:integration runs astryx doctor integration validate and astryx integration pack --check; verify and CI run it, and CI also runs theme:check. A test keeps the agent-doc lines within the CLI's 8-line, 240-character limits.
The fixture declares the canonical layer order, imports reset.css, astryx.css, the Lablup theme.css and ui-common.css through ui-common, wraps the app in <Theme theme={lablupTheme}>, and renders Astryx Button, Text and Card (root and mirrored subpath) beside the ui-common customs. It adds the @stylexjs/stylex peer.

check-fixture-styles now checks PageHeader (root) instead of the legacy Button, whose name is Astryx's now, and asserts that each Astryx sheet reached the consumer bundle. Verified locally against the packed tarball with both npm and a strict pnpm layout, where the fixture has no @astryxdesign/* of its own.
README covers the dependency form, setup (layer order, stylesheets, <Theme>), the mirrored surface and tokens.stylex, hidden subpaths, the name rule, strings, and what is deprecated. CONTRIBUTING covers the generator and exclusions, adding a custom export, admission, the boundary, strings, styling, the theme rebuild and the Astryx bump procedure. docs/astryx.md explains the architecture. CHANGELOG gains the 0.2.0-alpha.0 entry with the breaking changes.
PageHeader, PageLayout, StatCard, ErrorState, SmoothHeight, DigitPopIn and
the Skeleton composites keep their 0.1 props and are rebuilt on Astryx
primitives: Heading, Text, Button, IconButton, Icon, Card, ClickableCard and
Skeleton. Their styles move into @layer ui-common, read Astryx tokens only,
and use uic- BEM class names (page-header becomes uic-page-header).

Built-in strings (PageHeader's Retry and Dismiss labels, the composites'
loading names) now resolve through the ui-common catalog, with the explicit
prop still winning.

Tests that selected the old class names select the uic- ones. Tests that
read a declaration back through the cascade read the stylesheet source
instead, because jsdom drops @layer blocks. The token-contract test goes:
no component reads --token-* any more.

BREAKING CHANGE: the kept components' class names and DigitPopIn's tuning
properties gain a uic- prefix, and their markup is Astryx's.
Badge, BaseCard, Button, DataTable, Drawer, EmptyState, ProgressBar, Select,
the base Skeleton, StatusTag, Tabs and Tooltip are removed: source, tests,
styles and their ./components/<Name> subpaths. Each has an Astryx
replacement reached through ui-common (Badge/Token, Card/ClickableCard,
Button/IconButton, Table, lab Drawer, EmptyState, ProgressBar, Selector,
Skeleton, StatusDot, TabList, Tooltip). The Skeleton composites stay.

migration/0.1-to-0.2.json records, for the upgrade tool, every removed
import with its replacement, the prop renames and value maps a codemod can
apply, what needs manual review, the kept components' class renames, and
the stylesheet entry points. src/migrationMap.test.ts keeps it honest
against the package.

The component style checks move to src/components/componentStyles.test.ts:
one @layer ui-common per sheet, uic- classes, Astryx tokens only, no colour
literal, no focus rule. The focus-contrast and scrollbar tests keep checking
the deprecated styles/base.css and styles/themes sheets, which stay until
0.3 because existing imports of them must keep resolving.

BREAKING CHANGE: the twelve components and their
@lablup/ui-common/components/<Name> subpaths are gone. 0.2.0-alpha.0 had
announced their removal for 0.3.
Modal takes every Dialog prop, so a Dialog call site moves by changing the
specifier to @lablup/ui-common/Modal and Dialog/DialogProps to
Modal/ModalProps. It carries what backend.ai-ui's BAIDialog and BAIModal do
that is product-neutral:

- a document.body portal instead of the top layer, so layers above the
  modal band (a notification stack) stay visible and clickable; no
  aria-modal
- a shared level stack: a nested modal paints above its parent, only the
  topmost traps focus, covered roots go inert; Escape goes through Astryx's
  layer-dismissal stack, so a popover inside a modal closes alone
- trigger focus restore, title-based accessible name, drag-safe backdrop
  dismissal by purpose, theme name re-emitted on the portal root
- content mounted on first open and kept while closed, unmountOnClose to
  drop it, afterOpenChange on each edge
- with title, onAction or footer: a ModalHeader, the body and a footer
  with a primary action (pending while onAction's promise runs, via
  Button clickAction) and Cancel, labelled from uic.Modal.ok and
  uic.Modal.cancel

The z-index band defaults to 1100..10999 and moves with
configureModalZIndex; useModalLevel lets another portalled surface join
the stack. antd-shaped props (open, onOk, okText) stay out, for BUI's
adapter.

ModalHeader, ModalPosition, ModalPurpose and ModalVariant are Astryx's
Dialog parts under Modal names, and DialogHeader, DialogPosition,
DialogPurpose and DialogVariant are re-exported unchanged. The generator
now allows exactly that: the replacement named by an exclusion's
replacedBy may re-export the excluded subpath's names when they resolve to
Astryx's own declaration. It also fails when a replacedBy names neither a
custom nor a mirrored subpath.
ui-common-locales/ko-KR.json and ja-JP.json translate every uic.* key the rebuilt components and Modal use. The build still writes en.json from the code catalog. A test holds both files to the full key set, and PageHeader is checked against the shipped Korean labels through Astryx's provider.
The fixture drops the removed Drawer and renders StatCard from the root and Modal from its own top-level subpath, inside Astryx's InternationalizationProvider with uiCommonMessages from i18n-catalog. check-fixture-styles now asserts PageHeader's, StatCard's and Modal's sheets reach the consumer bundle next to Astryx's.
README lists ui-common's components with their subpaths, explains Modal and the Dialog-to-Modal rename, and replaces the 0.3 deprecation note for the removed customs with an upgrade table. CONTRIBUTING gains a component table, the reinstatement rule for an exclusion's replacement, and the styling rules the new style test enforces. docs/astryx.md and the astryx docs ui-common topic describe the component set. CHANGELOG records the removals, the rebuilt components and Modal.
`runUpgrade` runs the codemod steps registered between two ui-common
versions over a consumer's source (jscodeshift for scripts, postcss for
stylesheets), edits package.json, and writes ui-common-upgrade-report.md.
Edits happen in memory, so --dry-run changes nothing but the report.

The 0.1 -> 0.2 step:
- moves imports of the twelve removed customs, from the root barrel and
  from components/<Name>, to their Astryx counterpart under
  @lablup/ui-common/<X> (Drawer to /lab), keeping re-exported names;
- reshapes the props it can prove safe and leaves a
  TODO(ui-common-upgrade) marker on everything else;
- replaces styles/base.css with the layer order and the 0.2 stylesheet
  set, and drops the orange theme sheets;
- bumps @lablup/ui-common and adds @stylexjs/stylex, and
  @astryxdesign/lab when a Drawer moved;
- reports selectors, DOM hooks and tests on 0.1 class names, module
  mocks, and custom properties that collide with Astryx's.

Fixture projects reproduce the survey's consumer patterns; the tests
compare each upgraded tree with its expected output and check that a
second run changes nothing.
`ui-common` wraps the exact-pinned Astryx CLI:

- Any Astryx command passes through to the pinned bin, with its output
  rewritten to @lablup/ui-common paths and CLI invocations, and a note
  when it names a subpath ui-common hides (Dialog: use Modal). Exit codes
  are the child's; --json output stays valid JSON. Read-only lookups run
  in a symlinked shadow of the project when core is not reachable from
  it, so `component` and `search` work in a consumer that depends on
  ui-common alone. `ui-common astryx ...` runs Astryx unrewritten.
- `agents [--write <file>] [--check]` renders Astryx's agent block in
  memory, rewrites it, adds ui-common's rules, and keeps it between
  UI-COMMON markers that `astryx init` never touches.
- `upgrade` runs the registered codemods.
- `sync-astryx <version>` is the maintainer's pin bump inside this
  repository, recording the Astryx codemods consumers need under the next
  ui-common version (codemods/<version>/upstream.json).
- SelectOption maps to SelectorOptionData, the option object type.
  SelectorOption is Astryx's option component, so a type import of it
  would not type-check as the 0.1 option shape.
- Button title becomes tooltip: Astryx Button omits the title attribute
  and takes a tooltip string.
- movedExports records usePrefersReducedMotion moving from
  @lablup/ui-common/hooks (Astryx's hooks barrel from 0.2) to the root.
The codemods now read the replacement imports, type renames, prop
renames, value maps, required packages, stylesheet entry points and
manual notes from the migration map, and codemods/0.2/mapping.json is
gone. TODO markers quote the map's manual notes where one fits, and the
report lists the manual notes of every removed component a run met.

The class report now covers kept components too: a selector, DOM hook or
test on a 0.1 class of PageHeader, StatCard, the Skeleton composites and
the rest shows the uic- name the map's class-rename rule gives it.

The map ships in the tarball.
The integration manifest gains a `components` root, so
`astryx component Modal` (and `ui-common component Modal`) documents
ui-common's own components.

The CLI pairs every `{Name}.doc.mjs` with a same-stem `{Name}.tsx`, the
manifest cannot point that source elsewhere, and `doctor integration
validate` fails a doc without one. ui-common ships no source, so each doc
sits beside a one-line `.tsx` re-exporting the component; a test checks
the pairing. Consumers never run validate (discovery works from the doc
alone), but ui-common's own check does, and a stub keeps it meaningful.

The passthrough no longer appends the "not Dialog" note when the command
itself asks about Modal.
Seven product-neutral components that backend.ai-ui ships today move
here, each built on Astryx primitives with Astryx-shaped props and its
tests beside it:

- CountBadge: a count or a dot overlaid on its child's corner (Astryx
  Badge has no anchored form), with `max` overflow, `isZeroShown`,
  `offset`, `size` and a named live region.
- DoubleBadge: a run of Badges welded into one chip.
- BooleanToken: an on/off value as a Token, with a fallback for a value
  that is not a boolean.
- IconWithTooltip: a glyph in an unstyled, focusable button, named by
  its Tooltip's text.
- ImageWithFallback: an img that renders a fallback node once it fails.
- OverlayScrollbar: a persistent, draggable thumb drawn over a scroll
  container in place of its native bar.
- NotificationStack: floating Banner notices with task progress,
  Cancel/Retry and an action, auto-close that pauses on hover and focus,
  and enter/exit motion.

Their strings are catalog keys (uic.BooleanToken.*,
uic.NotificationStack.*) with ko-KR and ja-JP translations. Host
couplings in the origin became custom properties with neutral defaults:
--uic-overlay-scrollbar-z, --uic-notification-stack-z (default one above
Modal's band) and --uic-notification-stack-inset-top.
README, CONTRIBUTING and the ui-common docs topic list them, and each has a component doc for the Astryx CLI (`ui-common component CountBadge`), beside its one-line re-export.
The CLI, upgrade tool and CLI component docs landed after the 0.2.0-alpha.1 tarball was cut, so their entries move from the alpha.1 section to alpha.2, next to the components moved from backend.ai-ui. alpha.1 keeps what it shipped.
OK, Cancel, Confirm and Retry translate the same whichever component shows
them, so they are one catalog key each instead of one per component:

- uic.Modal.ok -> uic.common.ok
- uic.Modal.cancel, uic.NotificationStack.cancel -> uic.common.cancel
- uic.NotificationStack.retry, uic.PageHeader.retry -> uic.common.retry
- uic.common.confirm is new.

The English and the ko-KR/ja-JP text are unchanged. A consumer that
overrides one of the old keys has to use the new name. Catalog keys are
now uic.<Component>.<key> or uic.common.<key>.
Three product-neutral components that backend.ai-ui ships today move
here, on Astryx primitives with Astryx-shaped props and tests beside them:

- ConfirmPopover: a one-click confirmation on Popover for reversible
  actions, with the Modal/AlertDialog action vocabulary (onAction,
  actionLabel, actionVariant, isActionDisabled, cancelLabel). Cancel
  takes focus first, focus returns to the trigger, and the render-prop
  trigger stays a direct child of a ButtonGroup.
- SelectionLabel: "3 selected" with an optional clear button.
- UncontrolledInput: a TextInput, or a NumberInput for type="number",
  that commits on Enter and on blur only. The number branch now commits
  the value just entered; the origin read its draft before NumberInput's
  onChange had re-rendered it and committed the previous value.

Their strings are uic.common.confirm/cancel,
uic.SelectionLabel.{selectedCount,clear} and uic.UncontrolledInput.label,
as ICU messages ("{count} selected").

ui-common's catalog is translated into every language backend.ai-ui
ships, carried over from its locale files (i18next "{{count}}" becomes
ICU "{count}"): 18 new locale files next to ko-KR and ja-JP. id-ID,
mn-MN, ms-MY and th-TH have no Astryx catalog; they are the names
backend.ai-ui hands Astryx's provider. The catalog test now requires every
key in every locale file, except an explicit allowlist of the ten keys
whose origin had no translation outside ko/ja, and fails an allowlisted
pair once it is translated. It also checks each translation parses as ICU
and keeps the English placeholders.
README and the CLI topic list ConfirmPopover, SelectionLabel and UncontrolledInput. CONTRIBUTING says when a string is a shared uic.common key and when a component key, that messages are ICU MessageFormat with no markup, which locale names the files use, and that every key is translated in every locale file or allowlisted.
…x's name

An exports.exclude.json entry can now hide single names of a subpath ("exports": [...]) instead of the whole of it; the mirror file is then written as explicit named re-exports minus those names, plus the custom that replaces them. lab is always written that way, since Astryx ships it as one namespace with no per-component subpath to exclude. The runtime cross-check that guards core's root now covers these mirrors too.

A custom may keep an Astryx name only as a fork: its Astryx subpath (or names) excluded with replacedBy naming it, a "fork" field naming the Astryx module, and exactly the names Astryx's exports. Only a core fork may take the excluded Astryx subpath itself.
A product carried these fixes as pnpm patches on @astryxdesign/core and @astryxdesign/lab. patchedDependencies never reach a dependency's consumers, so ui-common now owns fixed copies under Astryx's own names and import paths:

- ComplexSelector (@lablup/ui-common/ComplexSelector, and the root): hasClear / onClear, a clear button as Selector has (facebook/astryx#6362).
- Drawer (@lablup/ui-common/lab): Escape acts only when it happened in the drawer's own DOM subtree and did not end an IME composition, and a consumer's aria-modal passes through.
- Tour, TourStep, useTour (@lablup/ui-common/lab): a step's highlight is promoted into the top layer once, never hidden and re-shown, so StrictMode no longer puts the spotlight dim over the callout.

ui-common has no StyleX compile step, so a fork reuses the StyleX objects Astryx's own build compiled for the pinned version, copied from dist/ into a generated <Name>.styles.ts by scripts/sync-forks.mjs. The forks render upstream's atomic class names, whose rules already arrive with astryx.css and lab/lab.css in Astryx's layer: no CSS of their own. Markup-parity tests render each fork next to Astryx's component and compare them.

src/forks/provenance.json records the version and a SHA-256 of every upstream file each fork uses; forks.test.ts fails on any Astryx bump until the fork is re-synced or deleted. Each fork also tests that Astryx's own component still lacks the fix, so the test that fails first once upstream ships it says to delete the fork. Upstream's tests run against the forks; two Drawer tests that read StyleX-injected CSS are skipped (they fail the same way against lab's Drawer here). The copied Astryx code is MIT; NOTICE carries its licence and check:pack requires it.

The agent block and the CLI passthrough notes say these come from ui-common, same API, rather than telling the reader to use a different name.
Lab's Drawer handled Escape in its own element-level onKeyDown and
preventDefault()ed it before core's document-level layer stack saw the
press. A popover, selector or complex selector opened inside the drawer
lives in its DOM subtree, so one Escape closed both the layer and the
drawer. Nested drawers closed the outer one first, because child effects
register before parent effects in the LIFO registry.

The fork now joins the stack the way core's Dialog does:
useLayerDismissal({ isActive: isOpen, escapeBehavior: 'close' }), content
wrapped in LayerDepthProvider, and the native cancel answered only when
shouldDismissOnCloseRequest() says this drawer is the top-most layer and
no IME composition is running. The LIFO registry remains for non-modal
z-indexes only. A non-modal drawer now closes on Escape wherever focus
is, as the top-most layer, instead of only while focus is inside it; the
one upstream test that asserted the old rule is changed and marked.

Recorded as a deliberate divergence in provenance.json (notes) and in
CONTRIBUTING's fork section.
The product-neutral part of Backend.AI WebUI's BAIComplexSelect: a
selector, single or multiple, whose options load a page at a time.
Scrolling the panel near its end calls onEndReached (once per arrival,
within endReachedThreshold px), search is reported per keystroke through
onSearchChange, and the foot shows the total count with a spinner while
the next page loads. Built on ui-common's ComplexSelector; the panel is
drawn like Astryx Selector's.

Props are Astryx-shaped: value holds option values (string | null, or
string[] with isMultiple), onChange also hands over each chosen value's
{ value, label }, and a selected value missing from the loaded options is
named from `labels`, then from any page it was seen on. options[] take
isDisabled, endContent, labelContent, icon and description; hasClear /
onClear, emptyText, header / footer, triggerDisplay, maxTriggerItems and
selectionIndicator follow MultiSelector's and Selector's vocabulary.

- The search row is Astryx's PanelSearchInput, which core does not
  export, adapted with its compiled class names and its interaction-
  modality store (shared through the same document symbol). No
  stylesheet, and the keyboard-only focus ring stays Astryx's.
- ComplexSelector's content inset goes through contentXstyle as Astryx's
  compiled padding:0 class, so no StyleX compiler and no selector into
  ComplexSelector's markup. A test fails if astryx.css stops defining any
  borrowed class.
- Strings are uic.PagedSelector.* (placeholder with {label}, searchOptions,
  searchPlaceholder, clearSearch, noResults, loading, totalItems with
  {total}), each also a prop, translated in all 20 locale files from the
  product's existing translations.
- Option rows, list and foot are uic- classes in @layer ui-common.

The popup and trigger tests are ported from the product onto the new
props, plus paging, multiple-selection, label-resolution and clear tests.

Also updates the ui-common docs topic's Drawer line for the previous
commit.
Records the problems the Backend.AI WebUI hit moving onto the Astryx-based ui-common (duplicate core from the lab canary peer, patches that do not travel, the Vite cold-start hang, Vitest and theme-build CSS imports, layer order, inert background under Modal, Escape through the layer stack, i18n wiring) with the fix for each and a pre-merge checklist. Linked from the README's upgrade section.
A git worktree under .claude/worktrees is another checkout of this
repository. ESLint walked into its dist, Prettier into its sources, and
Vitest ran its test files a second time, so `pnpm run verify` failed or
double-counted whenever one existed.
A tooltip is a layer on core's dismissal stack while it shows, so one
Escape closes it and the next closes the modal. A trigger that unmounts
while hovered (an inline rename's pencil) takes its tooltip's layer with
it: the modal owes that tooltip no press afterwards.

A consumer report read as a leaked layer turned out to be this routing:
the trigger remounted under a resting pointer, its tooltip showed again,
and took the press. These two cases keep the distinction checkable.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

React compatibility, notification lifecycle, accessibility, and public typing issues remain unresolved.

Review effort: Balanced
Findings: 3 High severity · 4 Medium severity

Open (7)
What changed in this PR

Rebuilds ui-common 0.2 as an Astryx-based compatibility and extension layer.

Changes:

  • Mirrors Astryx exports and adds Lablup themes, localization, and fixed forks.
  • Rebuilds components and introduces Form, DataGrid, modal, notification, and utility APIs.
  • Adds migration codemods, CLI integration, fixtures, and verification checks.
File Description
src/​astryx/​** Generated Astryx export mirror.
src/​components/​** Rebuilt and newly added components.
src/​forks/​** Fixed Astryx component forks.
src/​i18n/​** Catalog loading and translation helpers.
src/​theme/​**, src/​styles/​**, src/​ui-common.css Theme, token bridge, and global styles.
cli/​**, bin/​ui-common.mjs, codemods/​** CLI wrapper and 0.1→0.2 migration tooling.
astryx/​**, astryx.integration.* Astryx CLI documentation and integration metadata.
scripts/​**, exports.exclude.json Export generation, fork synchronization, and validation.
test/​upgrade/​fixtures/​** Migration input and expected-output coverage.
fixture/​** External consumer integration fixture.
package.json, pnpm-workspace.yaml, tsconfig.json Dependencies and toolchain configuration.
.github/​workflows/​ci.yml, eslint.config.js, .prettierignore CI and repository checks.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread codemods/0.2/package-json.mjs Outdated
Comment thread package.json Outdated
Comment thread src/components/NotificationStack/NotificationStack.tsx Outdated
Comment thread src/components/DoubleBadge/DoubleBadge.tsx Outdated
Comment thread src/components/DoubleToken/DoubleToken.tsx Outdated
Comment thread src/components/ImageWithFallback/ImageWithFallback.tsx
Comment thread src/components/NotificationStack/NotificationStack.tsx Outdated
…pendency has them

For a library, the presence check looked at every dependency field, so a
devDependency on @stylexjs/stylex (or on the lab canary) suppressed the
peerDependency the package needs. Check the peer and the devDependency
independently.
Modal, NotificationStack, UnitGrid and the Form's useWatch import
useEffectEvent, which React made stable in 19.2. The ^19.0.0 peer let a
19.0 or 19.1 install resolve, and those modules then failed to import.
A test now holds the peer floor to the React APIs the source imports.
The exit-prune effect depended on `visible`, a fresh slice every render
when maxVisible is set. Its own setExiting re-ran it, the cleanup
cancelled the 200 ms prune timer, and the re-run found nothing newly
removed, so the notice stayed mounted in its exiting state for good, its
buttons still in the tab order. Without maxVisible, any new notifications
array within the 200 ms did the same.

Memoize `visible`, and give each exiting key its own prune timer that a
re-render does not cancel; only unmount clears them.
…s still holds it

Hover and focus shared one isPaused flag, so moving the pointer off a
notice resumed its auto-close while focus was still inside it, and
blurring resumed it under a hovering pointer. Track the two separately
and pause while either holds.
`values` was Array<string> | Array<DoubleBadgeValue>, so the documented
example [{ label, variant }, "2m"] did not type-check although the
component renders it. The element type is now the union.
`values` was Array<string> | Array<DoubleTokenValue>, so the documented
example [{ label, color }, "12.4"] did not type-check although the
component renders it. The element type is now the union.
Once the image failed, the <img> and its alt were gone, so a meaningful
image lost its name and an icon that names itself could be announced in
place of a decorative one. The fallback now sits in a span that is an img
named by alt, or aria-hidden when alt is empty.
…raps children

Two rewrites made recast reprint a JSX parent: addTodo spliced a marker
into the parent's children, and Badge's children-to-label move built a
new fragment from them. Recast's reprint strips the leading whitespace of
every text child, so `{n} items` became `{n}items` and the text around
an element that got a TODO lost its spaces.

addTodo now leaves JSX-child markers pending on the element, and
printSource writes them into the printed text from a sentinel comment,
which reprints the element alone: a marker line above an element that
starts its line, or a marker right before one that does not, with no
whitespace added. A new fragment moves a text's first-line leading
whitespace into a {" "} child, which recast prints as is.
defaultOverrides stays merged under the user's overrides on every render,
and a key the user's record holds replaces the default's entry whole. The
settings dialog wrote `hidden` and `order` only where they differed from
the column's natural default, so re-showing a column the defaults hide
wrote {} and the default {hidden: true} came back, and restoring the
natural order over a reordering default did the same.

Compare against the effective default (natural, then defaultOverrides):
leave a column out only while its default already renders the user's
choice, write an explicit hidden where it differs from either, and state
every position while a default order is in play.
A scrimmed drawer opens its <dialog> with showModal(), which puts it in
the top layer and makes everything outside it inert. Modal portals to
document.body and never enters the top layer, so a Modal opened from
inside the drawer painted behind it and could not be clicked, focused or
typed into. jsdom stubs showModal and inert, so the fork's test passed.

The drawer now provides its dialog through ModalPortalContext once
showModal() has run, while it is open and modal. A Modal inside renders
into that dialog and enters the top layer as a manual popover, so it
paints above the drawer and escapes the panel's transform and clipping;
the drawer's content goes inert under it through the modal stack. A new
host remounts the Modal's surface, so its level claim, background and
popover follow the root element. Escape still closes the Modal first,
then the drawer, and focus returns to each opener. Checked in Chromium:
the Modal takes the hit test and typing, also when both open together.
@yomybaby

yomybaby commented Oct 2, 2026

Copy link
Copy Markdown
Member Author

Ready-gate agent review (Opus) — blocking issues only

# File:line Finding Outcome
O1 codemods/0.2/elements.mjs:217, codemods/lib/todo.mjs:80 ui-common upgrade strips leading whitespace from JSX text when it wraps Badge children or adds a TODO ({n} items → {n}items) Fixed in 403ccbd (TODO via sentinel comment, no parent reprint; fragment keeps {" "}); 5 tests
O2 src/components/Form/FormStore.ts:362 getFieldsValue drops list-row keys without a bound Form.Item Won't apply — the port follows @rc-component/form 1.8.5 (antd 6), whose useForm.js:208-253 matches line for line and always skips list entities; the rc-field-form 2.7.1 strict-only behaviour cited is antd 5's
O3 src/components/NotificationStack/NotificationStack.tsx:315 a closed notice stays mounted in exiting forever Fixed in aea5d96 (same as Copilot thread)
O4 src/components/DataGrid/DataGrid.tsx:436, :936 a column hidden via columnSettings.defaultOverrides can't be shown again; natural order can't be restored Fixed in 6874924; UI repro tests
O5 src/forks/Drawer/useDrawerDialogPresence.ts:85, Modal.tsx:455 a Modal opened inside a scrimmed Drawer is behind the drawer's top-layer <dialog> and inert Fixed in ff325e2: the open modal drawer provides its <dialog> as the Modal portal target and the Modal goes to the top layer; verified in Chromium (hit test, typing, Escape order, focus return, inert drawer body). Remaining limit: a Modal rendered outside the Drawer's React tree still sits behind it

Copilot: reviewed — 7 threads, all fixed (6ede03e, 120c6b8, aea5d96, e14aafd, 35b2926, 2ad7969, 13d9c6e).
Fixes landed after both reviews and were not re-reviewed; pnpm run verify passes on 598eddf (1370 tests).

… upgrade gaps) (#54)

Cascade layer order emitted by every shipped stylesheet; the CLI split into @lablup/ui-common-cli (packages/cli, lockstep, prereleases under next); pnpm fixture install in CI; dev-only duplicate-copy warning; upgrade codemod gaps found on six 0.1 consumers; docs.
@yomybaby
yomybaby marked this pull request as ready for review October 2, 2026 04:58
@yomybaby
yomybaby merged commit 9c67581 into main Oct 2, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants