diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8284e8e..ead0a27 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -40,6 +40,9 @@ jobs: - name: Public boundary run: pnpm run check:boundary + - name: Built Lablup theme is up to date + run: pnpm run theme:check + - name: Test run: pnpm run test @@ -49,6 +52,9 @@ jobs: - name: Packed artifact and export paths run: pnpm run check:pack + - name: Astryx CLI integration + run: pnpm run check:integration + external-install: # Building green is not the same as being installable. This consumes the # real tarball from a project that shares nothing with this repository, so @@ -72,20 +78,38 @@ jobs: run: | pnpm run build pnpm pack --pack-destination "$RUNNER_TEMP" - - - name: Install the tarball into a clean React project + pnpm --filter @lablup/ui-common-cli pack --pack-destination "$RUNNER_TEMP" + + # pnpm, as every consumer installs: pnpm 11 fails the install with + # ERR_PNPM_IGNORED_BUILDS when a dependency's build script is neither + # allowed nor declined, which npm never reports. fixture/pnpm-workspace.yaml + # holds the allowBuilds block a consumer needs, and makes the fixture its + # own workspace, apart from this repository's. + - name: Install the tarballs into a clean React project working-directory: fixture run: | - TARBALL=$(find "$RUNNER_TEMP" -name 'lablup-ui-common-*.tgz' | head -1) - echo "Installing $TARBALL" - npm install --no-package-lock "$TARBALL" - npm install --no-package-lock + LIB=$(find "$RUNNER_TEMP" -name 'lablup-ui-common-[0-9]*.tgz' | head -1) + CLI=$(find "$RUNNER_TEMP" -name 'lablup-ui-common-cli-*.tgz' | head -1) + echo "Installing $LIB and $CLI" + pnpm add "$LIB" + pnpm add --save-dev "$CLI" - name: Type-check and build the fixture against the packed artifact working-directory: fixture run: | - npx tsc --noEmit - npx vite build + pnpm exec tsc --noEmit + pnpm exec vite build + + # The CLI resolves the project's @lablup/ui-common and the Astryx CLI it + # pins from two different packages; this runs it where a consumer would. + - name: Run the ui-common bin from the fixture + working-directory: fixture + run: | + pnpm exec ui-common --version --verbose + pnpm exec ui-common --help > /dev/null + pnpm exec ui-common agents > /dev/null + pnpm exec ui-common component Button --json > /dev/null + pnpm exec ui-common upgrade --from 0.1 --dry-run > /dev/null # Building is not the same as being styled. 0.1.0-alpha.0 built here # green while shipping every component's CSS as an asset nothing could diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 1734c47..6b59761 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -56,39 +56,71 @@ jobs: - name: Verify before publishing run: pnpm run verify - - name: Confirm the release tag matches the package version - if: github.event_name == 'release' + # @lablup/ui-common (the root) and @lablup/ui-common-cli (packages/cli) + # are released together at one version: the CLI takes the library as an + # exact peer. + - name: Confirm both packages are at one version run: | - PKG_VERSION=$(node -p "require('./package.json').version") - TAG="${GITHUB_REF_NAME#v}" - if [ "$PKG_VERSION" != "$TAG" ]; then - echo "package.json is $PKG_VERSION but the release tag is $TAG" >&2 + LIB=$(node -p "require('./package.json').version") + CLI=$(node -p "require('./packages/cli/package.json').version") + if [ "$LIB" != "$CLI" ]; then + echo "@lablup/ui-common is $LIB but @lablup/ui-common-cli is $CLI" >&2 exit 1 fi + - name: Confirm the release tag matches the package versions + if: github.event_name == 'release' + run: | + TAG="${GITHUB_REF_NAME#v}" + for PKG in package.json packages/cli/package.json; do + PKG_VERSION=$(node -p "require('./$PKG').version") + if [ "$PKG_VERSION" != "$TAG" ]; then + echo "$PKG is $PKG_VERSION but the release tag is $TAG" >&2 + exit 1 + fi + done + + # Any prerelease goes to `next`, a plain version to `latest`. 0.1's + # prereleases went out under `alpha`; that tag is no longer moved. + # The registry still sets `latest` on a package's first publish, so + # @lablup/ui-common-cli's `latest` is its first alpha until 0.2.0; the + # docs say `@next` until then. Do not move `latest` here. - name: Choose the dist-tag from the version id: tag run: | VERSION=$(node -p "require('./package.json').version") case "$VERSION" in - *-alpha*) TAG=alpha ;; - *-beta*) TAG=beta ;; - *-rc*) TAG=rc ;; - *-*) TAG=next ;; - *) TAG=latest ;; + *-*) TAG=next ;; + *) TAG=latest ;; esac echo "Publishing $VERSION under dist-tag $TAG" echo "tag=$TAG" >> "$GITHUB_OUTPUT" + echo "version=$VERSION" >> "$GITHUB_OUTPUT" + + # pnpm packs both, rewriting the CLI's `workspace:*` peer to the exact + # version; the library goes first so the CLI's peer exists when it lands. + - name: Pack both packages + run: | + mkdir -p "$RUNNER_TEMP/packed" + pnpm pack --pack-destination "$RUNNER_TEMP/packed" + pnpm --filter @lablup/ui-common-cli pack --pack-destination "$RUNNER_TEMP/packed" + ls -la "$RUNNER_TEMP/packed" - name: Publish if: github.event_name == 'release' || inputs.dry_run == false - run: pnpm publish --no-git-checks --access public --tag ${{ steps.tag.outputs.tag }} --registry https://npm.pkg.github.com + run: | + for PKG in lablup-ui-common lablup-ui-common-cli; do + npm publish "$RUNNER_TEMP/packed/$PKG-${{ steps.tag.outputs.version }}.tgz" --access public --tag ${{ steps.tag.outputs.tag }} --registry https://npm.pkg.github.com + done env: NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Dry run if: github.event_name == 'workflow_dispatch' && inputs.dry_run - run: pnpm pack && ls -la *.tgz + run: | + for PKG in lablup-ui-common lablup-ui-common-cli; do + npm publish "$RUNNER_TEMP/packed/$PKG-${{ steps.tag.outputs.version }}.tgz" --dry-run --access public --tag ${{ steps.tag.outputs.tag }} --registry https://npm.pkg.github.com + done # GitHub Packages requires authentication even for public packages, so an # open-source consumer cannot install from it without every contributor @@ -130,46 +162,78 @@ jobs: - name: Verify before publishing run: pnpm run verify - - name: Confirm the release tag matches the package version - if: github.event_name == 'release' + # @lablup/ui-common (the root) and @lablup/ui-common-cli (packages/cli) + # are released together at one version: the CLI takes the library as an + # exact peer. + - name: Confirm both packages are at one version run: | - PKG_VERSION=$(node -p "require('./package.json').version") - TAG="${GITHUB_REF_NAME#v}" - if [ "$PKG_VERSION" != "$TAG" ]; then - echo "package.json is $PKG_VERSION but the release tag is $TAG" >&2 + LIB=$(node -p "require('./package.json').version") + CLI=$(node -p "require('./packages/cli/package.json').version") + if [ "$LIB" != "$CLI" ]; then + echo "@lablup/ui-common is $LIB but @lablup/ui-common-cli is $CLI" >&2 exit 1 fi + - name: Confirm the release tag matches the package versions + if: github.event_name == 'release' + run: | + TAG="${GITHUB_REF_NAME#v}" + for PKG in package.json packages/cli/package.json; do + PKG_VERSION=$(node -p "require('./$PKG').version") + if [ "$PKG_VERSION" != "$TAG" ]; then + echo "$PKG is $PKG_VERSION but the release tag is $TAG" >&2 + exit 1 + fi + done + + # Any prerelease goes to `next`, a plain version to `latest`. 0.1's + # prereleases went out under `alpha`; that tag is no longer moved. + # The registry still sets `latest` on a package's first publish, so + # @lablup/ui-common-cli's `latest` is its first alpha until 0.2.0; the + # docs say `@next` until then. Do not move `latest` here. - name: Choose the dist-tag from the version id: tag run: | VERSION=$(node -p "require('./package.json').version") case "$VERSION" in - *-alpha*) TAG=alpha ;; - *-beta*) TAG=beta ;; - *-rc*) TAG=rc ;; - *-*) TAG=next ;; - *) TAG=latest ;; + *-*) TAG=next ;; + *) TAG=latest ;; esac echo "Publishing $VERSION under dist-tag $TAG" echo "tag=$TAG" >> "$GITHUB_OUTPUT" + echo "version=$VERSION" >> "$GITHUB_OUTPUT" + + # pnpm packs both, rewriting the CLI's `workspace:*` peer to the exact + # version; the library goes first so the CLI's peer exists when it lands. + - name: Pack both packages + run: | + mkdir -p "$RUNNER_TEMP/packed" + pnpm pack --pack-destination "$RUNNER_TEMP/packed" + pnpm --filter @lablup/ui-common-cli pack --pack-destination "$RUNNER_TEMP/packed" + ls -la "$RUNNER_TEMP/packed" - name: Confirm the credential is valid for npmjs run: npm whoami --registry https://registry.npmjs.org env: NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} - # npm rather than pnpm here: provenance is an npm CLI feature and this - # is the one step that needs it. The tarball is identical either way, - # since both pack from the same files allowlist. + # npm publishes the tarballs pnpm packed: provenance is an npm CLI + # feature, and npm would leave the CLI's `workspace:*` peer unrewritten + # if it packed the directory itself. - name: Publish if: github.event_name == 'release' || inputs.dry_run == false - run: npm publish --provenance --access public --tag ${{ steps.tag.outputs.tag }} --registry https://registry.npmjs.org + run: | + for PKG in lablup-ui-common lablup-ui-common-cli; do + npm publish "$RUNNER_TEMP/packed/$PKG-${{ steps.tag.outputs.version }}.tgz" --provenance --access public --tag ${{ steps.tag.outputs.tag }} --registry https://registry.npmjs.org + done env: NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} - name: Dry run if: github.event_name == 'workflow_dispatch' && inputs.dry_run - run: npm publish --dry-run --access public --tag ${{ steps.tag.outputs.tag }} --registry https://registry.npmjs.org + run: | + for PKG in lablup-ui-common lablup-ui-common-cli; do + npm publish "$RUNNER_TEMP/packed/$PKG-${{ steps.tag.outputs.version }}.tgz" --dry-run --access public --tag ${{ steps.tag.outputs.tag }} --registry https://registry.npmjs.org + done env: NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/.gitignore b/.gitignore index 2d577cb..1e30d3e 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,4 @@ coverage/ .eslintcache fixture/node_modules/ fixture/dist/ +fixture/pnpm-lock.yaml diff --git a/.prettierignore b/.prettierignore index 4af75e3..85289f4 100644 --- a/.prettierignore +++ b/.prettierignore @@ -2,3 +2,6 @@ dist/ coverage/ pnpm-lock.yaml LICENSE +src/theme/*/built/ +packages/cli/test/upgrade/fixtures/ +.claude/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 3acb7de..fcff8f3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,823 @@ Versioning follows the policy in [CONTRIBUTING.md](CONTRIBUTING.md#versioning). ## [Unreleased] +## [0.2.0-alpha.15] + +Hardening for the apps moving off 0.1: ui-common's styles now sit in their +cascade layer in every bundle, the CLI ships as its own package, +`@lablup/ui-common-cli`, React 19.2 is the floor, and the upgrade tool and +several components get the fixes a pre-merge review found. + +### Changed + +- **The `ui-common` CLI is its own package, `@lablup/ui-common-cli`**, in + this repository under `packages/cli` and released in lockstep with the + library, the way Astryx ships `@astryxdesign/cli` beside + `@astryxdesign/core`. `@lablup/ui-common` no longer has a `bin` and no + longer depends on `@astryxdesign/cli`, `jscodeshift` or `postcss`: a + production install of an app on the library alone drops from 196 MB (132 + packages) to 31 MB (25). Run the 0.1 upgrade with + `pnpm dlx @lablup/ui-common-cli@next upgrade --from 0.1` (`@next` until + 0.2.0 is published: npm points a new package's `latest` at its first + prerelease); after it, + `@lablup/ui-common-cli` is a devDependency and `pnpm exec ui-common` works + as before. The CLI needs Node 22.13 or later, as `@astryxdesign/cli` does. +- Prereleases publish under the `next` dist-tag; only a plain version moves + `latest`. The 0.1 line's `alpha` tag stays where it is. The registry sets + a new package's `latest` on its first publish regardless, so until 0.2.0 + `@lablup/ui-common-cli`'s `latest` is its first alpha: name `@next`. +- `ui-common upgrade --from 0.1`: + - migrates elements a project imports through its own barrels (relative + imports and tsconfig `paths`), and lists local wrapper components around + a 0.1 component for review instead of rewriting their call sites; + - drops a local re-export of a removed type together with its import (it + used to refuse to write such a file); + - wires the 0.2 stylesheets into the app entry when the project never + imported `styles/base.css`, and places that import before any + `@lablup/ui-common` import; + - adds `` at a single clear root render and + reports 0.1 theme switches (`data-theme="orange-*"`, `[data-theme]` + selectors); + - searches the whole project for manual-review findings (tests, e2e, + scripts), not only `src/`; `--scan ` narrows it; + - narrows a library's `react` / `react-dom` peers to the React ui-common's + own peer starts at, alternative by alternative (`>=18 <21 || ^22` → + `>=19.2.0 <21 || ^22`), and reports a range with no such React; + - adds `allowBuilds` for `@astryxdesign/core` and `@astryxdesign/cli` to a + pnpm project's `pnpm-workspace.yaml`; + - adds `@lablup/ui-common-cli` as a devDependency; + - lists class names the project also defines and uses itself as lower + confidence. + +### Fixed + +- **`ui-common` was the lowest cascade layer in consumer bundles.** Each + component module imports its own stylesheet, and each opened + `@layer ui-common{…}`. A product imports ui-common's modules before its + entry stylesheet, where the order statement lives, and a layer's position is + fixed by first appearance, so the bundle ranked `ui-common` below `reset` + and Astryx's `astryx-base` and `astryx-theme`: Astryx's rules beat + ui-common's composites (`StatCard`'s `overflow: hidden` lost to `Card`'s + `overflow: clip`). Every stylesheet the package ships (component sheets, + `ui-common.css`, `legacy-tokens.css`, `styles/`, the Lablup `theme.css` and + the Astryx `@import` mirrors) now opens with + `@layer reset, theme, base, astryx-base, astryx-theme, ui-common, components, utilities;`, + defined once in `scripts/layer-order.mjs` and prepended by the build. + Keep declaring it in the app's entry stylesheet too; repeating it is a + no-op. `check:pack` fails on a packed stylesheet without it, and CI's + fixture check fails when the consumer bundle establishes any other order. +- **The `react` and `react-dom` peers are `^19.2.0`.** `Modal`, + `NotificationStack`, `UnitGrid` and `Form` import `useEffectEvent`, stable + since React 19.2; `^19.0.0` let 19.0 and 19.1 install and then fail. +- A `Modal` opened inside a scrimmed `Drawer` from `@lablup/ui-common/lab` + opens above it. It rendered behind the drawer's modal ``, inert: + it could not be clicked, focused or typed into. +- `NotificationStack` unmounts a closed notice once its exit has played. With + `maxVisible`, or a new `notifications` array during the exit, it stayed + mounted for good, buttons still tabbable. Its countdown stays paused while + either hover or focus holds it, not until the first of them leaves. +- `DataGrid`'s settings dialog can re-show a column `defaultOverrides` hides + and restore the natural order over a default order; both came back. +- `ImageWithFallback`'s fallback keeps the image's semantics: an `img` named + by `alt`, or hidden when `alt` is empty. +- `DoubleBadge` and `DoubleToken` take string and object values mixed in one + `values` list, as documented. +- `ui-common upgrade` keeps JSX text whitespace: Badge children moved into + `label` (`{n} items`), and text next to an element given a TODO, lost their + leading spaces. For a library it adds the StyleX (and lab) peer even when + a devDependency already names the package. + +### Added + +- In development, a warning when a second copy of `@lablup/ui-common` is + loaded, naming each copy and saying whether they also run on separate + copies of `@astryxdesign/core`. Each copy registers under + `globalThis[Symbol.for("@lablup/ui-common/instance")]` on first use of its + string translator or modal stack; production builds drop it. + +### Documentation + +- README: `allowBuilds` for Astryx's postinstall scripts under pnpm 10 and + later (pnpm 11 fails with `ERR_PNPM_IGNORED_BUILDS` without it), the + shipped layer statement, why `` is required and + how dark mode works (``; 0.1's `data-theme="orange-dark"` + toggles no longer work), and Vitest's `server.deps.inline` for jsdom tests. +- The Astryx integration's agent docs and doc page name + `@lablup/ui-common/theme/lablup/built`, the theme that pairs with + `theme.css`, instead of the source theme. + +## Upgrading from 0.1 + +The 0.2 alphas below add up to these changes for a 0.1 consumer. Run +`pnpm dlx @lablup/ui-common-cli@next upgrade --from 0.1 --dry-run` (plain +`@lablup/ui-common-cli` once 0.2.0 is published) for the +mechanical part; +[`packages/cli/migration/0.1-to-0.2.json`](packages/cli/migration/0.1-to-0.2.json) lists every import, +prop, class and stylesheet change it reads. + +- **Dependencies.** Astryx (`@astryxdesign/core`, `theme-neutral`) is an + exact-pinned dependency (the CLI moved to `@lablup/ui-common-cli`); import it only through `@lablup/ui-common`. + `@stylexjs/stylex` ^0.19 is a new peer, `@astryxdesign/lab` an optional + exact peer (with an `overrides` entry for its core), and React 19.2 or + later is required. (0.2.0-alpha.0; 19.2 since the release after + 0.2.0-alpha.14) +- **The root barrel is Astryx's**, and `@lablup/ui-common/hooks` is Astryx's + hooks; `usePrefersReducedMotion` moved to the root. (0.2.0-alpha.0, + 0.2.0-alpha.2) +- **Removed components, each replaced by Astryx:** `Badge`, `BaseCard`, + `Button`, `DataTable`, `Drawer` (lab), `EmptyState`, `ProgressBar`, + `Select` (`Selector`), `Skeleton`, `StatusTag` (`StatusDot`), `Tabs` + (`TabList`) and `Tooltip`, with their props renamed to Astryx's + (`children` → `label`, `disabled` → `isDisabled`, …). (0.2.0-alpha.1) +- **Kept components keep their props** but render Astryx, style in + `@layer ui-common` and take `uic-` class names (`page-header` → + `uic-page-header`). CSS, tests and DOM queries on the old names need + updating. (0.2.0-alpha.1) +- **Dialogs:** Astryx `Dialog` and `AlertDialog` are hidden; use `Modal` and + `AlertModal`. An open `Modal` makes the rest of the page `inert`; an + overlay of the app's own that must stay usable over it needs + `data-uic-modal-live`. (0.2.0-alpha.1, 0.2.0-alpha.5, 0.2.0-alpha.7) +- **Stylesheets and theme:** `styles/base.css` becomes the layer statement + plus `reset.css`, `astryx.css`, `theme/lablup/theme.css`, `ui-common.css` + (and `legacy-tokens.css` while `--token-*` names are still read); drop + `styles/themes/*.css`. Wrap the app in `` from + `theme/lablup/built`. `--token-*`, `styles/base.css` and + `styles/themes/*.css` are deprecated and go in 0.3. (0.2.0-alpha.0) +- **Strings** resolve through Astryx's `InternationalizationProvider`; pass + `uiCommonMessages` from `@lablup/ui-common/i18n-catalog`. Shared keys are + `uic.common.*`. (0.2.0-alpha.0, 0.2.0-alpha.3) +- **Custom properties** follow Astryx's naming: the theme's info hue is + `--color-info`, component knobs are `---` + (DigitPopIn's are their 0.1 names again), and no `--uic-*` name remains. + (0.2.0-alpha.12) + +## [0.2.0-alpha.14] + +A paged selector moves in from a product, and `Drawer` hands Escape to the +layer on top. + +### Added + +- `PagedSelector`: a searchable selector, single or multiple + (`isMultiple`), over options loaded a page at a time. Scrolling the panel + within `endReachedThreshold` px (30) of its end calls `onEndReached` once + per arrival; `onSearchChange` reports each keystroke; `totalCount` and + `isLoadingMore` fill the foot. `value` holds option values (`string | +null`, or `string[]`), `onChange` also hands over each chosen value's + `{ value, label }`, and a selected value missing from `options` is named + from `labels`. Built on `ComplexSelector`, with Astryx's own panel search + row and Selector-shaped option rows. Strings are `uic.PagedSelector.*`, + translated in every shipped locale. + +### Changed + +- `Drawer` from `@lablup/ui-common/lab` routes Escape through Astryx core's + layer-dismissal stack, as core's `Dialog` does, instead of handling it on + its own element. An Escape in a popover, selector or modal opened inside + the drawer now closes that layer only, and a second Escape closes the + drawer; a drawer opened inside another closes first. **Behaviour change:** + a scrimless drawer closes on Escape wherever focus is, as the top-most + layer, not only while focus is inside it. The native `cancel` closes it + only while it is on top and no IME composition runs. + +## [0.2.0-alpha.13] + +Astryx fixes a product used to carry as pnpm patches now ship in ui-common, +so its consumers get them without patching. `DataGrid` and `DoubleToken` +take two fixes from the same product. + +### Added + +- `ComplexSelector` (`@lablup/ui-common/ComplexSelector` and the root) is + ui-common's own copy of Astryx's, same API, adding `hasClear` and + `onClear`: a clear button between the spinner and the chevron while + `triggerLabel` is set, as `Selector` has. `onClear` runs, or + `onChange(undefined)` without it. Upstream: facebook/astryx#6362. +- `DoubleToken`: a value's `endContent` renders in place of its visible + label (a copy control around the text, say). The label stays the + accessible name; `highlightKeyword` does not reach into it. +- `exports.exclude.json` can hide single names of a subpath (`exports`), and + `@lablup/ui-common/lab` is now written as named re-exports. Same names as + before. + +### Fixed + +- `Drawer` from `@lablup/ui-common/lab` is ui-common's own copy of lab's, + same API. An Escape from a layer opened inside the drawer (a modal + portalled out of it) no longer reaches the drawer, so the layer closes and + the drawer stays; an Escape that ends an IME composition no longer closes + it; and a consumer's `aria-modal` passes through on a scrimless drawer. +- `Tour`, `TourStep` and `useTour` from `@lablup/ui-common/lab` are + ui-common's own copies of lab's, same API. A step's highlight is promoted + into the top layer once and never hidden and re-shown, so under React + StrictMode the spotlight dim no longer paints over the callout. +- `DoubleToken` squares inner end corners with `:not(:last-of-type)`, so an + element trailing the tokens (a copy control's tooltip) no longer squares + the last token's outer corners. +- `DataGrid`'s root (`.uic-data-grid`) is `min-width: 0; max-width: 100%`, + so a wide grid inside a flex or grid parent scrolls itself instead of + stretching the parent. + +## [0.2.0-alpha.12] + +Custom properties take Astryx's naming as is: no `--uic-` prefix. + +### Changed + +- **Breaking for alpha consumers: every `--uic-*` custom property is + renamed or gone.** `uic-` class names and `data-uic-*` attributes are + unchanged. The rule is in CONTRIBUTING ("Styling") and + `componentStyles.test.ts` enforces it, including that no name collides + with one Astryx core, lab or the neutral theme declares or reads. + - The Lablup theme's info hue is `--color-info` (was `--uic-color-info`), + the name the Backend.AI WebUI theme uses, declared in the theme's + `tokens`. `LABLUP_INFO_TOKEN` carries the new name. `StatCard`'s info + tone and the legacy `--token-colorInfo` read it. + - Hooks that only carried a theme value are gone; the component reads the + theme token, with an Astryx fallback: + - `--uic-form-item-description-color` -> `--color-text-description` + (fallback `--color-text-secondary`). + - `--uic-text-highlighter-background` -> `--color-warning-border-hover` + (fallback `--color-warning-muted`). + - `--uic-progress-with-label-color` -> removed; the `color` prop paints + the fill directly (default `--color-success`). + - `--uic-stat-card-tone`, `--uic-stat-card-tone-muted`, + `--uic-error-state-tone`, `--uic-error-state-tone-muted` and + `--uic-progress-with-label-font-size` -> removed; the variant rules + read the tokens. + - Component knobs drop the prefix and follow Astryx's component-variable + form, `---`: + - `--board-item-title-z`, `--count-badge-offset-x`/`-y`, + `--data-grid-scroll-width`, `--data-grid-max-height`, + `--data-grid-dialog-list-height`, `--digit-pop-in-duration`/ + `-distance`/`-stagger`/`-blur`/`-ease`/`-index` (their 0.1 names + again), `--divided-row-column-gap`, `--form-item-margin-bottom`, + `--form-item-gap`, `--form-item-line-height`, + `--list-banner-max-height`, `--modal-z`, `--modal-level`, + `--modal-dir-x`/`-y`, `--notification-stack-z`, + `--notification-stack-inset-top`, `--overlay-scrollbar-z`, + `--progress-with-label-radius`, `--unit-grid-group-1`..`-7`, + `--unit-grid-ink-dark`, `--unit-grid-ink-light`, + `--unit-grid-cell-stroke`, `--unit-grid-cell-empty`, + `--unit-grid-popover-z`: the old name without `uic-`. + - `--uic-notification-body-max-height` -> + `--notification-stack-body-max-height`. + +## [0.2.0-alpha.11] + +Review fixes for the table cluster, and translation fixes. + +### Fixed + +- `DataGrid` select-all honours `selection.getIsItemEnabled`: it adds and + removes enabled rows only, a disabled row keeps its state, and the header's + checked and indeterminate states count enabled rows only. +- `DataGrid` passes a column's `getCellProps(item, index)` the row's index on + the page; it always received 0. +- `DataGridSettingsModal` drag handles are named (`uic.DataGrid.reorderColumn`, + "Reorder {column}") instead of focusable but `aria-hidden`, move through the + list by keyboard (Space, arrow keys, Space; Escape cancels), and announce + pick-up, moves, drop and cancel with the column's label and position. + Five more catalog keys (`uic.DataGrid.reorderInstructions`, + `reorderPickedUp`, `reorderMoved`, `reorderDropped`, `reorderCancelled`), + all six translated in every shipped locale. +- `uic.common.cancel` and `uic.common.delete` read as button labels, not + infinitives or the wrong word, in de, el, fi, id, it, ja, mn, ms, pl, + pt-PT, tr and vi; `uic.common.apply` in ms and th. +- `uic.common.ok`, `uic.common.retry` and the Card, Row and Text skeletons' + "Loading" are translated in the 18 locales that lacked them (Mongolian + OK excepted). +- NOTICE carries the MIT licence text of Ant Design Icons, whose path data + the form feedback glyphs use, and `check:pack` asserts it is packed. + +## [0.2.0-alpha.10] + +backend.ai-ui's table cluster (DataGrid, its dialogs, BulkErrorModal) and +five single components, the moves its theme shim had held back. + +### Added + +- **Components moved from backend.ai-ui**, with Astryx-shaped props and + their tests, exported from the root and from + `@lablup/ui-common/components/`: + - `DataGrid`: Astryx `Table` with a page bar and range line, client + (`compare`) or server (`sort`/`onSortChange`) sorting, row selection by + key, resizable and pinnable columns, expandable rows, per-user column + settings in one overrides record (`hidden`, `order`, `width`) and a CSV + export picker. Rows are paged unless `pagination.totalItems` says they + already are one page; a page past the last shows a way back to page 1. + `DataGridSettingsModal` (visibility and drag-to-reorder) and + `DataGridExportModal` (columns that export the same keys toggle + together) are its dialogs, exported on their own; both start fresh on + every open. Helpers `dataGridColumnLabel` and `isDataGridColumnVisible`. + - `BulkErrorModal`: the failed items of a bulk operation in a `DataGrid` + (compact, column rules, ten rows a page, no page bar on one page) under + an optional error banner; no footer. + - `ProgressWithLabel`: a bar carrying its label and value label over a + fill of `value` percent. `--uic-progress-with-label-color` (default + `--color-success`) and `--uic-progress-with-label-radius` (default + `--radius-inner`). + - `TextHighlighter`: marks every case-insensitive occurrence of a keyword. + `--uic-text-highlighter-background` (default `--color-warning-muted`). + - `CountdownBorder`: a border that fills clockwise over `durationMs`, a + countdown to the next refresh; `rx` resolves the theme's + `--radius-inner`. + - `DoubleToken`: welded Tokens for a settled pair, the Token counterpart + of `DoubleBadge`, with keyword highlighting. + - `ListBanner`: a `Banner` whose description is a keyboard-scrollable + list, capped at `maxHeight`. +- Dependencies `@dnd-kit/core` 6.3.1, `@dnd-kit/sortable` 10.0.0, + `@dnd-kit/modifiers` 9.0.0 and `@dnd-kit/utilities` 3.2.2, exact-pinned, + for the column reorder in `DataGridSettingsModal`. +- Catalog keys `uic.common.apply`, `uic.DataGrid.*` (11) and + `uic.BulkErrorModal.*` (2), translated in every shipped locale from + backend.ai-ui's locale files. The Mongolian range line + (`uic.DataGrid.range`) lost its stray braces on the way. + +## [0.2.0-alpha.9] + +A form engine with antd's form API, and the bulk-edit form item, from +backend.ai-ui. + +### Added + +- `@lablup/ui-common/Form` (also at the root): `Form` with `Form.Item`, + `Form.List`, `Form.ErrorList`, `Form.Provider`, `Form.useForm`, + `Form.useWatch`, `Form.useFormInstance` and `Form.Item.useStatus`; + `FormItem`, `FormList`, `ErrorList`, `FormProvider`, `useForm`, + `useWatch`, `useFormInstance`; `FormItemVisual` (the item shell); + `FormConfigProvider`, `FormConfigContext`, `useFormValidateMessages`; + `FormItemInputContext`, `NoStyleItemContext`; `FormStore`, + `defaultValidateMessages`; and the types `FormInstance`, `FormProps`, + `FormRef`, `FormItemProps`, `FormListProps`, `ListField`, + `ListOperations`, `ErrorListProps`, `WatchOptions`, `FieldData`, + `FieldError`, `Meta`, `NamePath`, `InternalNamePath`, `Rule`, + `RuleObject`, `RuleRender`, `RuleType`, `Store`, `StoreValue`, + `ValidateErrorEntity`, `ValidateMessages`, `ValidatorRule`, + `FormConfig`, `FormItemStatusContextValue`, `RequiredMark` and + `FormItemVisualProps`. It keeps antd's form API on purpose (a form-state + API, not a component vocabulary); see README, "Form". Its tests, + including the acceptance suite, came with it. +- `BulkEditFormItem`: a `Form.Item` that edits one field across many + records: "Keep as is", edit, and with `hasClear` a "Clear" that sets + `null`. `keepValueLabel`, `clearValueLabel`, `clearLabel` and + `undoLabel` override its strings. +- Catalog keys `uic.Form.*` (the 21 validation message templates and the + `(optional)` suffix) and `uic.BulkEditFormItem.*` (3), translated for all + 20 locales from backend.ai-ui. Four Malay templates (`stringMin`, + `stringMax`, `numberMin`, `numberMax`) gained the field name they had + dropped, and a Mongolian one (`arrayMin`) its `{min}` placeholder, which + had been translated into a name that never resolved. +- `lucide-react` (^1.18.0, the range `@astryxdesign/theme-neutral` already + requires) is a dependency, for the form item's help glyph. + +## [0.2.0-alpha.8] + +One `Modal` focus fix. + +### Fixed + +- `Modal` returned focus to `` instead of the opener when its content + took focus as it mounted: the first open of a `DeleteConfirmModal` with a + confirm field (or any autofocusing input), and every open with + `unmountOnClose`. The opener was read after the content's autofocus had + already run, so the modal recorded its own field as the opener. It is now + read before the content commits, and focus returns to it on close by + Escape, Cancel, the action or the backdrop, nested modals included. + +## [0.2.0-alpha.7] + +Review fixes: `Modal` now makes the page behind it inert, and the upgrade +tool stops overwriting files, capturing names and hiding lab's second core. + +### Changed + +- **`Modal` makes the page behind it inert** while it is open, and the + topmost dialog is `aria-modal="true"`, as `showModal()` would make them. + Every other child of `document.body` goes `inert` (and, where a kept + element is nested, every sibling on the way down to it), except modal + roots claimed through `useModalLevel` (a drawer portal's too) and elements + marked `data-uic-modal-live`. Closing the last modal removes only the + `inert` it set. Nested modals behave as before: only the topmost is + interactive. **Breaking:** an overlay of the app's own that must stay + usable over a modal (a toaster, a chat widget) needs `data-uic-modal-live`, + and `refreshModalBackground()` if it mounts while a modal is open. +- `NotificationStack` marks its root `data-uic-modal-live`, so notices stay + readable and dismissible over a modal. +- New exports from the root and `@lablup/ui-common/Modal`: + `MODAL_LIVE_ATTRIBUTE` and `refreshModalBackground`. +- `ui-common upgrade --dry-run` writes nothing: it prints the report after + the summary, and writes it only to a path given with `--report`. +- `ui-common upgrade` points `@astryxdesign/lab`'s core peer at ui-common's + core whenever it adds lab: an `overrides` entry in the nearest + `pnpm-workspace.yaml` (created when missing) for pnpm, in package.json for + npm, and a report note with both recipes otherwise. README's install + section documents the same recipes, and `ui-common sync-astryx` moves them + with the core pin. + +### Fixed + +- The lab canary (`0.6.2-canary.c9fb1ad`) peers on exactly the core canary + it was cut from, so a consumer got a second `@astryxdesign/core` and + `@lablup/ui-common/lab` ran on it. The documented overrides resolve it to + ui-common's core (verified with pnpm 11 and 12, and npm 11); a test fails + when lab's peer differs from the core pin and README's recipes are missing + or stale. +- Codemods: the Drawer `onClose` → `onOpenChange` wrapper named its + parameter `isOpen`, capturing a handler's own `isOpen` + (`() => { if (isOpen) close(); }` never ran). The parameter now takes a + name the file does not use. +- Codemods: a component rename (`BaseCard` → `Card`, `Tabs` → `TabList`) + checked only module-level names, so a function-local `const Card` captured + the import, and locals or parameters shadowing a 0.1 name were migrated. + The import now takes a free `Uic`-prefixed alias when any scope uses the + new name, and only references that resolve to the import are rewritten. +- `ui-common upgrade` overwrote an existing `ui-common-entry.css` outside + the scanned paths. A file this run did not read is never written: an + identical entry is reused, otherwise the entry goes to + `ui-common-entry-2.css` and the report says so. The report path is + replaced only when it holds an earlier report. +- The SCSS rewrite put the `@layer` order above `@use`, which Sass rejects. + It now follows the leading `@use`/`@forward` rules. +- Upstream Astryx codemods rewrote every mention of `@lablup/ui-common` and + `@astryxdesign/core` in a file, comments and strings included; only module + specifiers are swapped now. + +## [0.2.0-alpha.6] + +The last component moves from backend.ai-ui that do not wait on its theme +shim: its unit grid and its colour picker. + +### Added + +- **Components moved from backend.ai-ui**, with Astryx-shaped props and + their tests, exported from the root and from + `@lablup/ui-common/components/`: + - `UnitGrid`: groups of unit squares packed on one lattice (`serpentine` + or `wordwrap`), each group a tinted plate with its initial, a hover card + (`renderGroupPopover`), an optional palette picker (`hueOverrides`, + `onHueOverrideChange`), a legend row and a partial fill per unit. The + seven default hues are `--uic-unit-grid-group-1` to `-7` (Astryx + `--color-icon-*` by default), the initial's inks + `--uic-unit-grid-ink-dark`/`-light` (`--color-on-light`/`--color-on-dark`), + and `--uic-unit-grid-popover-z` places the hover card. + `UnitGridSkeleton` is its loading stand-in. + - `ColorPicker`: a hex colour field on the platform colour input, with a + hex text field and an optional clear button (`value`, `onChange` on the + settled colour, `hasValueLabel`, `hasClear`, `onClear`, `isDisabled`, + `label`). `toHexColor` normalises `#rgb`, `#rrggbbaa`, `rgb()` and + `rgba()` to `#rrggbb`. It had no tests in the origin and gets them here. +- Catalog keys `uic.UnitGrid.label`, `uic.UnitGrid.changeGroupColor`, + `uic.UnitGrid.useColor` (ICU `{index}`), `uic.ColorPicker.label`, + `uic.ColorPicker.hexValue`, `uic.ColorPicker.clear` and + `uic.ColorPicker.noColor`, translated in every shipped locale from + backend.ai-ui's locale files. + +## [0.2.0-alpha.5] + +Six more components move in from backend.ai-ui, the ones its theme shim +and its flex primitive held back, and Astryx `AlertDialog` is hidden behind +`AlertModal`. + +### Added + +- **Components moved from backend.ai-ui**, with Astryx-shaped props and + their tests, exported from the root and from + `@lablup/ui-common/components/`. Their layout is Astryx + `Stack`/`HStack`/`VStack` and `@layer ui-common` CSS on Astryx tokens: + - `BoardItemTitle`: a dashboard panel's sticky title row (`title`, + `tooltip`, `tooltipIcon`, `endContent`); `--uic-board-item-title-z` + sets its z-index (default 50). + - `Statistic`: a metric with a caption, a large value and a notched usage + bar (`label`, `value`, `total`, `unit`, `precision`, `progressMode` + `hidden`/`placeholder`/`visible`, `progressSteps`, `color`, + `unlimitedLabel`, `infinityLabel`). + - `DividedRow`: a wrapping row that draws a divider between neighbours on + the same line only (`wrap`, `rowGap`, `columnGap`, `dividerWidth`, + `dividerColor`, `dividerInset`, `itemStyle`). + - `TokenList`: values inline, the rest behind `+N` on hover or click + (`items`, `maxInline`, `emptyText`, `variant`, `trigger`). + - `TokenRow`: tokens cut off with "and N more" (`items`, `maxCount`, + `totalCount`, `color`, `emptyText`, `moreLabel`). + - `NotificationItem`: the title, description, actions and footer of one + notice. +- Catalog keys `uic.Statistic.unlimited` and `uic.TokenRow.more` (ICU + `{count}`), translated in every shipped locale from backend.ai-ui's locale + files. + +### Removed + +- **Breaking:** `AlertDialog` is no longer mirrored. The + `@lablup/ui-common/AlertDialog` subpath is gone, and `AlertDialog`, + `AlertDialogProps`, `useImperativeAlertDialog` and + `ImperativeAlertDialogReturn` leave the root barrel. Dialog-based surfaces + go through `Modal`'s level stack; a raw `AlertDialog` bypasses it. Use + `AlertModal`, which now also has the top-level subpath + `@lablup/ui-common/AlertModal`, the way `Modal` stands in for `Dialog`. + +## [0.2.0-alpha.4] + +Three more components move in from backend.ai-ui: the rest of its dialog +family and its list-stepped number field. + +### Added + +- **Components moved from backend.ai-ui**, with Astryx-shaped props and + their tests, exported from the root and from + `@lablup/ui-common/components/`: + - `AlertModal`: the WAI-ARIA alert-dialog pattern on `Modal`'s portalled + surface and level stack (`title`, `description`, `actionLabel`, + `onAction`, `actionVariant`, `isActionLoading`, `isActionDisabled`, + `cancelLabel`, `isCancelDisabled`, plus `Modal`'s own props). Cancel + takes focus first; Escape cancels, the backdrop does not. Use it instead + of `AlertDialog` beside `Modal`. + - `DeleteConfirmModal`: confirms a deletion on `Modal` (`items`, `target`, + `description`, `title`, `titleIcon`, `onAction`, `actionLabel`), with a + typed confirmation (`isConfirmInputRequired`, `confirmText`, + `inputLabel`, `inputPlaceholder`, `isInputDisabled`) for irreversible + deletions and `isReversible` for undoable ones. `inputLabel` takes a node + or a function that places the confirm-text token. + - `StepNumberInput`: a number field that steps along `steps` on its + stepper and on ArrowUp/ArrowDown. `NumberStepper` (the stepper column for + an `InputGroup`) and `getNextStepIndex` are exported with it. +- Catalog keys `uic.common.delete`, `uic.DeleteConfirmModal.{title, +titleMany,description,targetDescription,typeToConfirm,confirmText, +cannotBeUndone}` and `uic.NumberStepper.{increase,decrease}`, translated + in every shipped locale from backend.ai-ui's locale files. + `uic.DeleteConfirmModal.titleMany` is an ICU plural. +- `Modal`: `headerClassName` and `footerClassName`, class names on the + header and footer it generates. + +## [0.2.0-alpha.3] + +Three more components move in from backend.ai-ui, and ui-common's strings +are translated into every language backend.ai-ui ships. + +### Added + +- **Components moved from backend.ai-ui**, with Astryx-shaped props and + their tests, exported from the root and from + `@lablup/ui-common/components/`: + - `ConfirmPopover`: a one-click confirmation on `Popover` for reversible + actions: `title`, `description`, `icon`, `onAction` (may be async; the + popover closes when it resolves), `actionLabel`, `actionVariant`, + `isActionDisabled`, `onCancel`, `cancelLabel`. Cancel takes focus first + and focus returns to the trigger on close. Every other `Popover` prop, + the render-prop trigger included, passes through. + - `SelectionLabel`: "3 selected" with an optional clear button (`count`, + `onClear`, `label`, `clearLabel`, `clearIcon`). + - `UncontrolledInput`: a `TextInput`, or a `NumberInput` for + `type="number"`, that calls `onCommit` on Enter and on blur only. +- Shared catalog keys `uic.common.{ok,cancel,confirm,retry}` for the generic + action labels, and `uic.SelectionLabel.{selectedCount,clear}` and + `uic.UncontrolledInput.label`. +- Translations for every language backend.ai-ui ships, carried over from its + locale files: `de-DE`, `el-GR`, `es-ES`, `fi-FI`, `fr-FR`, `id-ID`, + `it-IT`, `mn-MN`, `ms-MY`, `pl-PL`, `pt-BR`, `pt-PT`, `ru-RU`, `th-TH`, + `tr-TR`, `vi-VN`, `zh-CN` and `zh-TW`, 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 gives Astryx's provider. Strings those languages have + no translation for yet are an explicit allowlist in the catalog test. + +### Changed + +- **Catalog keys renamed** to the shared keys. A consumer that overrides one + under its old name must use the new one: + `uic.Modal.ok` → `uic.common.ok`; `uic.Modal.cancel` and + `uic.NotificationStack.cancel` → `uic.common.cancel`; + `uic.NotificationStack.retry` and `uic.PageHeader.retry` → + `uic.common.retry`. The English and the `ko-KR`/`ja-JP` text are unchanged. + +### Fixed + +- `UncontrolledInput` with `type="number"` commits the value just entered. + The backend.ai-ui original committed the previous one, because + `NumberInput` reports the new value in the same event as Enter or blur. + +## [0.2.0-alpha.2] + +Seven components move in from backend.ai-ui, and the `ui-common` bin ships: +the Astryx CLI under ui-common's paths, the agent block, and the 0.1 → 0.2 +upgrade tool. + +### Added + +- **Components moved from backend.ai-ui**, each built on Astryx with + Astryx-shaped props, exported from the root and from + `@lablup/ui-common/components/`: + - `CountBadge`: a count or a dot overlaid on its child's top-end corner, + with `max` overflow (`99+`), `isZeroShown`, `offset`, `size` (`sm`/`md`) + and a named `role="status"` region. `className` goes on the wrapper. + - `DoubleBadge`: a run of Badges welded into one chip. + - `BooleanToken`: an on/off value as a Token (green for true), 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; `focusable={false}` renders a span. + - `ImageWithFallback`: an `` that renders a fallback node once it + fails to load. + - `OverlayScrollbar`: a persistent, draggable thumb drawn over a scroll + container, which hides the native bar through + `data-uic-overlay-scrollbar`. Its stacking order is + `--uic-overlay-scrollbar-z`. + - `NotificationStack`: floating Banner notices with task progress, + Cancel/Retry and an action, auto-close that pauses on hover and focus, + `maxVisible`, and enter/exit motion. `--uic-notification-stack-z` + (default 11000, above Modal's band) and + `--uic-notification-stack-inset-top` place it. +- Catalog strings `uic.BooleanToken.{true,false}` and + `uic.NotificationStack.{cancel,retry,progress}`, with `ko-KR` and `ja-JP` + translations. +- **The `ui-common` bin**, wrapping the Astryx CLI ui-common pins: + - `ui-common ` runs any Astryx command with its output + rewritten to `@lablup/ui-common` paths and `ui-common` commands, and a note + when it names a hidden subpath ("Use Modal, not Dialog"). `--json` stays + valid JSON and exit codes are Astryx's. `component`, `search` and the + other lookups work in a project that depends on ui-common alone. + `ui-common astryx …` runs Astryx without rewriting. + - `ui-common agents [--write ] [--check]` writes the agent block + between `UI-COMMON` markers: Astryx's block, rewritten, plus ui-common's + rules. + - `ui-common upgrade` runs the 0.1 → 0.2 codemods from the migration map: + imports of the removed components move to their Astryx counterparts, + provable prop renames are applied and the rest marked + `TODO(ui-common-upgrade)`, the `styles/base.css` import becomes the 0.2 + stylesheet set, and `package.json` gets the new version, the StyleX peer + and, with a Drawer, the lab canary. `ui-common-upgrade-report.md` lists + every TODO, the selectors, DOM queries and tests on 0.1 class names (with + the `uic-` name where the component was kept), module mocks, and custom + properties that collide with Astryx's. `--dry-run` writes only the report. + - `ui-common sync-astryx `, the maintainer's Astryx bump, which + records the Astryx codemods consumers need for later `upgrade` runs. +- Component docs for the CLI: `ui-common component Modal`, + `ui-common component PageHeader`, and one for each component above. + +### Changed + +- `migration/0.1-to-0.2.json` is corrected and extended: it records that + `usePrefersReducedMotion` moved from `/hooks` (Astryx's hooks barrel from + 0.2) to the root, maps `SelectOption` to Astryx's `SelectorOptionData`, and + Button `title` to `tooltip`. The codemods read all of their data from it. + +## [0.2.0-alpha.1] + +The component layer moves onto Astryx: the 0.1 look-alikes are gone, the +components Astryx has no counterpart for are rebuilt on it with their 0.1 +props, and `Modal` takes the place of the hidden `Dialog`. +[`migration/0.1-to-0.2.json`](packages/cli/migration/0.1-to-0.2.json) lists every change +below in the form `ui-common upgrade` reads. + +### Removed + +These 0.1 components are removed, source, styles and +`@lablup/ui-common/components/` subpath alike. Each is replaced by +Astryx, reached through ui-common. 0.2.0-alpha.0 announced their removal for +0.3; it lands in 0.2 so the prerelease line never ships two components under +one name: + +- `Badge`: Astryx `Badge` (`@lablup/ui-common/Badge`), or `Token` for a chip. + `children` becomes `label`; `danger` becomes `error`. +- `BaseCard`: Astryx `Card`, or `ClickableCard` when it is clickable. +- `Button`: Astryx `Button`, or `IconButton` for an icon-only button. + `children` becomes `label`, `disabled` `isDisabled`, `loading` `isLoading`, + `danger` `destructive`; sizes are `sm`, `md`, `lg`. +- `DataTable`: Astryx `Table`. `rows` becomes `data`, `getRowKey` `idKey`, a + column's `id` `key` and `render` `renderCell`. Sorting and resizing are + Table plugins. +- `Drawer`: lab `Drawer` (`@lablup/ui-common/lab`, needs the optional + `@astryxdesign/lab` peer). `onClose` becomes `onOpenChange`. +- `EmptyState`: Astryx `EmptyState`. `illustration` becomes `icon`; the two + action objects become an `actions` node. +- `ProgressBar`: Astryx `ProgressBar`. `value={null}` becomes + `isIndeterminate`; `label` is required. +- `Select`: Astryx `Selector`. `searchable` becomes `hasSearch`, `disabled` + `isDisabled`; `label` is a required string. +- `Skeleton` (the base shape only): Astryx `Skeleton`. `variant="circle"` + becomes `radius="rounded"`. It is always decorative; announce the wait on + the region around it. +- `StatusTag`: Astryx `StatusDot`, with `state` mapped onto `variant` and + `pulse` onto `isPulsing`. The dot carries the label as its accessible name + only; render the text beside it. +- `Tabs`: Astryx `TabList`. It renders the strip; the caller renders the + panel. `activeTab` becomes `value`, `onTabChange` `onChange`. +- `Tooltip`: Astryx `Tooltip`. `placement` `top`/`bottom` becomes + `above`/`below`. + +### Changed + +- **`PageHeader`, `PageLayout`, `StatCard`, `ErrorState`, `SmoothHeight`, + `DigitPopIn`, `SkeletonCard`, `SkeletonText`, `SkeletonChart` and + `SkeletonRow` are rebuilt on Astryx**, with the same props. They render + Astryx `Heading`, `Text`, `Button`, `IconButton`, `Icon`, `Card`, + `ClickableCard` and `Skeleton`, and their styles now live in + `@layer ui-common` and read Astryx tokens only. +- **Their class names moved to `uic-`**: `page-header` is `uic-page-header`, + `stat-card__value` is `uic-stat-card__value`, and so on. The shapes inside + the Skeleton composites are `uic-skeleton-shape` (on Astryx's + `astryx-skeleton`). `ErrorState`'s `error-state__action-btn` is + `uic-error-state__action`. DigitPopIn's tuning properties are + `--uic-digit-pop-in-*`. StatCard no longer sets `corner-accent` or reads + `--corner-accent-color`; its tone draws its own corner. +- **Built-in strings come from the catalog**: PageHeader's Retry and Dismiss + labels and the Skeleton composites' loading names resolve through + `uic.PageHeader.*` and `uic.Skeleton*.loading`. The props that set them + still win. +- `StatCard` with `onClick` renders Astryx `ClickableCard`; its accessible name + sits on the card's inner button. +- `ErrorState`'s default icon is Astryx's `error` glyph. + +### Added + +- **`Modal`**, at `@lablup/ui-common/Modal` and the root: ui-common's dialog in + place of Astryx `Dialog`. It takes every `Dialog` prop, renders into a + `document.body` portal so layers above the modal band stay reachable, stacks + nested modals (only the topmost traps focus and takes Escape, through + Astryx's layer stack), keeps content mounted while closed unless + `unmountOnClose`, and reports each edge through `afterOpenChange`. With + `title`, `onAction` or `footer` it lays out a header, the body and a footer + with a primary action (pending while `onAction`'s promise runs) and Cancel. + `ModalHeader`, `ModalPosition`, `ModalPurpose` and `ModalVariant` are + Astryx's Dialog parts under Modal names; `DialogHeader`, `DialogPosition`, + `DialogPurpose` and `DialogVariant` are re-exported unchanged, so a `Dialog` + import moves by changing the specifier and `Dialog`/`DialogProps`. + `configureModalZIndex` sets the z-index band (default 1100 to 10999) and + `useModalLevel` lets another portalled surface join the stack. +- Catalog strings `uic.Modal.ok`, `uic.Modal.cancel`, `uic.PageHeader.retry`, + `uic.PageHeader.dismissError` and `uic.Skeleton{Card,Text,Row,Chart}.loading`, + with `ko-KR` and `ja-JP` translations in `ui-common-locales/`. +- `migration/0.1-to-0.2.json`, the 0.1 → 0.2 map for the upgrade tool. +- The export generator checks that an exclusion's `replacedBy` exists, and lets + the replacement re-export the excluded subpath's own names when they resolve + to Astryx's declaration. + +## [0.2.0-alpha.0] + +ui-common is now Lablup's layer on top of Astryx. This release lays the +foundation: the mirrored Astryx surface, the Lablup theme, the new stylesheets +and the string catalog. The 0.1 components stay in place for now; the ones +Astryx covers are deprecated and go in 0.3. See [docs/astryx.md](docs/astryx.md) +for the architecture. + +### Breaking + +- **Astryx is a dependency.** `@astryxdesign/core`, `@astryxdesign/theme-neutral` + and `@astryxdesign/cli` 0.6.2 are exact-pinned dependencies. + `@stylexjs/stylex` ^0.19 is a new peer. `@astryxdesign/lab` + 0.6.2-canary.c9fb1ad is an optional exact peer. +- **React 19 only.** The `react` and `react-dom` peer range is now `^19.0.0`, + which Astryx requires. React 18 is no longer supported. +- **The root barrel is Astryx's.** `import { Button } from "@lablup/ui-common"` + now gives Astryx's `Button`. The same holds for `Badge`, `EmptyState`, + `ProgressBar`, `Skeleton` and `Tooltip`, and their props types. The 0.1 + components of those names are still at + `@lablup/ui-common/components/` until 0.3. +- **`@lablup/ui-common/hooks` is Astryx's hooks.** `usePrefersReducedMotion` + is still exported from the package root. + +### Added + +- **The Astryx surface, mirrored 1:1.** Every `@astryxdesign/core` subpath + exists under the same name (`@lablup/ui-common/Button`, + `@lablup/ui-common/Table/utils`, `@lablup/ui-common/theme/tokens.stylex`, + `@lablup/ui-common/astryx.css`, `@lablup/ui-common/locales/.json`). + Lab is at `@lablup/ui-common/lab` and `lab/lab.css`, and the neutral theme + at `theme/neutral`, `theme/neutral/built` and `theme/neutral/theme.css`. The + surface is generated by `scripts/gen-exports.mjs` and guarded by a drift + test. `Dialog` is hidden (use `Modal` once it ships), along with two Astryx + CLI data files; see `exports.exclude.json`. +- **The Lablup theme.** `@lablup/ui-common/theme/lablup` (source), + `theme/lablup/built` and `theme/lablup/theme.css` (pre-built). It extends + neutral with the orange accent (`#FF7A00` / `#DC6B03`), the 0.1 status hues, + and the 0.1 font family name. Info is the theme-local `--uic-color-info`, + since Astryx has no info token. +- **`@lablup/ui-common/ui-common.css`**, the global sheet, in + `@layer ui-common`. It carries the scrollbar rules, now on Astryx tokens. + The canonical layer order is + `@layer reset, theme, base, astryx-base, astryx-theme, ui-common, components, utilities;`. +- **`@lablup/ui-common/legacy-tokens.css`**, a deprecated bridge. It declares + all 122 0.1 `--token-*` names inside `@layer ui-common`, each as the matching + Astryx token where one exists and as its 0.1 value otherwise. +- **String catalog.** `@lablup/ui-common/i18n-catalog` exports + `uiCommonMessages`, `uiCommonCatalog` and `mergeMessages`, and + `@lablup/ui-common/ui-common-locales/.json` ships the catalog per + locale. Custom components resolve their built-in strings through Astryx's + `InternationalizationProvider`, with English as the fallback. The catalog is + empty until the first component moves its strings in. +- **Astryx CLI integration.** `astryx.integration.mjs` gives consumers + `astryx docs ui-common` and four agent-block lines, including "Use Modal, + not Dialog". + +### Deprecated (removed in 0.3) + +- The `--token-*` contract, `styles/base.css` and `styles/themes/*.css`. Use + the Lablup theme and Astryx tokens; `legacy-tokens.css` bridges meanwhile. +- These 0.1 components, each with its Astryx replacement: `Badge` → `Badge` or + `Token`, `BaseCard` → `Card`, `Button` → `Button`, `DataTable` → `Table`, + `Drawer` → lab `Drawer`, `EmptyState` → `EmptyState`, `ProgressBar` → + `ProgressBar`, `Select` → `Selector`, `Skeleton` → `Skeleton`, `StatusTag` → + `StatusDot`, `Tabs` → `TabList`, `Tooltip` → `Tooltip`. + `PageHeader`, `PageLayout`, `StatCard`, `ErrorState`, `SmoothHeight`, + `DigitPopIn` and the Skeleton composites stay, and are rebuilt on Astryx. + ## [0.1.0-alpha.23] ### Added @@ -619,7 +1436,27 @@ mid-migration. validation, and a clean external React install fixture. - Apache-2.0 license and the initial public boundary rules. -[Unreleased]: https://github.com/lablup/ui-common/compare/v0.1.0-alpha.19...HEAD +[Unreleased]: https://github.com/lablup/ui-common/compare/v0.2.0-alpha.15...HEAD +[0.2.0-alpha.15]: https://github.com/lablup/ui-common/compare/v0.2.0-alpha.14...v0.2.0-alpha.15 +[0.2.0-alpha.14]: https://github.com/lablup/ui-common/compare/v0.2.0-alpha.13...v0.2.0-alpha.14 +[0.2.0-alpha.13]: https://github.com/lablup/ui-common/compare/v0.2.0-alpha.12...v0.2.0-alpha.13 +[0.2.0-alpha.12]: https://github.com/lablup/ui-common/compare/v0.2.0-alpha.11...v0.2.0-alpha.12 +[0.2.0-alpha.11]: https://github.com/lablup/ui-common/compare/v0.2.0-alpha.10...v0.2.0-alpha.11 +[0.2.0-alpha.10]: https://github.com/lablup/ui-common/compare/v0.2.0-alpha.9...v0.2.0-alpha.10 +[0.2.0-alpha.9]: https://github.com/lablup/ui-common/compare/v0.2.0-alpha.8...v0.2.0-alpha.9 +[0.2.0-alpha.8]: https://github.com/lablup/ui-common/compare/v0.2.0-alpha.7...v0.2.0-alpha.8 +[0.2.0-alpha.7]: https://github.com/lablup/ui-common/compare/v0.2.0-alpha.6...v0.2.0-alpha.7 +[0.2.0-alpha.6]: https://github.com/lablup/ui-common/compare/v0.2.0-alpha.5...v0.2.0-alpha.6 +[0.2.0-alpha.5]: https://github.com/lablup/ui-common/compare/v0.2.0-alpha.4...v0.2.0-alpha.5 +[0.2.0-alpha.4]: https://github.com/lablup/ui-common/compare/v0.2.0-alpha.3...v0.2.0-alpha.4 +[0.2.0-alpha.3]: https://github.com/lablup/ui-common/compare/v0.2.0-alpha.2...v0.2.0-alpha.3 +[0.2.0-alpha.2]: https://github.com/lablup/ui-common/compare/v0.2.0-alpha.1...v0.2.0-alpha.2 +[0.2.0-alpha.1]: https://github.com/lablup/ui-common/compare/v0.2.0-alpha.0...v0.2.0-alpha.1 +[0.2.0-alpha.0]: https://github.com/lablup/ui-common/compare/v0.1.0-alpha.23...v0.2.0-alpha.0 +[0.1.0-alpha.23]: https://github.com/lablup/ui-common/compare/v0.1.0-alpha.22...v0.1.0-alpha.23 +[0.1.0-alpha.22]: https://github.com/lablup/ui-common/compare/v0.1.0-alpha.21...v0.1.0-alpha.22 +[0.1.0-alpha.21]: https://github.com/lablup/ui-common/compare/v0.1.0-alpha.20...v0.1.0-alpha.21 +[0.1.0-alpha.20]: https://github.com/lablup/ui-common/compare/v0.1.0-alpha.19...v0.1.0-alpha.20 [0.1.0-alpha.19]: https://github.com/lablup/ui-common/compare/v0.1.0-alpha.18...v0.1.0-alpha.19 [0.1.0-alpha.18]: https://github.com/lablup/ui-common/compare/v0.1.0-alpha.17...v0.1.0-alpha.18 [0.1.0-alpha.17]: https://github.com/lablup/ui-common/compare/v0.1.0-alpha.16...v0.1.0-alpha.17 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 603f4cd..ecaa7bd 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,159 +1,481 @@ # Contributing to @lablup/ui-common -## Component admission +Read [docs/astryx.md](docs/astryx.md) first. It explains how the package is put +together. This file lists the rules. + +## What ui-common is + +Astryx, mirrored 1:1, plus the Lablup theme, plus a small set of components of +its own. Astryx is the component system. ui-common adds only what Astryx does +not have. + +## The Astryx surface is generated + +Never hand-edit these. `scripts/gen-exports.mjs` writes them: + +- `src/astryx/**`, one re-export file per Astryx subpath +- `src/index.ts`, the root barrel +- `exports` in `package.json` + +Run `pnpm run gen:exports` after any of these changes: + +- an `@astryxdesign/*` version bump +- an edit to `exports.exclude.json` +- an edit to `exports.customs.json` + +`src/exports.test.ts` regenerates in memory and fails on any difference. So a +bump fails CI until the generator has run and someone has read the diff. + +### Hiding an Astryx subpath + +Add an entry to `exports.exclude.json`: + +```json +{ "name": "Dialog", "replacedBy": "Modal", "reason": "..." } +``` + +`name` is the ui-common subpath (`Dialog`, `lab/lab.css`). `replacedBy` is what +to use instead, or `null`. The subpath disappears from the exports map, and its +names disappear from the root barrel. An entry that no longer matches an Astryx +subpath fails the generator, so stale entries get removed. So does a +`replacedBy` that is neither a custom in `exports.customs.json` nor a mirrored +subpath. + +`exports` hides single names of a subpath instead of the whole of it: + +```json +{ + "name": "lab", + "exports": ["Drawer", "DrawerProps"], + "replacedBy": "Drawer", + "reason": "..." +} +``` -Most reusable-looking components should not be here. A component that lives in -one product can be changed by the team that owns it in an afternoon. Once it is -here, changing it means a version bump, a compatibility range, and four -consumers who did not ask for the change. That cost is worth paying only when -the component is genuinely shared. +The mirror file then lists the subpath's names one by one, minus these. `lab` +is always written that way, since Astryx ships it as one namespace with no +per-component subpath. A listed name the subpath no longer exports fails the +generator. + +There is no other way to hide something. Do not curate the export map by hand. + +### Adding a custom export + +1. Build the component under `src/components//`, with an `index.ts`. +2. Add it to `exports.customs.json`: + + ```json + { "name": "Modal", "source": "components/Modal/index.ts", "subpath": "Modal" } + ``` + + `subpath` is optional. With it, the component also gets its own top-level + subpath, `@lablup/ui-common/Modal`. + +3. Run `pnpm run gen:exports`. + +The generator refuses a custom that exports a name Astryx core or lab also +exports. Two exceptions: + +- The replacement of an excluded subpath may re-export that subpath's names + unchanged, so moving an import onto it changes only the specifier. `Modal` + re-exports `DialogHeader` this way. The generator checks that the name + resolves to Astryx's own declaration, not to something else of that name. +- Entries marked `legacy` may collide, and Astryx's export wins in the root + barrel. The mechanism is kept for a future deprecation; no entry uses it + since the 0.1 look-alikes were removed. +- A fork (below) keeps Astryx's names, because its exclusion takes Astryx's + out of the mirror first. + +## Name rule + +A ui-common component never shares a name with an Astryx core or lab export. +Pick a different name, or use the Astryx component. + +The one exception is a fork. + +## Forks of Astryx components + +A fork is a copy of an Astryx component with an upstream fix applied, shipped +under Astryx's own name and import path until Astryx ships the fix. It exists +because a product's pnpm `patchedDependencies` never reach that product's +consumers, and ui-common's consumers import Astryx through ui-common. + +| Fork | Where | Fix | Upstream | +| ----------------- | ----------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------- | +| `ComplexSelector` | `@lablup/ui-common/ComplexSelector` | `hasClear` / `onClear` | [facebook/astryx#6362](https://github.com/facebook/astryx/pull/6362) | +| `Drawer` | `@lablup/ui-common/lab` | Escape goes through core's layer-dismissal stack (see below); `aria-modal` passes through; a `Modal` inside a scrimmed drawer opens above it | not filed | +| `Tour` | `@lablup/ui-common/lab` | a step's highlight is promoted once (StrictMode) | not filed | + +`Drawer` is more than a fix: lab's drawer handles Escape itself, ahead of +core's layer-dismissal stack, so an Escape in a popover, selector or modal +inside it closed the drawer too. The fork registers with the stack through +`useLayerDismissal`, as core's `Dialog` does, so one press closes only the +top-most layer. A deliberate consequence: a non-modal drawer closes on Escape +wherever focus is, not only while focus is inside it. Keep this change on a +bump until lab's `Drawer` joins the stack itself (its "still differs from +lab's" test fails then); `provenance.json` records it as `notes`. + +How one is put together: + +- **Source** is in `src/forks//`, lab's too: upstream's + source (`astryx swizzle ` rewrites its imports to public subpaths), + the fix, and a header saying what changed. An internal Astryx does not + export is inlined from the same version. +- **Styles** are not compiled here. `.styles.ts` holds the StyleX + objects Astryx's own build compiled for the pinned version, copied from its + `dist/` by `pnpm run sync:forks`, so the fork renders upstream's atomic class + names and their rules arrive with `astryx.css` / `lab/lab.css`. Where + upstream's compiler folded a `stylex.props` call into a class string, the + fork carries that string. No new CSS, no second copy of a rule, the same + cascade layer as upstream. +- **Exports**: an `exports.exclude.json` entry with `replacedBy` naming the + fork, and an `exports.customs.json` entry with `fork` naming the Astryx + module. A core fork takes the excluded subpath (`"subpath": "ComplexSelector"`) + and must export exactly the names Astryx's does; a lab fork excludes names of + `lab` and must export exactly those, and the generator writes them into the + `lab` mirror. Anything else keeps the name rule. +- **Tests**: upstream's tests, run against the fork (`.test.tsx`), and + ui-common's (`.fork.test.tsx`): the fix, markup parity with Astryx's + component where the fix does not apply, and a test that Astryx's component + still lacks the fix. +- **Provenance**: `src/forks/provenance.json` records the Astryx version and a + SHA-256 of every upstream file the fork uses. The copied code is MIT; its + licence is in `NOTICE`. + +`src/forks/forks.test.ts` fails as soon as the installed Astryx differs from +the recorded version or file. On an Astryx bump: + +1. If upstream now carries the fix (the "still lacks the fix" test fails, or + the issue is closed), delete the fork: its directory, its entries in + `exports.customs.json`, `exports.exclude.json` and `provenance.json`, and + its row above and in NOTICE's scope. Run `pnpm run gen:exports`. Astryx's + own component comes back under the same name. +2. Otherwise re-take upstream's new source, re-apply the fix, then run + `node scripts/sync-forks.mjs --accept` to regenerate the styles and record + the new version and hashes. + +## Components + +| Component | Source | Built on | +| -------------------------------------------------------------- | ------------------------------------ | --------------------------------------------------- | +| `Modal` | `src/components/Modal/` | `Dialog` (inline), `DialogHeader`, `Layout` | +| `PageLayout` | `src/components/PageLayout/` | plain CSS | +| `PageHeader` | `src/components/PageHeader/` | `Heading`, `Text`, `Button`, `IconButton` | +| `StatCard` | `src/components/StatCard/` | `Card`, `ClickableCard`, `Text`, `Skeleton` | +| `ErrorState` | `src/components/ErrorState/` | `Icon`, `Heading`, `Text`, `Button` | +| `SkeletonCard`, `SkeletonText`, `SkeletonChart`, `SkeletonRow` | `src/components/Skeleton/` | `Skeleton` | +| `SmoothHeight` | `src/components/SmoothHeight/` | plain CSS | +| `DigitPopIn` | `src/components/DigitPopIn/` | plain CSS | +| `CountBadge` | `src/components/CountBadge/` | `Badge` | +| `DoubleBadge` | `src/components/DoubleBadge/` | `Badge`, `HStack` | +| `BooleanToken` | `src/components/BooleanToken/` | `Token` | +| `IconWithTooltip` | `src/components/IconWithTooltip/` | `Tooltip`, `Text` | +| `ImageWithFallback` | `src/components/ImageWithFallback/` | plain ``, plain CSS | +| `NotificationStack` | `src/components/NotificationStack/` | `Banner`, `Button`, `ProgressBar`, `Stack`, `Text` | +| `OverlayScrollbar` | `src/components/OverlayScrollbar/` | plain CSS | +| `ConfirmPopover` | `src/components/ConfirmPopover/` | `Popover`, `Button`, `Stack`, `Text` | +| `PagedSelector` | `src/components/PagedSelector/` | `ComplexSelector` (fork), `SelectorOption`, `Token` | +| `SelectionLabel` | `src/components/SelectionLabel/` | `Text`, `IconButton`, `HStack` | +| `UncontrolledInput` | `src/components/UncontrolledInput/` | `TextInput`, `NumberInput` | +| `AlertModal` | `src/components/AlertModal/` | `Modal`, `Heading`, `Text`, `Button`, `Layout` | +| `DeleteConfirmModal` | `src/components/DeleteConfirmModal/` | `Modal`, `TextInput`, `Token`, `Banner`, `Text` | +| `StepNumberInput`, `NumberStepper` | `src/components/StepNumberInput/` | `InputGroup`, `NumberInput`, `Icon` | +| `BoardItemTitle` | `src/components/BoardItemTitle/` | `HStack`, `Heading`, `Icon`, `IconWithTooltip` | +| `Statistic` | `src/components/Statistic/` | `Stack`, `Text`, `Tooltip` | +| `DividedRow` | `src/components/DividedRow/` | plain CSS | +| `TokenList` | `src/components/TokenList/` | `Token`, `Badge`, `Link`, `HoverCard`, `Popover` | +| `TokenRow` | `src/components/TokenRow/` | `Token`, `HStack` | +| `NotificationItem` | `src/components/NotificationItem/` | `Stack`, `Text` | +| `UnitGrid`, `UnitGridSkeleton` | `src/components/UnitGrid/` | `Stack`, `Text`, `VisuallyHidden`, `Skeleton` | +| `ColorPicker` | `src/components/ColorPicker/` | `Popover`, `TextInput`, `Button` | +| `Form` (engine, `Form.Item` shell, hooks) | `src/components/Form/` | `Tooltip`, plain CSS | +| `BulkEditFormItem` | `src/components/BulkEditFormItem/` | `Form.Item`, `TextInput`, `Link`, `HStack` | +| `DataGrid`, `DataGridSettingsModal`, `DataGridExportModal` | `src/components/DataGrid/` | `Table` + plugins, `Pagination`, `Modal`, dnd-kit | +| `BulkErrorModal` | `src/components/BulkErrorModal/` | `Modal`, `Banner`, `DataGrid` | +| `ProgressWithLabel` | `src/components/ProgressWithLabel/` | `Text`, plain CSS | +| `TextHighlighter` | `src/components/TextHighlighter/` | plain CSS | +| `CountdownBorder` | `src/components/CountdownBorder/` | SVG, plain CSS | +| `DoubleToken` | `src/components/DoubleToken/` | `Token`, `HStack`, `TextHighlighter` | +| `ListBanner` | `src/components/ListBanner/` | `Banner` | + +Each has tests beside it. `src/components/componentStyles.test.ts` holds every +stylesheet to the styling rules below. + +### Component docs for the CLI + +`ui-common component ` reads `astryx/components/.doc.mjs` (the +`components` root in `astryx.integration.mjs`). The Astryx CLI pairs every +doc with a same-stem `.tsx` and fails validation without one; ui-common +ships no source, so that file is one line re-exporting the component: -A component is admitted when all three hold: +```tsx +export { PageHeader } from "@lablup/ui-common"; +``` -1. **Product-neutral.** Its props are generic view models and callbacks. If a - prop type would have to be imported from a product API, the component is not - ready. Reshape the props, or leave it where it is. -2. **A real second consumer.** Not "another product could use this." A specific - product that will consume it, with someone committed to doing that. One - consumer plus a hypothetical is one consumer. -3. **Tests travel with it.** Accessibility and behavior tests come in the same - change. A component without tests is a component whose behavior nobody can - safely change later, which defeats the point of sharing it. +`src/astryxIntegration.test.ts` checks the pairing. Add a doc when you add or +change a component's props; `pnpm run check:integration` validates it. -Failing any one of these is a normal outcome. Say so in the pull request and -leave the component with its product. +## Component admission + +Most reusable-looking components should not be here. Once a component is here, +changing it means a version bump and every consumer upgrading. That cost is +worth paying only when the component is genuinely shared. + +A custom component is admitted when all of these hold: + +1. **Astryx does not already do it.** Check with `astryx search` first. +2. **Product-neutral, Astryx-shaped props.** Generic view models and callbacks, + and prop names in Astryx's vocabulary (`label`, `variant`, `isOpen`...). If + a prop type would come from a product API, the component is not ready. +3. **Built on Astryx.** Astryx primitives and Astryx tokens. No second styling + system. +4. **An origin consumer ships it today**, with no product dependency. Moves can + happen in bulk, one dependency cluster at a time. +5. **Tests travel with it.** Accessibility and behavior tests come in the same + change. A candidate without tests gets them written in the move. +6. **The name rule holds.** + +Failing one of these is a normal outcome. Say so in the pull request and leave +the component with its product. + +### The form engine + +`Form` is the one custom that keeps a non-Astryx vocabulary. It is a +form-state API (`Form.useForm`, `rules`, `valuePropName`, `FormInstance`), +not a component prop surface, and it keeps antd's names and shapes so code +written against antd's form moves over with an import rewrite. Its +`Store` and values are `any` by design, which is why ESLint's +`no-explicit-any` is off under `src/components/Form/`. + +- The state half (`FormStore`, `Field`, `List`, `validate`, `namePath`) is + a behavioural port of rc-field-form and async-validator. Match upstream + when fixing it: code depends on its quirks, and `Form.acceptance.test.tsx` + pins them. +- The item shell (`FormItemVisual`) follows the Styling rules below. Its + classes (`uic-form-item__*`) and the `data-uic-field-id` attribute are + public: consumers' tests and CSS select on them. +- Validation messages are catalog keys (`uic.Form.*`). The engine + interpolates rule values itself with `${label}`-style templates, so + `buildValidateMessages` formats each ICU message with every placeholder + standing for itself. +- The feedback glyphs are Ant Design Icons path data (MIT, see NOTICE); the + tooltip glyph is `lucide-react`'s, a dependency. + +### Dependencies of the customs + +Besides Astryx, the customs depend on `lucide-react` (glyphs), +`intl-messageformat` (catalog fallback formatting) and `@dnd-kit/core`, +`@dnd-kit/sortable`, `@dnd-kit/modifiers` and `@dnd-kit/utilities` +(drag-to-reorder in `DataGridSettingsModal`; Astryx has no sortable list). +The dnd-kit packages are exact-pinned and move together. A new runtime +dependency is recorded here and in `CHANGELOG.md`. ### Things that are never admitted API clients, endpoints, authentication, application state, stores, routing, -Tauri APIs, licensing, permissions, product-specific feature panels, and -anything that reads a product locale key. +Tauri APIs, licensing, permissions, and product-specific feature panels. ## The boundary `pnpm run check:boundary` fails the build on imports of the private AI package, -product path aliases, `react-i18next`, `@tauri-apps/*`, `zustand`, and relative -imports that escape `src/`. ESLint enforces the same set at the resolver level. +product path aliases, `react-i18next` and similar product i18n runtimes, +`@tauri-apps/*`, `zustand`, and relative imports that escape `src/`. ESLint +enforces the same set at the resolver level. -These are not style rules. Each one, if it lands, makes the package -uninstallable or unusable for at least one consumer, and it usually lands by -accident during a copy from a product repository. +ui-common's own source may import `@astryxdesign/*`. Its consumers may not: +they import Astryx through ui-common. Consumers enforce that with an ESLint +`no-restricted-imports` rule on `@astryxdesign/*`. -## Text and labels +## Strings -Never call a translation function. Every user-facing string is a prop with an -English default: +Every user-facing string is a prop. Its default comes from ui-common's catalog, +through Astryx's translator: ```tsx -interface DrawerProps { - /** Accessible label for the close control. */ - closeLabel?: string; // default: "Close" -} +// src/components/Modal/Modal.messages.ts +export const modalMessages = defineMessages({ + "uic.Modal.cancel": { defaultMessage: "Cancel", description: "Cancel button label" }, +}); + +// src/components/Modal/Modal.tsx +const t = useUicTranslator(); +const label = cancelLabel ?? t("uic.Modal.cancel"); ``` -The default keeps an untranslated consumer working and accessible. The prop -lets a translated consumer pass its own string. Adding a hard-coded English -string with no prop is a bug; so is adding a prop with no default. +- Keys are `uic..`. +- A generic action word whose translation does not depend on the component + (OK, Cancel, Confirm, Retry) is a shared key, `uic.common.`, in + `src/i18n/common.messages.ts`. Use it instead of adding a component key with + the same text. A string that names something specific to the component + ("Deselect all", "Dismiss error") gets its own key, even when a shared key + has the same English, because a translator needs its context. +- English lives in code, in the `.messages.ts` file. Spread it into + `uiCommonCatalog` in `src/i18n/catalog.ts`. +- Messages are ICU MessageFormat, which Astryx's translator formats: + `{count} selected`, `{count, plural, one {# item} other {# items}}`. Never + i18next's `{{count}}` or `_one`/`_other` suffixes. Markup does not go in a + message: a sentence with a styled part is a node-typed prop or a render + slot. +- Keep `.messages.ts` files free of React and CSS imports. The build reads the + catalog. +- Translations go in `src/i18n/locales/.json`, named like Astryx's own + locale files (`ko-KR.json`, `ja-JP.json`). Four locales Astryx has no file + for are named the way products hand them to Astryx's provider: `id-ID`, + `mn-MN`, `ms-MY`, `th-TH`. Tests reject unknown keys and other locale names. +- Every key is translated in every locale file. A key a locale cannot + translate yet goes on the allowlist in `src/i18n/useUicTranslator.test.tsx`, + which fails once the translation lands. A component moved from a product + brings that product's translations for every language it ships. +- Never import a product i18n runtime. + +A string with no prop is a bug. So is a prop with no catalog default. -## Design tokens - -Tokens are API. Adding one is a minor release. Renaming or removing one is a -major release, and needs the same migration note a renamed prop would get. +## Styling -Always give a token a fallback so a consumer that has not adopted a theme still -renders: +- Plain CSS, co-located with the component and imported by it. +- Every rule inside `@layer ui-common`. +- Class names are BEM with a `uic-` prefix: `uic-page-header__title`. +- Values come from Astryx tokens, `var(--color-...)`, `var(--spacing-...)`. + No new `--token-*` names. +- Custom properties use Astryx's naming as is, with no ui-common prefix: + - A theme value Astryx has no token for is a theme token in Astryx's + form, `--color-info`, and a component reads it with an Astryx fallback: + `var(--color-info, var(--color-accent))`. The names are the ones the + Backend.AI WebUI theme declares, so a product theme that has them needs + no setter. The list is `THEME_EXTENSION_TOKENS` in + `src/components/componentStyles.test.ts`. + - A component knob (z-index, geometry, motion, a value a prop writes) is + `---`, the component name in kebab case, the form + Astryx core uses for its own (`--dialog-dir-x`, `--spinner-color`, + `--table-sticky-background`): `--modal-z`, `--data-grid-max-height`, + `--unit-grid-group-1`. + - A value that only picks a token for a variant is not a custom + property: the variant rule reads the token. + - A name must not be one Astryx core, lab or the neutral theme declares + or reads, nor one the WebUI declares (`--bai-*`, `--token-*`, its theme + tokens). `componentStyles.test.ts` fails on a collision. +- No colour literals. A length literal is allowed only where Astryx has no + token (a media query breakpoint, a page width, a readable measure), with a + comment saying so. +- No focus styling. Astryx primitives draw focus. +- Restyle an Astryx primitive through a `uic-` class you pass it, never through + its `astryx-` class. +- No StyleX compile step for now. If one is added, it uses + `classNamePrefix: "uic"`, and any exported `defineVars` uses keys that start + with `--`. A hashed key never matches the name a consumer's compiler derives. + +jsdom drops `@layer` blocks, so a test cannot read a component's cascade back +through `getComputedStyle`. Test the rendered classes and attributes, and read +the stylesheet source when a declaration itself is the contract (see the +StatCard truncation test). + +## Tokens + +Astryx's token set is the contract. It is versioned with the Astryx pin. + +`src/legacy-tokens.css` maps the 122 old `--token-*` names onto Astryx tokens +for consumers that still read them. It is deprecated and is removed in 0.3. Do +not add names to it. + +## The Lablup theme + +The source is `src/theme/lablup/lablupTheme.ts`. After changing it, rebuild the +committed artifacts: -```css -color: var(--token-colorText, #1a1a1a); +``` +pnpm run theme:build ``` -A token's inline fallback should equal its value in `src/styles/base.css`. -About 143 inherited fallbacks do not, because they came over verbatim from -the source product where they were already unreachable. Do not add new ones that -disagree, and do not "fix" the inherited ones without treating it as the -visual change it is. +`pnpm run theme:check` fails when `src/theme/lablup/built/` is stale. It runs +in `verify`. Rebuild after every Astryx bump too: Astryx does not repair stale +pre-built CSS at runtime. -### Focus indicators take no fallback literal +## Bumping Astryx -The one exception to the rule above. A declaration that paints a focus -indicator carries no colour literal at all: +`@astryxdesign/core`, `@astryxdesign/theme-neutral` and `@astryxdesign/cli` +move together, exact-pinned. The Astryx CLI is pinned twice: as a dependency +of `packages/cli` (`@lablup/ui-common-cli`, which runs it for consumers) and as +a devDependency of the root (its `theme:*` and `check:integration` scripts). `@astryxdesign/lab` is an exact canary pin, as +both a devDependency and an optional peer. -```css -/* An outline resolves through the ring token and stops there. */ -outline: var(--token-focusRingWidth, 2px) var(--token-focusRingStyle, solid) - var(--token-focusRingColor, var(--token-colorPrimary)); +`ui-common sync-astryx` does the bump: -/* A ring drawn as the element's own border mixes the accent with the text - * colour, inline in the longhand. */ -border-color: color-mix(in srgb, var(--token-colorPrimary) 70%, var(--token-colorText)); +``` +node packages/cli/bin/ui-common.mjs sync-astryx 0.6.3 --dry-run # plan, and Astryx's codemods on src/ as a dry run +node packages/cli/bin/ui-common.mjs sync-astryx 0.6.3 --lab 0.6.3-canary.abc1234 ``` -WCAG 2.2 SC 1.4.11 requires the indicator of a component state to clear 3:1 -against the surface it lands on. A fixed literal cannot know that surface, so -"renders without a theme" and "meets the contrast floor" are not both -achievable from a constant, and for an accessibility affordance the second one -wins. Never paint a focus indicator from the bare `--token-colorPrimary` -either: that token is chosen for brand, and it measures as low as 2.37:1 in -this package's own default palette. - -Two mechanical points that are easy to get backwards: - -- **`color-mix()` belongs in a longhand, never in a token that feeds a - shorthand.** A custom property holds an unparsed token stream, so on an - engine without `color-mix()` the property substitutes successfully and only - then invalidates its consumer at computed-value time. Feeding that to - `outline` yields `outline-style: none` and no ring at all, with the `var()` - fallback never firing because the property was never guaranteed-invalid. In a - longhand the failure is the opposite and benign: the declaration is dropped - at parse time and the cascade keeps the element's resting border. This is why - `--token-focusRingColor` is declared as a resolved literal in - `src/styles/base.css` rather than as a `color-mix()` call. -- **A `border-color` plus `outline: none` in the same rule block is a focus - indicator**, even though the block never writes an `outline` colour. Grep for - `outline` alone will not find it; the two declarations have to be read - together. - -Never add a token to a component without adding it to `src/styles/base.css`. -A token that only exists in a product's theme file makes the component render -correctly there and nowhere else, which is the failure mode this package -exists to prevent. +It moves the pins in both `package.json` files, runs `pnpm install`, `pnpm run gen:exports` and +`pnpm run theme:build`, runs Astryx's own codemods on `src/` (a dry run, then +applied), runs the tests, and records the Astryx codemods consumers need in +`packages/cli/codemods//upstream.json` (`--as ` picks the version; +release under that version). `ui-common upgrade` runs them for a consumer that +crosses it, with the `@lablup/ui-common` specifiers swapped for Astryx's so +Astryx's codemods recognise them. It runs Astryx's codemods before the tests, +since a rename Astryx ships a codemod for would otherwise fail them first. + +Then, by hand: + +1. Read the `gen:exports` diff. Update `exports.exclude.json` if the generator + reports a stale entry or a new data export. +2. Re-sync or delete each fork ("Forks of Astryx components"); + `src/forks/forks.test.ts` fails until you do. +3. `pnpm run verify`. +4. Note new and removed subpaths in `CHANGELOG.md`. A removed subpath is a + breaking change. + +## The upgrade tool + +`ui-common upgrade` runs the steps in `packages/cli/codemods/registry.mjs`, keyed by the +ui-common version that made the change, over a consumer's source. + +- **0.1 → 0.2** (`packages/cli/codemods/0.2/`) takes all of its data from + [`packages/cli/migration/0.1-to-0.2.json`](packages/cli/migration/0.1-to-0.2.json): replacement + imports, prop renames, value maps, required packages, stylesheet entry + points, class renames and the manual notes its TODO markers quote. Change + the map, not the codemods, when the migration changes. + `packages/cli/codemods/0.2/legacy-classes.json` lists the 0.1 class names; regenerate it + from a 0.1 checkout with `packages/cli/scripts/extract-legacy-classes.mjs`. +- **Upstream steps** are `packages/cli/codemods//upstream.json`, written by + `sync-astryx`. + +A codemod that cannot prove a rewrite safe leaves the code as it was, with a +`TODO(ui-common-upgrade):` comment and a report entry. `packages/cli/test/upgrade/` runs +every step over fixture projects and compares the result with `expected/`, +report included. After an intended change: -## Styling +``` +UPDATE_FIXTURES=1 pnpm vitest run packages/cli/test/upgrade +``` -Component CSS lives next to the component and is imported by it, so a subpath -import pulls only that component's styles. Theme families under -`src/styles/themes/` are standalone entry points that no component imports. -Do not add an import that pulls a theme file into a component; it would make -every consumer ship every theme. +and read the diff. Fixtures are consumer code: keep them free of product +names, like the rest of this repository. ## Versioning -Semver, with the public surface defined as: exported components and their -props, exported hooks, exported types, the `exports` map, the `--token-*` -contract, and the CSS entry point paths. +Semver. The public surface is: exported components and their props, exported +hooks and types, the `exports` map, the Astryx version (it defines the token +contract), and the CSS entry point paths. -| Change | Release | -| --------------------------------------------------------- | ------- | -| New component, new optional prop, new token | Minor | -| Bug fix that keeps the rendered contract | Patch | -| Removed or renamed prop, component, token, or export path | Major | -| Changed default value that alters rendering | Major | -| Raised React peer range floor | Major | +| Change | Release | +| ------------------------------------------------------ | ------- | +| New component, new optional prop, new mirrored subpath | Minor | +| Bug fix that keeps the rendered contract | Patch | +| Removed or renamed prop, component, or export path | Major | +| Astryx bump that removes or renames anything | Major | +| Changed default value that alters rendering | Major | +| Raised React or StyleX peer floor | Major | -While the API is migrating across products, releases are prereleases -(`0.1.0-alpha.N`). Stable `1.0.0` waits until the first consumer and at least -one other have validated the contracts in a shipped build. Publishing -`1.0.0` before a second consumer exists would freeze props that only one -product has ever exercised. +While the API is migrating, releases are prereleases (`0.2.0-alpha.N`). +Before 1.0, a breaking change bumps the minor version. ## Pull requests -Run `pnpm run verify` before pushing. It is what CI runs, and it ends with -packing the real tarball and installing it into the clean external project -under `fixture/`. +Run `pnpm run verify` before pushing. It is what CI runs. CI also installs the +packed tarball into the clean project under `fixture/` and builds it. -For a component admission, say in the description which product is the second -consumer and who is doing that migration. +For a component admission, say in the description which product ships it +today and who does the move. ## Releasing @@ -166,9 +488,7 @@ consumer and who is doing that migration. ## Pre-public review -This repository is Internal now and becomes public once the initial import has -been reviewed. Until then, every change is held to a public bar: no internal -hostnames, credentials, customer names, unreviewed fixtures, or assets whose -redistribution rights under Apache-2.0 have not been confirmed. Check comments -and test fixtures too, not just the implementation. Those are where internal -details survive a copy. +This repository is held to a public bar: no internal hostnames, credentials, +customer names, unreviewed fixtures, or assets whose redistribution rights +under Apache-2.0 have not been confirmed. Check comments and test fixtures too, +not just the implementation. Those are where internal details survive a copy. diff --git a/NOTICE b/NOTICE index 1d0fa73..db54839 100644 --- a/NOTICE +++ b/NOTICE @@ -8,3 +8,65 @@ Portions of this package were extracted from an existing Lablup product frontend. The extraction is a clean import: no upstream git history is published here, and the migration record is kept in that product's own repository. + +The form item's validation feedback glyphs +(src/components/Form/feedbackIcons.tsx, shipped in dist/components/Form) use +path data from Ant Design Icons (https://github.com/ant-design/ant-design-icons), +licensed under the MIT License: + +-------------------------------------------------------------------------------- +MIT LICENSE + +Copyright (c) 2018-present Ant UED, https://xtech.antfin.com/ + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +-------------------------------------------------------------------------------- + +The forks of Astryx components under src/forks/ (shipped in dist/forks/), +including their generated *.styles files, are copies of Astryx source from +@astryxdesign/core and @astryxdesign/lab (https://github.com/facebook/astryx), +with fixes applied; src/forks/provenance.json records each one's origin. +src/components/PagedSelector/PanelSearchInput.tsx is adapted from +@astryxdesign/core's Field/PanelSearchInput and utils/interactionModality. +Astryx is licensed under the MIT License: + +-------------------------------------------------------------------------------- +MIT License + +Copyright (c) 2026 Meta Platforms, Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +-------------------------------------------------------------------------------- diff --git a/README.md b/README.md index f096210..576f7da 100644 --- a/README.md +++ b/README.md @@ -1,34 +1,104 @@ # @lablup/ui-common -Product-neutral UI components and design tokens shared across Lablup products. +Lablup's UI layer on top of [Astryx](https://github.com/facebook/astryx). + +It gives Lablup products three things through one dependency: + +- **Astryx itself**, re-exported 1:1. Every Astryx subpath exists here under the + same name. +- **The Lablup brand theme**, as an Astryx theme. +- **A few components of its own**, built on Astryx, for patterns Astryx does not + cover. Consumers are Lablup product frontends, including [all-smi](https://github.com/lablup/all-smi). The package holds presentation only. It has no API client, no application state, no router, and no -desktop-shell integration, because those differ per product and are what makes -a component impossible to share. +desktop-shell integration. ## Install ``` -pnpm add @lablup/ui-common +pnpm add @lablup/ui-common @stylexjs/stylex +``` + +That is npmjs, which needs no authentication. + +Peer dependencies: + +- `react` and `react-dom` ^19.2. Components use `useEffectEvent`, which + React 19.2 made stable. +- `@stylexjs/stylex` ^0.19. It is the one runtime copy that Astryx, ui-common + and your own StyleX code share. +- `@astryxdesign/lab`, optional. Install it only if you use + `@lablup/ui-common/lab`. It is pinned to the exact canary ui-common is built + against, and it needs the override below. + +Astryx itself (`@astryxdesign/core`, `@astryxdesign/theme-neutral`) comes in +as ui-common's own dependencies, pinned exactly. The `ui-common` bin and the +Astryx CLI it wraps are a separate dev-time package, `@lablup/ui-common-cli` +([The ui-common CLI](#the-ui-common-cli)). +`lucide-react` (the icon set Astryx's neutral theme already depends on) and +`intl-messageformat` come in the same way. +Do not add them to your project. ui-common owns the Astryx version. Two copies +of Astryx means two copies of its React contexts, and components stop seeing +the theme. + +### pnpm 10 and later + +`@astryxdesign/core` and `@astryxdesign/cli` have `postinstall` scripts. pnpm +10 and later run no dependency's install scripts until the project decides +about each one: pnpm 10 installs and prints a warning, pnpm 11 fails +`pnpm install` with `ERR_PNPM_IGNORED_BUILDS`. The scripts only print an +`astryx init` hint, so decline them, in `pnpm-workspace.yaml`: + +```yaml +allowBuilds: + "@astryxdesign/cli": false + "@astryxdesign/core": false +``` + +This repository's own `pnpm-workspace.yaml` does the same. npm runs the +scripts, or asks about them, and needs nothing. + +### With `@lablup/ui-common/lab` + +The lab canary declares an exact peer on the core canary it was cut from, not +on the core ui-common pins. Without an override, pnpm installs that canary +core beside ui-common's, npm nests it under lab, and `@lablup/ui-common/lab` +runs on the second copy. Add the override for your package manager, next to +`@astryxdesign/lab` itself (`ui-common upgrade` adds both when it moves a +Drawer to lab): + +pnpm, in `pnpm-workspace.yaml` (pnpm 10 and later read settings only from +there): + +```yaml +overrides: + "@astryxdesign/lab>@astryxdesign/core": "0.6.2" ``` -That is npmjs, which needs no authentication and is the right route for -essentially everyone, including open-source consumers and forked CI. +npm, in the root `package.json`: + +```json +"overrides": { + "@astryxdesign/lab": { "@astryxdesign/core": "0.6.2" } +} +``` -`react` and `react-dom` are peer dependencies. Version 18 and 19 are both -supported. +The version is the `@astryxdesign/core` that ui-common pins; it moves with +each ui-common release that moves Astryx. Then `pnpm why @astryxdesign/core` +(or `npm ls @astryxdesign/core`) lists one version. pnpm may still warn that +lab's peer is unmet; with the override that is expected. A project that already +lists `@astryxdesign/core` itself, at the same version, gets the same effect +from pnpm resolving the peer to its own copy. ### The GitHub Packages mirror The same versions are also published to GitHub Packages for projects that -already authenticate to GitHub. It is a mirror, not a different package, so -there is no reason to prefer it unless your organization requires it. +already authenticate to GitHub. It is a mirror, not a different package. -GitHub Packages requires authentication **even for public packages**, which is -why it is not the default route here. To use it, point the scope at that -registry: +GitHub Packages requires authentication **even for public packages**. To use +it, point the scope at that registry: ``` # .npmrc @@ -50,106 +120,458 @@ env: Locally, use a personal access token with `read:packages`, in your user `~/.npmrc` and never in a project file. +## Set up + +Declare the layer order first in your app's entry stylesheet, then load the +stylesheets in this order: + +```css +@layer reset, theme, base, astryx-base, astryx-theme, ui-common, components, utilities; + +@import "@lablup/ui-common/reset.css"; +@import "@lablup/ui-common/astryx.css"; +@import "@lablup/ui-common/theme/lablup/theme.css"; +@import "@lablup/ui-common/ui-common.css"; +/* Only if you use @lablup/ui-common/lab: */ +@import "@lablup/ui-common/lab/lab.css"; +``` + +Every stylesheet ui-common ships starts with the same `@layer` statement too, +component sheets included. A layer's place is fixed by the first stylesheet +that names it, and a component's sheet (imported by its module) usually +reaches the page before your entry stylesheet does. Without the statement in +the component sheets, `ui-common` would be the lowest layer and Astryx's base +styles would beat ui-common's. Declaring it in your entry stylesheet as well +is still recommended: it documents the order, and it places your own +`components` and `utilities` layers wherever your sheets load. Repeating an +identical statement changes nothing. + +Wrap the app in the theme: + +```tsx +import { Theme } from "@lablup/ui-common"; +import { lablupTheme } from "@lablup/ui-common/theme/lablup/built"; + + + +; +``` + +`` is required. `theme.css` is scoped to +`[data-astryx-theme="lablup"]`, which only `` sets, so without it the +app renders Astryx's default palette. No error is raised. + +Dark mode is the `mode` prop: ``, +`"light"`, or `"system"` (the default, which follows the OS). The root +`` owns `html[data-theme]`: it sets `light` or `dark`, removes the +attribute for `system`, and removes it on unmount. A 0.1-style toggle that +writes its own value there, such as `data-theme="orange-dark"`, no longer +works. Switch `mode` instead. + +`/theme/lablup/built` pairs with `theme.css` and injects nothing at runtime. +`@lablup/ui-common/theme/lablup` is the same theme as source, for runtime +injection or for extending it with `defineTheme`. Use one or the other. +Astryx's neutral theme is mirrored the same way at `/theme/neutral`. + +The theme names its font family (Ubuntu Sans, then Pretendard Variable) but +does not load it. Loading fonts is the app's job. + +### Tests (Vitest with jsdom) + +ui-common's modules import their stylesheets, and Node cannot load a `.css` +import from `node_modules`. Vitest externalises dependencies by default, so a +test that imports ui-common fails with `Unknown file extension ".css"`. +Let Vitest process the package instead: + +```ts +// vitest.config.ts +export default defineConfig({ + test: { + environment: "jsdom", + server: { deps: { inline: [/@lablup\/ui-common/] } }, + }, +}); +``` + +### Two copies + +In development, ui-common warns in the console when a second copy of itself +is loaded, and says whether the copies also run on separate copies of +`@astryxdesign/core`. Two copies do not share the modal stack, and with two +Astryx cores the `Theme` and i18n providers stop reaching components. Dedupe +until `pnpm why @lablup/ui-common` lists one version. + +### Layers + +| Layer | Owner | +| ------------------------- | ----------------------------------- | +| `reset`, `theme`, `base` | Astryx reset, your own base rules | +| `astryx-base` | Astryx component styles | +| `astryx-theme` | theme overrides, including Lablup's | +| `ui-common` | ui-common's own styles | +| `components`, `utilities` | yours | + +ui-common's styles beat Astryx's base and theme styles for the primitives they +wrap. Your `components` layer beats ui-common. Unlayered rules beat all of it. + ## Use -Import from the root, or from a component subpath when you want the smallest -possible graph: +Astryx components come from the root or from their own subpath, exactly as in +Astryx: ```tsx -import { Button, StatusTag } from "@lablup/ui-common"; -import { Drawer } from "@lablup/ui-common/components/Drawer"; +import { Button, Text } from "@lablup/ui-common"; +import { Table } from "@lablup/ui-common/Table"; +import { useClipboard } from "@lablup/ui-common/hooks"; ``` -Styling is opt-in and split so that importing a component never drags in every -theme: +StyleX users import tokens from the `.stylex` subpath. The StyleX compiler +recognises a theme import by that suffix, so the root barrel will not do: ```ts -// The token contract the components resolve against. Required. -// It also carries the default palette, so this alone is a working theme. -import "@lablup/ui-common/styles/base.css"; +import { colorVars, spacingVars } from "@lablup/ui-common/theme/tokens.stylex"; +``` + +Lab components live at `@lablup/ui-common/lab`. + +ui-common's own components come from the root, and from the subpaths below: + +```tsx +import { PageHeader, PageLayout, StatCard } from "@lablup/ui-common"; +import { Modal } from "@lablup/ui-common/Modal"; +``` + +| Component | What it is | Subpath | +| -------------------------------------------------------------- | ----------------------------------------------------------- | -------------------------------- | +| `Modal` | The dialog, in place of Astryx `Dialog`. See below. | `/Modal` | +| `PageLayout` | A page's width clamp (`standard`, `wide`, `full`) | `/components/PageLayout` | +| `PageHeader` | A page's title, description, actions and error banner | `/components/PageHeader` | +| `StatCard` | A dashboard metric, on Astryx `Card` | `/components/StatCard` | +| `ErrorState` | A full-area error with recovery actions | `/components/ErrorState` | +| `SkeletonCard`, `SkeletonText`, `SkeletonChart`, `SkeletonRow` | Loading placeholders drawn with Astryx `Skeleton` | `/components/Skeleton` | +| `SmoothHeight` | Animates a container toward its content's height | `/components/SmoothHeight` | +| `DigitPopIn` | A number whose characters pop in, one after another | `/components/DigitPopIn` | +| `CountBadge` | A count or dot overlaid on its child's corner | `/components/CountBadge` | +| `DoubleBadge` | A run of Badges welded into one chip | `/components/DoubleBadge` | +| `BooleanToken` | An on/off value as a Token | `/components/BooleanToken` | +| `IconWithTooltip` | A focusable glyph that explains itself in a Tooltip | `/components/IconWithTooltip` | +| `ImageWithFallback` | An image that renders a fallback node when it fails to load | `/components/ImageWithFallback` | +| `NotificationStack` | Floating notices with task progress and actions | `/components/NotificationStack` | +| `OverlayScrollbar` | A persistent scroll thumb drawn over a scroll container | `/components/OverlayScrollbar` | +| `ConfirmPopover` | A one-click confirmation anchored to its trigger | `/components/ConfirmPopover` | +| `PagedSelector` | A searchable selector over options loaded a page at a time | `/components/PagedSelector` | +| `SelectionLabel` | "3 selected", with a button that clears the selection | `/components/SelectionLabel` | +| `UncontrolledInput` | A field that reports its value on Enter or blur | `/components/UncontrolledInput` | +| `AlertModal` | An alert dialog, in place of Astryx `AlertDialog` | `/AlertModal` | +| `DeleteConfirmModal` | Confirms a deletion, with typed confirmation when needed | `/components/DeleteConfirmModal` | +| `StepNumberInput`, `NumberStepper` | A number field that steps along a list of values | `/components/StepNumberInput` | +| `BoardItemTitle` | A dashboard panel's sticky title row | `/components/BoardItemTitle` | +| `Statistic` | A metric with a caption, a large value and a notched bar | `/components/Statistic` | +| `DividedRow` | A wrapping row with dividers between neighbours on a line | `/components/DividedRow` | +| `TokenList` | Values inline, the rest behind `+N` on hover | `/components/TokenList` | +| `TokenRow` | Tokens cut off with "and N more" | `/components/TokenRow` | +| `NotificationItem` | The title, description, actions and footer of one notice | `/components/NotificationItem` | +| `UnitGrid`, `UnitGridSkeleton` | Groups of unit squares on one lattice, with a hover card | `/components/UnitGrid` | +| `ColorPicker` | A hex colour field on the platform colour input | `/components/ColorPicker` | +| `Form` and its hooks | A form engine with antd's form API. See below. | `/Form` | +| `BulkEditFormItem` | A form item that edits one field across many records | `/components/BulkEditFormItem` | +| `DataGrid`, `DataGridSettingsModal`, `DataGridExportModal` | A table with paging, sorting, selection and column settings | `/components/DataGrid` | +| `BulkErrorModal` | The failed items of a bulk operation, in a grid | `/components/BulkErrorModal` | +| `ProgressWithLabel` | A bar that carries its label and value label | `/components/ProgressWithLabel` | +| `TextHighlighter` | Marks a search keyword in a string | `/components/TextHighlighter` | +| `CountdownBorder` | A border that fills as a countdown to a refresh | `/components/CountdownBorder` | +| `DoubleToken` | A run of Tokens welded into one chip | `/components/DoubleToken` | +| `ListBanner` | A Banner that lists items, scrolling past a height | `/components/ListBanner` | +| `usePrefersReducedMotion` | The `prefers-reduced-motion` media query, as a hook | root only | + +Their styles live in `@layer ui-common`, under `uic-` class names. + +### Modal + +`Modal` takes every prop Astryx `Dialog` takes, so a `Dialog` call site moves +over by renaming the import: `@astryxdesign/core/Dialog` becomes +`@lablup/ui-common/Modal`, `Dialog` becomes `Modal`, `DialogProps` becomes +`ModalProps`. `DialogHeader`, `DialogPosition`, `DialogPurpose` and +`DialogVariant` are re-exported unchanged, and as `ModalHeader`, +`ModalPosition`, `ModalPurpose` and `ModalVariant`. One difference is visible +to a caller: `ref` reaches the `div` that carries `role="dialog"`, not a +`` element. + +What it adds: + +- It renders into a `document.body` portal instead of the browser's top layer, + so whatever the app layers above the modal band, such as a notification + stack, stays visible. The band is `z-index` 1100 to 10999 by default; + `configureModalZIndex({ base, step, max })` moves it. +- While a modal is open, the topmost one is `aria-modal="true"` and the rest + of the page is `inert`, as `showModal()` would make it. Two things stay + reachable: modal roots, and elements marked `data-uic-modal-live` + (`MODAL_LIVE_ATTRIBUTE`). `NotificationStack` marks itself, so notices over a + modal can still be read and dismissed; mark your own live region the same + way, and call `refreshModalBackground()` if it mounts while a modal is open. + An `inert` the page set itself is left as it was. +- A modal opened from inside another paints above it. Only the topmost one + traps focus and takes Escape; covered ones are `inert`. Other portalled + surfaces (a drawer) join the same stack with `useModalLevel`, which also + keeps them out of the inert background. +- Content mounts on first open and stays mounted while closed. + `unmountOnClose` drops it. `afterOpenChange` reports each open and close. +- With `title`, `onAction` or `footer`, it lays out a header, the body and a + footer with a primary action and Cancel. `onAction` may return a promise; the + button stays pending until it settles. It does not close the modal. -// Optional, and only if you switch themes at runtime through [data-theme]. -import "@lablup/ui-common/styles/themes/orange-dark.css"; +```tsx + { + await rename(name); + setIsOpen(false); + }} +> + + ``` -The package ships the theming mechanism and one default palette, the Lablup -brand orange. A product with its own visual identity defines its own -`[data-theme]` blocks over the same 119 token names and ships them itself, -rather than the package accumulating everyone's palettes. The source product -does exactly that with its five families. +### Form + +`@lablup/ui-common/Form` is a form engine with antd's form API: `Form`, +`Form.Item`, `Form.List`, `Form.ErrorList`, `Form.Provider`, +`Form.useForm`, `Form.useWatch`, `Form.useFormInstance` and +`Form.Item.useStatus`, with antd's rules (`required`, `message`, +`validator`, `type`, `min`, `max`, `pattern`, `whitespace`, +`warningOnly`). It keeps antd's names on purpose: it is a form-state API, +not a component, so code written against antd's form moves over by changing +the import. The item shell renders on Astryx tokens. + +```tsx +import { Form } from "@lablup/ui-common/Form"; -Component CSS travels with the component: importing `Button` brings -`Button.css` with it, so a subpath import pulls that component's styles and no -others. The two entry points above are the only stylesheets you import by hand, -and `base.css` is the one you must not skip, since it carries the tokens every -component resolves against. +const [form] = Form.useForm(); -## Text is yours, not ours +
+ + + +
; +``` -No component calls a translation function or reads a locale key. Every -user-facing string is a prop with an English default: +- Validation messages come from ui-common's catalog in the locale of the + nearest `InternationalizationProvider` (see Strings). `FormConfigProvider` + sets `validateMessages`, `requiredMark` and `optionalLabel` app-wide; a + form's own `validateMessages` wins over both. +- A control shows its item's validation state by reading + `Form.Item.useStatus()` or `FormItemInputContext`. +- `form.scrollToField` and `scrollToFirstError` find the control by + `data-uic-field-id`, which `Form.Item` puts on its child; Astryx inputs + keep `data-*` attributes. +- The DOM is `.uic-form` (with `data-layout`) and `.uic-form-item` with + `__label`, `__label--required`, `__control`, `__control-input`, + `__explain`, `__explain-error`, `__explain-warning` and `__extra`. + The item carries `data-layout`, `data-size` and `data-status`. These + class names are the public hooks for tests and product CSS. +- Three custom properties adjust it: `--form-item-margin-bottom` + (default `--spacing-6`), `--form-item-gap` (label to control in a + vertical item, default `--spacing-2`) and `--form-item-line-height` + (default `--text-body-leading`). Help, extra, the tooltip glyph and the + optional suffix take the theme's `--color-text-description` where the + theme declares one, else `--color-text-secondary`. + +### What is hidden + +A few Astryx subpaths are deliberately not re-exported. They are listed, with +the reason and the replacement, in [`exports.exclude.json`](exports.exclude.json). +Today that is `Dialog` (use `Modal`), `AlertDialog` (use `AlertModal`) and two +Astryx CLI data files. + +### Name rule + +A ui-common component never shares a name with an Astryx core or lab export. +If a name is `Button`, it is Astryx's `Button`. The same holds the other way: +`DialogHeader` from `@lablup/ui-common/Modal` is Astryx's own `DialogHeader`. + +## Strings + +ui-common's components show a few built-in strings. Every one of them is also a +prop, and an explicit prop always wins. + +The defaults resolve through Astryx's own `InternationalizationProvider`. +Supply translations once, at the provider: ```tsx - - ... - +import { InternationalizationProvider } from "@lablup/ui-common/i18n"; +import { mergeMessages, uiCommonMessages } from "@lablup/ui-common/i18n-catalog"; +import astryxKo from "@lablup/ui-common/locales/ko-KR.json"; + + + +; ``` -A consumer with no i18n setup gets working, accessible English. A consumer with -translations passes them in. Neither ends up depending on the other's locale -bundle. +- `@lablup/ui-common/locales/.json` is Astryx's own catalog. +- `@lablup/ui-common/ui-common-locales/.json` is ui-common's. +- Without a provider, everything renders in English. + +ui-common never uses a product i18n runtime. -## Design tokens are API +## Upgrading from 0.1 -Components resolve their colors, spacing, motion, radii, and shadows from -`--token-*` custom properties. Those properties are a versioned part of the -public surface: renaming or removing one is a breaking change, exactly like -renaming a prop. Every token a component reads has a fallback, so a consumer -that adopts a component without adopting a theme still renders. +Removed in 0.2, each replaced by Astryx: -## What gets in +| 0.1 | 0.2 | +| ------------- | ------------------------------------- | +| `Badge` | `Badge`, or `Token` for a chip | +| `BaseCard` | `Card`, or `ClickableCard` | +| `Button` | `Button`, or `IconButton` | +| `DataTable` | `Table` | +| `Drawer` | `Drawer` from `@lablup/ui-common/lab` | +| `EmptyState` | `EmptyState` | +| `ProgressBar` | `ProgressBar` | +| `Select` | `Selector` | +| `Skeleton` | `Skeleton` (the composites stay) | +| `StatusTag` | `StatusDot` | +| `Tabs` | `TabList` | +| `Tooltip` | `Tooltip` | -The package is not a home for every reusable-looking component. A component is -admitted when all three hold: +The kept components keep their 0.1 props. Their class names moved to `uic-` +(`page-header` is `uic-page-header`), so CSS or tests that select the old +names need updating. [`packages/cli/migration/0.1-to-0.2.json`](packages/cli/migration/0.1-to-0.2.json) +lists every import, prop, class and stylesheet change in a form the upgrade +tool reads. -1. It is product-neutral, taking generic view models and callbacks rather than - any product's API types. -2. It has a concrete consumer in more than one product, present or committed. -3. It brings its accessibility and behavior tests with it. +Before you start, read [docs/migrating-to-0.2.md](docs/migrating-to-0.2.md): +the problems the first app hit when it moved onto 0.2, and a checklist. -Anything that fails one of these stays with the product that needs it. See -[CONTRIBUTING.md](CONTRIBUTING.md). +Let the upgrade tool do the mechanical part. It ships in +`@lablup/ui-common-cli`, so run it one-off from the project still on 0.1: + +``` +pnpm dlx @lablup/ui-common-cli@next upgrade --from 0.1 --dry-run # writes nothing; prints the changes and the report +pnpm dlx @lablup/ui-common-cli@next upgrade --from 0.1 # applies it +``` + +(`npx @lablup/ui-common-cli@next upgrade --from 0.1` with npm.) Keep the +`@next` while 0.2 is in prerelease: the CLI has published only prereleases, +which go to the `next` dist-tag, and npm points `latest` at a package's first +publish, so a bare `@lablup/ui-common-cli` resolves to its first alpha. Drop +`@next` once 0.2.0 is published. It bumps +`@lablup/ui-common` in `package.json` and adds `@lablup/ui-common-cli` as a +devDependency at the same version; then run your install, and later upgrades +are `pnpm exec ui-common upgrade --from `. + +It moves the imports, reshapes the props it can prove safe, rewrites the +`styles/base.css` import into the 0.2 stylesheet set (or, in an app that +never imported it, imports that set first in the app's entry script), wraps +the app's root render (`createRoot(…).render()`) in +`` when no module uses `` yet, and updates +`package.json`. Code that imports a moved component through a module of your +own that re-exports it (a barrel such as `@/components/common`, found through +relative paths and your tsconfig `paths`) gets the same rewrite. A component of +yours that wraps one and takes its props is listed in the report instead: its +props are yours to change. Everything else is a `TODO(ui-common-upgrade)` comment in the +code and a line in `ui-common-upgrade-report.md`, together with the CSS, DOM +queries, tests and module mocks that still name 0.1 classes, custom +properties of yours that Astryx declares too, and code that switches 0.1 +themes through `data-theme`. Steps the app cannot work without (the +stylesheets or ``, where the upgrade could not add them) open the +report under "Action required". + +Deprecated in 0.2, removed in 0.3: + +- **`--token-*` custom properties.** Astryx's token set is the contract now. + `@lablup/ui-common/legacy-tokens.css` declares every old name as the matching + Astryx token, inside `@layer ui-common`, so code that reads them keeps + working while it moves. +- **`styles/base.css` and `styles/themes/*.css`.** Use the Lablup theme. No + ui-common component reads them any more; they stay one release so existing + imports keep resolving while the upgrade tool rewrites them. ## What is deliberately absent - API clients, endpoints, and authentication. - Application state, stores, and routing. - Tauri APIs and plugins, and any desktop-shell assumption. -- Product locale keys and any application-global i18n instance. +- Product i18n runtimes and product locale keys. - Anything from the private AI companion package. The dependency runs the other way, and CI fails if it ever reverses. +## The ui-common CLI + +The `ui-common` bin is its own package, `@lablup/ui-common-cli`, released at +the same version as `@lablup/ui-common` and taking it as a peer. It wraps the +Astryx CLI it pins, so a project needs no `@astryxdesign/*` dependency of its +own to use it. Being separate keeps the Astryx CLI and the codemod toolchain +(jscodeshift, postcss) out of a production install, the way Astryx splits +`@astryxdesign/cli` from `@astryxdesign/core`. Keep it a devDependency pinned +to the same version as `@lablup/ui-common`, and bump the two together: + +``` +pnpm add -D @lablup/ui-common-cli@ +``` + +Under pnpm 11, allow or decline the Astryx packages' postinstall (it only +prints an `astryx init` nudge) in `pnpm-workspace.yaml`, or the install stops +with `ERR_PNPM_IGNORED_BUILDS` (`ui-common upgrade` adds the entries a pnpm +project does not decide yet): + +```yaml +allowBuilds: + "@astryxdesign/core": false + "@astryxdesign/cli": false +``` + +Then: + +``` +pnpm exec ui-common component Button # any Astryx command: component, search, +pnpm exec ui-common search "date picker" # docs, build, template, theme, hook, ... +pnpm exec ui-common agents --write AGENTS.md +pnpm exec ui-common upgrade --from 0.1 --dry-run +``` + +| Command | What it does | +| ------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `ui-common …` | Runs the pinned Astryx CLI and rewrites its output to ui-common: `@astryxdesign/core/` is `@lablup/ui-common/`, `@astryxdesign/lab` is `@lablup/ui-common/lab`, `@astryxdesign/theme-neutral` is `@lablup/ui-common/theme/neutral`, and commands read `ui-common …`. A name ui-common hides gets a note ("Use Modal, not Dialog"). `--json` output stays valid JSON; the note goes to stderr. The exit code is Astryx's. | +| `ui-common astryx …` | The same, without rewriting. | +| `ui-common agents [--write ] [--check]` | Prints the agent block: Astryx's `init --features agents` block, rewritten, plus ui-common's rules. It sits between `` and ``, which `astryx init` never touches. `--write` replaces the block in place and keeps the rest of the file; `--check` exits 1 when it is stale. | +| `ui-common upgrade [--from ] [--to ] [--dry-run] [--diff] [--report ] [--scan ]… [paths…]` | Runs the codemods between two ui-common versions over `src/` (or `paths`), updates `package.json`, and writes `ui-common-upgrade-report.md` (a `--dry-run` writes nothing and prints the report, unless `--report` names a file). The report's manual-review findings come from the whole project (tests, e2e specs, scripts), or only from the `--scan` paths. `--from` defaults to the version `package.json` declares, `--to` to the CLI's own (the ui-common version it ships with). | +| `ui-common sync-astryx [--lab ] [--as ] [--dry-run]` | Maintainers only; see [CONTRIBUTING.md](CONTRIBUTING.md#bumping-astryx). | + +Exit codes: a passed-through command exits with Astryx's code. ui-common's own +commands exit 0 on success, 1 on a failed check or run, and 2 on bad arguments. + +`component`, `search` and the other lookups find `@astryxdesign/core` through +the project's `@lablup/ui-common`, so they work in a project that depends on +ui-common (and the CLI) alone. Without the CLI installed, any command runs +one-off as `pnpm dlx @lablup/ui-common-cli@next ` (or `npx`; plain +`@lablup/ui-common-cli` once 0.2.0 is published). + +ui-common is also an Astryx CLI integration: `ui-common docs ui-common` (or +`astryx docs ui-common`) explains the layer, and `ui-common component Modal` +documents ui-common's own components. + ## Development ``` pnpm install -pnpm run verify # typecheck, lint, format, boundary, test, build, pack +pnpm run verify # typecheck, lint, format, boundary, theme, test, build, pack, integration pnpm run test:watch ``` -`pnpm run verify` is what CI runs. It ends by packing the real tarball and -asserting that every path in the exports map resolves inside it, then a -separate job installs that tarball into a clean external React project under -`fixture/`. Building green and being installable are different claims, and the -second is the one consumers depend on. +See [CONTRIBUTING.md](CONTRIBUTING.md) and [docs/astryx.md](docs/astryx.md). ## Provenance The initial component and token slice was extracted from an existing Lablup product frontend. The import is clean: no upstream git history was published -here. The detailed migration record, including the source commit for each -imported file, lives in that product's own repository. +here. ## License -[Apache-2.0](LICENSE). See [NOTICE](NOTICE). +[Apache-2.0](LICENSE). See [NOTICE](NOTICE). Astryx is MIT-licensed by Meta +Platforms, Inc. It is a dependency, not vendored. diff --git a/astryx.integration.d.mts b/astryx.integration.d.mts new file mode 100644 index 0000000..1255b21 --- /dev/null +++ b/astryx.integration.d.mts @@ -0,0 +1,5 @@ +/** Types for the Astryx CLI integration manifest, so tests can import it. */ +import type { AstryxIntegration } from "@astryxdesign/cli/authoring"; + +declare const manifest: AstryxIntegration; +export default manifest; diff --git a/astryx.integration.mjs b/astryx.integration.mjs new file mode 100644 index 0000000..7fa7ceb --- /dev/null +++ b/astryx.integration.mjs @@ -0,0 +1,35 @@ +/** + * What @lablup/ui-common contributes to the Astryx CLI (FR-4051). + * + * A consumer that lists @lablup/ui-common as a dependency gets this loaded + * implicitly (Astryx CLI 0.6.1+): `astryx docs ui-common` serves the topic + * below, `astryx search` finds it, and `astryx init --features agents` + * appends the `agentDocs` lines to the managed agent block. + * + * `components` holds ui-common's component docs (`astryx component Modal`). + * The CLI pairs each `{Name}.doc.mjs` with a same-stem `{Name}.tsx`, and + * `doctor integration validate` fails a doc without one. The manifest cannot + * point that source elsewhere, and the tarball ships no source, so each doc + * sits beside a one-line `{Name}.tsx` that re-exports the component from the + * package. `astryx swizzle` on one copies that line, not an implementation. + * + * Identity (name, version) comes from package.json. Each root is shipped in + * the tarball (see `files` in package.json). `pnpm run check:integration` + * validates the manifest and proves the packed package carries it. + * + * @type {import('@astryxdesign/cli/authoring').AstryxIntegration} + */ +export default { + docs: "./astryx/docs", + components: "./astryx/components", + agentDocs: { + append: [ + "Import Astryx through @lablup/ui-common, never @astryxdesign/*: same subpaths (@lablup/ui-common/Button, /theme/tokens.stylex, /reset.css, /lab).", + "Use Modal, not Dialog. ui-common hides Astryx Dialog (exports.exclude.json) so every product has one dialog surface.", + "Theme with from @lablup/ui-common/theme/lablup/built (pairs with theme.css). Declare @layer reset, theme, base, astryx-base, astryx-theme, ui-common, components, utilities; first.", + "ui-common strings resolve through Astryx InternationalizationProvider: pass uiCommonMessages from @lablup/ui-common/i18n-catalog in its messages.", + "ComplexSelector, and lab's Drawer and Tour, come from ui-common: fixed copies of Astryx's, same API and import paths. ComplexSelector adds hasClear/onClear.", + ], + }, + issuesUrl: "https://github.com/lablup/ui-common/issues", +}; diff --git a/astryx/components/AlertModal.doc.mjs b/astryx/components/AlertModal.doc.mjs new file mode 100644 index 0000000..179572b --- /dev/null +++ b/astryx/components/AlertModal.doc.mjs @@ -0,0 +1,91 @@ +/** + * `astryx component AlertModal` (and `ui-common component AlertModal`). + * + * @type {import('@astryxdesign/cli/authoring').ComponentDoc} + */ +export default { + type: "component", + name: "AlertModal", + displayName: "AlertModal", + import: "@lablup/ui-common", + category: "Overlay", + keywords: ["alert dialog", "confirm", "alertdialog", "destructive", "are you sure"], + description: + "The WAI-ARIA alert dialog on Modal's portalled surface: role=alertdialog named by its title and described by its description, Cancel focused first, Escape cancels, the backdrop does not. ui-common hides Astryx AlertDialog in its favour: it joins Modal's level stack and leaves the notification layer reachable.", + props: [ + { + name: "isOpen", + type: "boolean", + description: "Whether it is open.", + required: true, + }, + { + name: "onOpenChange", + type: "(isOpen: boolean) => void", + description: "Called with false on Cancel and on Escape.", + required: true, + }, + { name: "title", type: "string", description: "The question.", required: true }, + { + name: "description", + type: "string", + description: "The consequence.", + required: true, + }, + { + name: "actionLabel", + type: "string", + description: "Action button label.", + required: true, + }, + { + name: "onAction", + type: "() => unknown", + description: "Runs on the action button. It does not close the modal.", + required: true, + }, + { + name: "actionVariant", + type: "ButtonVariant", + description: "Action button variant.", + default: '"destructive"', + }, + { + name: "isActionLoading", + type: "boolean", + description: "Shows the action pending.", + }, + { name: "isActionDisabled", type: "boolean", description: "Disables the action." }, + { + name: "cancelLabel", + type: "string", + description: "Cancel label.", + default: 'the catalog\'s uic.common.cancel ("Cancel")', + }, + { + name: "isCancelDisabled", + type: "boolean", + description: "Disables the Cancel button. Escape still cancels.", + }, + ], + usage: { + description: + "A short, blocking question before an action. For an irreversible deletion that needs typed confirmation, use DeleteConfirmModal.", + }, + examples: [ + { + label: "Confirm a termination", + code: ` { + await terminate(); + setIsOpen(false); + }} +/>`, + }, + ], +}; diff --git a/astryx/components/AlertModal.tsx b/astryx/components/AlertModal.tsx new file mode 100644 index 0000000..ccdc7a3 --- /dev/null +++ b/astryx/components/AlertModal.tsx @@ -0,0 +1,3 @@ +// The Astryx CLI pairs each component doc with a same-stem source file. +// ui-common ships no source, so this names the export instead. +export { AlertModal } from "@lablup/ui-common"; diff --git a/astryx/components/BoardItemTitle.doc.mjs b/astryx/components/BoardItemTitle.doc.mjs new file mode 100644 index 0000000..92c16f3 --- /dev/null +++ b/astryx/components/BoardItemTitle.doc.mjs @@ -0,0 +1,51 @@ +/** + * `astryx component BoardItemTitle` (and `ui-common component BoardItemTitle`). + * + * @type {import('@astryxdesign/cli/authoring').ComponentDoc} + */ +export default { + type: "component", + name: "BoardItemTitle", + displayName: "BoardItemTitle", + import: "@lablup/ui-common", + category: "Layout", + keywords: ["title", "panel", "dashboard", "board", "sticky", "header"], + description: + "The title row of a dashboard panel: a heading with an optional help tooltip, and actions at the end. It sticks to the top of the panel's scroll area on the surface colour, and its two groups wrap onto separate lines when the panel is narrow.", + props: [ + { + name: "title", + type: "ReactNode", + description: + "The title. A string renders as a level-5 heading; a node renders as is.", + required: true, + }, + { + name: "tooltip", + type: "ReactNode", + description: + "Help text in a tooltip beside the title. Without it, no help glyph.", + }, + { + name: "tooltipIcon", + type: "ReactNode", + description: "Glyph of the help tooltip.", + default: "the theme's info icon", + }, + { + name: "endContent", + type: "ReactNode", + description: "Actions at the end of the row.", + }, + ], + usage: { + description: + "At the top of a dashboard panel or a board item. Other div attributes (className, style, data-*) reach the row. The --board-item-title-z property sets its z-index (default 50).", + }, + examples: [ + { + label: "Panel title with help and an action", + code: '}\n/>', + }, + ], +}; diff --git a/astryx/components/BoardItemTitle.tsx b/astryx/components/BoardItemTitle.tsx new file mode 100644 index 0000000..4a8dea3 --- /dev/null +++ b/astryx/components/BoardItemTitle.tsx @@ -0,0 +1,3 @@ +// The Astryx CLI pairs each component doc with a same-stem source file. +// ui-common ships no source, so this names the export instead. +export { BoardItemTitle } from "@lablup/ui-common"; diff --git a/astryx/components/BooleanToken.doc.mjs b/astryx/components/BooleanToken.doc.mjs new file mode 100644 index 0000000..a30048e --- /dev/null +++ b/astryx/components/BooleanToken.doc.mjs @@ -0,0 +1,57 @@ +/** + * `astryx component BooleanToken` (and `ui-common component BooleanToken`). + * + * @type {import('@astryxdesign/cli/authoring').ComponentDoc} + */ +export default { + type: "component", + name: "BooleanToken", + displayName: "BooleanToken", + import: "@lablup/ui-common", + category: "Data Display", + keywords: ["boolean", "true false", "on off", "enabled", "token", "yes no"], + description: + "An on/off value as an Astryx Token: green for true, the default outline for false, and a fallback when the value is not a boolean.", + props: [ + { + name: "value", + type: "boolean | null | undefined", + description: "The value.", + required: true, + }, + { + name: "trueLabel", + type: "string", + description: "Label for true.", + default: "the catalog's uic.BooleanToken.true", + }, + { + name: "falseLabel", + type: "string", + description: "Label for false.", + default: "the catalog's uic.BooleanToken.false", + }, + { + name: "fallback", + type: "ReactNode", + description: "Rendered when the value is not a boolean.", + default: '"-"', + }, + ], + usage: { + description: "In table cells and metadata lists that show a setting.", + bestPractices: [ + { + guidance: true, + description: + "Name what true means (Enabled, Public) when the column title does not.", + }, + ], + }, + examples: [ + { + label: "A setting", + code: '', + }, + ], +}; diff --git a/astryx/components/BooleanToken.tsx b/astryx/components/BooleanToken.tsx new file mode 100644 index 0000000..97cc96c --- /dev/null +++ b/astryx/components/BooleanToken.tsx @@ -0,0 +1,3 @@ +// The Astryx CLI pairs each component doc with a same-stem source file. +// ui-common ships no source, so this names the export instead. +export { BooleanToken } from "@lablup/ui-common"; diff --git a/astryx/components/BulkEditFormItem.doc.mjs b/astryx/components/BulkEditFormItem.doc.mjs new file mode 100644 index 0000000..c55ad32 --- /dev/null +++ b/astryx/components/BulkEditFormItem.doc.mjs @@ -0,0 +1,68 @@ +/** + * `astryx component BulkEditFormItem` (and `ui-common component BulkEditFormItem`). + * + * @type {import('@astryxdesign/cli/authoring').ComponentDoc} + */ +export default { + type: "component", + name: "BulkEditFormItem", + displayName: "BulkEditFormItem", + import: "@lablup/ui-common", + category: "Inputs", + keywords: ["bulk edit", "batch edit", "keep as is", "multiple records", "form item"], + description: + 'A Form.Item for editing one field across many records. It starts as a read-only "Keep as is" placeholder (the value stays undefined, so a submit leaves every record alone); clicking or focusing it swaps in the wrapped control; hasClear adds "Clear", which sets the value to null; "Undo changes" returns to keep mode. Takes every Form.Item prop but required, and rules without required.', + props: [ + { + name: "name", + type: "NamePath", + description: "The field the item edits.", + required: true, + }, + { + name: "children", + type: "ReactElement", + description: + "The control. It is cloned with value/onChange, a ref (focused when editing starts) and open/onOpenChange (opened when editing starts).", + }, + { + name: "hasClear", + type: "boolean", + description: "Offers Clear, which sets the value to null.", + }, + { + name: "keepValueLabel", + type: "string", + description: "The placeholder in keep mode.", + default: 'the catalog\'s uic.BulkEditFormItem.keepAsIs ("Keep as is")', + }, + { + name: "clearValueLabel", + type: "string", + description: "The placeholder once cleared.", + default: 'the catalog\'s uic.BulkEditFormItem.clear ("Clear")', + }, + { + name: "clearLabel", + type: "string", + description: "The clear action's label.", + default: 'the catalog\'s uic.BulkEditFormItem.clear ("Clear")', + }, + { + name: "undoLabel", + type: "string", + description: "The label of the action that returns to keep mode.", + default: 'the catalog\'s uic.BulkEditFormItem.undoChanges ("Undo changes")', + }, + ], + usage: { + description: + "A form that edits several selected records at once, where an untouched field must not overwrite their differing values. Render it inside a Form from @lablup/ui-common/Form.", + }, + examples: [ + { + label: "An optional field that can be cleared on every record", + code: '\n \n', + }, + ], +}; diff --git a/astryx/components/BulkEditFormItem.tsx b/astryx/components/BulkEditFormItem.tsx new file mode 100644 index 0000000..86929be --- /dev/null +++ b/astryx/components/BulkEditFormItem.tsx @@ -0,0 +1,3 @@ +// The Astryx CLI pairs each component doc with a same-stem source file. +// ui-common ships no source, so this names the export instead. +export { BulkEditFormItem } from "@lablup/ui-common"; diff --git a/astryx/components/BulkErrorModal.doc.mjs b/astryx/components/BulkErrorModal.doc.mjs new file mode 100644 index 0000000..0ff7b1c --- /dev/null +++ b/astryx/components/BulkErrorModal.doc.mjs @@ -0,0 +1,75 @@ +/** + * `astryx component BulkErrorModal` (and `ui-common component BulkErrorModal`). + * + * @type {import('@astryxdesign/cli/authoring').ComponentDoc} + */ +export default { + type: "component", + name: "BulkErrorModal", + displayName: "BulkErrorModal", + import: "@lablup/ui-common", + category: "Overlay", + keywords: ["bulk", "partial failure", "error report", "failed items", "batch"], + description: + "Reports the failures of a bulk operation: a Modal with a DataGrid of the failed items, in columns the caller describes, under an optional error banner with guidance. It has no footer; the header's close button, the backdrop or Escape close it. The grid is compact with column rules, pages at ten rows and hides its page bar on one page.", + props: [ + { + name: "columns", + type: "ReadonlyArray>", + description: "How one failed item renders.", + required: true, + }, + { + name: "data", + type: "ReadonlyArray", + description: "One item per failure.", + required: true, + }, + { + name: "idKey", + type: "string | ((item: T) => Key)", + description: "Row identity.", + default: "'id', then key", + }, + { + name: "description", + type: "ReactNode", + description: + "Guidance in an error banner above the grid. Without it there is no banner.", + }, + { + name: "descriptionTitle", + type: "ReactNode", + description: "The banner's title.", + default: "the catalog's uic.BulkErrorModal.errorOccurred", + }, + { + name: "title", + type: "ReactNode", + description: "The modal's title.", + default: "an error glyph and the catalog's uic.BulkErrorModal.title", + }, + { + name: "isOpen / onOpenChange", + type: "boolean / (isOpen: boolean) => void", + description: "Visibility, as on Modal.", + required: true, + }, + { + name: "width", + type: "number | string", + description: "Modal width.", + default: "720", + }, + ], + usage: { + description: + "Only for a partial failure; a wholly failed operation is one error notice. Keep the caller's form open behind it, so the user can fix the failed items and retry.", + }, + examples: [ + { + label: "Failed folder deletions", + code: ' 0}\n onOpenChange={(open) => !open && setFailures([])}\n description="Fix the failed folders and try again."\n columns={[\n { key: "name", header: "Folder", renderCell: (f) => f.name },\n { key: "message", header: "Error", renderCell: (f) => f.message },\n ]}\n data={failures}\n/>', + }, + ], +}; diff --git a/astryx/components/BulkErrorModal.tsx b/astryx/components/BulkErrorModal.tsx new file mode 100644 index 0000000..7f1d239 --- /dev/null +++ b/astryx/components/BulkErrorModal.tsx @@ -0,0 +1,3 @@ +// The Astryx CLI pairs each component doc with a same-stem source file. +// ui-common ships no source, so this names the export instead. +export { BulkErrorModal } from "@lablup/ui-common"; diff --git a/astryx/components/ColorPicker.doc.mjs b/astryx/components/ColorPicker.doc.mjs new file mode 100644 index 0000000..6db2299 --- /dev/null +++ b/astryx/components/ColorPicker.doc.mjs @@ -0,0 +1,88 @@ +/** + * `astryx component ColorPicker` (and `ui-common component ColorPicker`). + * + * @type {import('@astryxdesign/cli/authoring').ComponentDoc} + */ +export default { + type: "component", + name: "ColorPicker", + displayName: "ColorPicker", + import: "@lablup/ui-common", + category: "Inputs", + keywords: ["color", "colour", "picker", "hex", "swatch", "theme", "accent"], + description: + "A hex colour field: a swatch trigger with Astryx field chrome opens a Popover holding the platform's and a hex TextInput, and optionally a clear button. The value is #rrggbb on both edges (toHexColor also accepts #rgb, #rrggbbaa, rgb() and rgba()). onChange fires on the settled colour, not while dragging. No alpha, no presets.", + props: [ + { + name: "value", + type: "string | null", + description: "The colour. An unparseable value renders as unset.", + }, + { + name: "onChange", + type: "(hex: string) => void", + description: + "Fires with #rrggbb when the user settles on a colour: the native input's change event, or a complete hex in the field.", + }, + { + name: "hasValueLabel", + type: "boolean", + description: "Shows the hex next to the swatch on the trigger.", + }, + { + name: "hasClear", + type: "boolean", + description: "Offers a clear button in the popover.", + }, + { + name: "onClear", + type: "() => void", + description: "Fires when the clear button is pressed; the popover closes.", + }, + { + name: "isDisabled", + type: "boolean", + description: "Disables the trigger and the popover's fields.", + }, + { + name: "label", + type: "string", + description: "Accessible name of the trigger and the colour area.", + default: 'the catalog\'s uic.ColorPicker.label ("Select color")', + }, + { + name: "hexValueLabel", + type: "string", + description: "Hidden label of the hex field.", + default: 'the catalog\'s uic.ColorPicker.hexValue ("Hex value")', + }, + { + name: "clearLabel", + type: "string", + description: "The clear button's label.", + default: 'the catalog\'s uic.ColorPicker.clear ("Clear")', + }, + { + name: "noColorLabel", + type: "string", + description: "Shown on the trigger with hasValueLabel when there is no colour.", + default: 'the catalog\'s uic.ColorPicker.noColor ("No color")', + }, + { + name: "data-testid", + type: "string", + description: + "Test id of the trigger; the area, hex field, clear button and value label get -area, -hex, -clear and -value.", + }, + ], + usage: { + description: + "A theme or branding setting where the colour is stored as hex and written once per choice. className and style reach the trigger.", + }, + examples: [ + { + label: "An accent colour that can be reset", + code: ' setAccent(null)} />', + }, + ], +}; diff --git a/astryx/components/ColorPicker.tsx b/astryx/components/ColorPicker.tsx new file mode 100644 index 0000000..e9b0d99 --- /dev/null +++ b/astryx/components/ColorPicker.tsx @@ -0,0 +1,3 @@ +// The Astryx CLI pairs each component doc with a same-stem source file. +// ui-common ships no source, so this names the export instead. +export { ColorPicker } from "@lablup/ui-common"; diff --git a/astryx/components/ConfirmPopover.doc.mjs b/astryx/components/ConfirmPopover.doc.mjs new file mode 100644 index 0000000..9c542b6 --- /dev/null +++ b/astryx/components/ConfirmPopover.doc.mjs @@ -0,0 +1,99 @@ +/** + * `astryx component ConfirmPopover` (and `ui-common component ConfirmPopover`). + * + * @type {import('@astryxdesign/cli/authoring').ComponentDoc} + */ +export default { + type: "component", + name: "ConfirmPopover", + displayName: "ConfirmPopover", + import: "@lablup/ui-common", + category: "Overlay", + keywords: ["confirm", "popconfirm", "are you sure", "confirmation", "popover"], + description: + "A one-click confirmation anchored to its trigger: a question, an optional supporting line, Cancel and a confirm action, on Astryx Popover. Cancel takes focus first; the action may be async.", + props: [ + { + name: "title", + type: "ReactNode", + description: "The question. A string renders as the heading line.", + required: true, + }, + { + name: "description", + type: "ReactNode", + description: "Supporting line under the title.", + }, + { name: "icon", type: "ReactNode", description: "Leading glyph beside the title." }, + { + name: "onAction", + type: "(event: MouseEvent) => unknown", + description: + "Runs on confirm. A returned promise keeps the button pending; the popover closes when it resolves.", + }, + { + name: "actionLabel", + type: "string", + description: "Confirm button label.", + default: 'the catalog\'s uic.common.confirm ("Confirm")', + }, + { + name: "actionVariant", + type: "ButtonVariant", + description: "Confirm button variant; destructive for a harmful action.", + default: '"primary"', + }, + { + name: "isActionDisabled", + type: "boolean", + description: "Disable the confirm button.", + default: "false", + }, + { + name: "onCancel", + type: "(event: MouseEvent) => void", + description: "Runs on Cancel; the popover closes either way.", + }, + { + name: "cancelLabel", + type: "string", + description: "Cancel button label.", + default: 'the catalog\'s uic.common.cancel ("Cancel")', + }, + { + name: "label", + type: "string", + description: "Accessible name of the popover.", + default: "title when it is a string, else the action label", + }, + { + name: "children", + type: "ReactNode | (triggerProps) => ReactNode", + description: + "The trigger, as on Popover. Use the render-prop form inside a ButtonGroup so the button stays a direct child.", + }, + { + name: "isOpen / onOpenChange", + type: "boolean / (isOpen: boolean) => void", + description: + "Controlled open state; uncontrolled without isOpen. Other Popover props pass through.", + }, + ], + usage: { + description: + "For reversible actions: deactivate, restore, reset a form, leave a shared folder.", + bestPractices: [ + { + guidance: false, + description: + "Guard an action that cannot be undone with it; use a Modal that asks for the name to be typed.", + }, + ], + }, + examples: [ + { + label: "A reversible, harmful action", + code: ' deactivate(key.id)}\n>\n