feat!: rebuild ui-common on Astryx (0.2.0) - #53
Merged
Merged
Conversation
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.
This was referenced Sep 29, 2026
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.
There was a problem hiding this comment.
Copilot review overview
🟡 Changes recommended
React compatibility, notification lifecycle, accessibility, and public typing issues remain unresolved.
Review effort: Balanced
Findings: 3
Open (7)
Check peerDependencies separately from devDependencies · New useEffectEvent requires React 19.2, not 19.0 or 19.1 · New Prevent timer cancellation from stranding exiting notices · New Allow mixed shorthand and object values in the public type · New Type array elements as a union to allow mixed values · New Preserve image semantics and alt text in the load-error fallback · New Track hover and focus pause states independently · New
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.
…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.
Member
Author
Ready-gate agent review (Opus) — blocking issues only
Copilot: reviewed — 7 threads, all fixed ( |
… 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
marked this pull request as ready for review
October 2, 2026 04:58
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.


Summary
0.2 turns
@lablup/ui-commonfrom a set of its own components on a--token-*contract into Lablup's thin layer over Astryx 0.6.2.@astryxdesign/core0.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.mjswrites 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).exports.exclude.json):Dialogis replaced byModal,AlertDialogbyAlertModal.ComplexSelectorand lab'sDrawerandTourship as fixed forks under Astryx's own names and import paths, so consumers get the fixes without pnpm patches.theme/lablup(source),theme/lablup/built,theme/lablup/theme.css.--token-*contract is retired.legacy-tokens.cssis 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).@lablup/ui-common/i18n-catalogplusui-common-locales/<locale>.json; built-in strings areuic.<Component>.<key>resolved through AstryxInternationalizationProvider, translated in 20 locales besides English.Badge,BaseCard,Button,DataTable,Drawer(0.1),EmptyState,ProgressBar,Select,Skeleton(base shape),StatusTag,Tabs,Tooltip.PageHeader,PageLayout,StatCard,ErrorState,SmoothHeight,DigitPopIn, the Skeleton composites.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.@lablup/ui-common/Form(Form,Form.Item,Form.List,Form.useForm, validation rules,FormConfigProvider); DOM hooks are.uic-form-item*anddata-uic-field-id.data,idKey,renderCell,sort,page/totalItems,columnSettingswith keyboard-reorderable drag handles,csvExport,expansion).ui-commonCLI (bin/ui-common.mjs): wraps the Astryx CLI with ui-common import paths, plusupgrade(0.1 → 0.2 codemods driven bymigration/0.1-to-0.2.json, then Astryx's own codemods),agents(writes theUI-COMMONagent block) andsync-astryx(the maintainer's Astryx bump).Decisions
@astryxdesign/*directly.ui-commonCLI.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, inmigration/0.1-to-0.2.json. Consumers migrate with: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 bylegacy-tokens.css); class names moved touic-.Reviewer guide
Commits are in order (
git log --oneline --reverse origin/main..feat/astryx-foundation, 73 commits):660f885..18b0a53ui-common.css+legacy-tokens.css, i18n catalog, Astryx CLI integration, fixture, README/CONTRIBUTINGd97161e..30be71eModal, ko-KR/ja-JP catalog872ba80..3be542cui-commonbin, migration map (includes two branch merges and alpha.1)65f5fb2..b311ea6AlertDialoghidden, BoardItemTitle/Statistic/DividedRow/TokenList/TokenRow/NotificationItem, UnitGrid/ColorPicker (alpha.2–alpha.6)67c3fe3..161c31cupgradewrite safety and--dry-run, Modal makes the page behind it inert and setsaria-modal, focus return (alpha.7–alpha.8)02d5af6..95c4ae6BulkEditFormItem(alpha.9)d42eccc..479c39fDataGrid,BulkErrorModaland five more components (alpha.10)85abfd7..12e83e9getCellProps, Ant Design Icons licence text (alpha.11)1017f34..f34cbf2--uic-prefix from custom properties (alpha.12)48bb2cf..45ca089DoubleTokenendContent, name-by-name lab mirror,ComplexSelector/Drawer/Tourforks (alpha.13)fde887f..95f178aDrawerEscape through core's layer-dismissal stack,PagedSelector(alpha.14)Skim only — generated:
src/astryx/**(export mirror), theexportsmap inpackage.json,theme/lablup/builtoutput, and the fork*.styles.tsfiles written by sync-forks. They are reproduced by the generators and guarded by drift tests; review the generators andexports.exclude.jsoninstead.Verification
pnpm run verify(typecheck, lint, format, boundary check, theme check, tests, build, pack check, integration check) on95f178a:Merge / publish conditions
0.2.0-alpha.15, published under thenextdist-tag (both@lablup/ui-commonand the new@lablup/ui-common-cli).latest/alphakeep pointing at the 0.1 line, and every 0.1 consumer pins an exact0.1.0-alpha.N, so nothing upgrades implicitly.0.2.0(movinglatest) still waits for the webui cut-over (FR-4099).Follow-ups
Other consumers — backend.ai-go, continuum-hub, mlxcel,
@lablup/ui-ai,@lablup/ui-charts— runui-common upgrade --from 0.1in their own repositories after 0.2.0 is published.