Changelog

Changelog

The packages are released independently: @hulianui/ui provides components and @hulianui/tokens provides design-token CSS. Changes follow semantic versioning and are generated from changesets.

Current version

v0.60.0
npmGitHub Releases
  1. v0.60.0

    @hulianui/uiFeaturesFixes

    AuthPanel gains contentAlign?: "start" | "center" (#338).

    The panel used to pin its top block to the top and its bottom block to the bottom. The form on the other half of a split auth page is vertically centered (place-items-center in the docs example), so on a taller viewport the headline sat at y≈128 while the form started around y≈284, with a large gap between them. Consumers could only stretch the internal layout with [&>div:first-child]:flex-1, which guesses at the DOM and is not a contract. With contentAlign="center" the panel becomes a three-row grid 1fr auto 1fr: the brand still sits at the top, highlights / footer still sit at the bottom, and the middle content (title / description / children) is centered relative to the whole panel. The two outer rows share the remaining space equally, so a brand slot and a footer of different heights do not push the middle off center (a flex-1 + justify-center layout would). The default is start, so existing pages render the same DOM as before.

    5873b64

    QuestionCard gains figureAlt?: (index) => string: the alt text for figures split out of the stem now comes from the locale-owning parent (the QuestionEditor preview passes its own figureAlt). Previously it always fell back to the Chinese default alt text regardless of language, which left Chinese text on the English docs examples.

    The Stat header row is now a fixed 32px tall with or without icon (#339).

    Since 0.58.0 the corner icon sits on a size-8 base, so cards with an icon had a 32px header row while cards without one had 20px. In a row of KPI cards where some pass an icon and some do not (typically the ones reserving the top-right corner for an overlaid Sparkline), the value baseline shifted by 12px and card heights diverged. The header row now has min-h-8; the "all cards with icons or none" constraint is enforced by the component instead of being something consumers must remember.

    034331f

    The Upload drop zone now rests on a faint primary-tinted dashed border and background, darkens on hover, and is heaviest while dragging over: three distinct steps.

    Previously the resting state was a border-border dashed line on bg-surface. The drop zone usually sits on a same-colored card, so its only boundary was a 1px gray dashed line at roughly 1.1:1 contrast on white; consumers reported the edge as barely visible, and hover only swapped the background, making it nearly indistinguishable from rest. The resting state is now one step away from the card, hover differs from rest, and the drag-over bg-primary/10 is one step heavier than before.

    4e8dccb

    QuestionAnswer difficulty marker now uses text-muted-foreground. It previously used text-muted, which has been a background token since 0.28.0; Tailwind generated no text-color rule for it, so the marker silently inherited the body color.

  2. v0.59.0

    @hulianui/uiFeaturesFixes

    New standalone subpath @hulianui/ui/math-field: MathField (a MathLive-powered visual formula input whose value is LaTeX without $; it satisfies MathFieldLikeProps, so it plugs straight into the visualEditor of MathTextarea / QuestionEditor and the mathField of QuestionAnswer; the server and the first client frame render a skeleton, mathlive is loaded dynamically on the client, and a missing package shows an install hint instead of throwing; virtualKeyboard auto / manual / off, keyboardLayouts, readOnly, placeholder; MathLive's CSS variables are pinned to the library tokens) and createCasComparator() (tier-3 equivalence grading that returns Promise<(a, b) => boolean> to feed the equivalent option of gradeObjective; any parse failure is false).

    mathlive (>=0.110.0) and @cortex-js/compute-engine (>=0.58.0, the dependency mathlive pins) join as optional peerDependencies: consumers who do not install them are unaffected, the main package and @hulianui/ui/math contain zero MathLive, and the initial chunk of @hulianui/ui/math-field measures 12.9KB (mathlive 221KB / compute-engine 294KB are lazy-loaded through import()). Fonts come from the consumer's import "mathlive/fonts.css".

    The built-in HanLearn demo gains a question bank page (QuestionEditor + MathField) and a practice page (QuestionAnswer with three-tier instant grading) and now mounts the ToastProvider it was missing; docs/consuming-math.md is new.

    50f5f37

    @hulianui/ui/math gains the question domain types and pure functions: Question / QuestionType (a closed enum of single / multiple / judge / blank / short_answer / calculation / essay) / QuestionAnswer, plus validateQuestion, defaultShape, normalizeOptions, blankCount, splitStemFigures, toWireAnswer / fromWire, answerText, and gradeObjective (objective grading whose default tier matches the consumer's server logic word for word; normalisation and numeric tolerance are opt-in). Grading and figure extraction each ship a cross-language contract fixture (grade.contract.json / stem-figures.contract.json) for the Python side to verify against.

    QuestionCard: kind (four values) is deprecated in favour of type (seven values); old values still map for one minor and warn in development. New answer / analysis / showAnswer (off by default) render the key and explanation; options becomes { key, text }, with the legacy { label, text } shape accepted for one more minor. Type tag and answer copy now come from the locale (new question entries).

    9f2ed95

    @hulianui/ui/math gains MathTextarea, a LaTeX input for question authoring. Templates insert at the caret (select x, click Fraction, get \frac{x}{} with the caret in the denominator), one click wraps the selection in $…$ / $$…$$, the pre-submit check reports only unclosed $ and unbalanced {} with line and column, KaTeX parse errors are mapped back to a position in the whole string, and the live preview is the same Formula used for display. visualEditor injects any component satisfying MathFieldLikeProps (MathField in phase 5); the Visual input tab appears only when provided. Copy comes from the locale (new mathTextarea entries, including built-in template names). Companion pure functions applyFormulaTemplate / wrapSelectionInMath / isInsideMath / mathSpans / validateFormulaSyntax / textPosition / katexErrorAt are exported alongside.

    Size: the export * upper bound of @hulianui/ui/math rises from 95.6KB to 154.4KB (Popover, Tabs, and form controls now live behind this entry) and the baseline is raised to 178KB; the package is sideEffects:false, so consumers importing only Formula are unaffected after tree-shaking.

    257102d

    @hulianui/ui/math gains QuestionAnswer, the student-side answer card for one question. It renders the right control per type (single → RadioGroup, multiple → CheckboxGroup, judge → the two built-in True / False options with values "true" | "false", blank → one input per blank with its number; when blankCount is missing it counts ____ in the stem, then falls back to 1); a choice question with missing options says plainly that it cannot be answered yet instead of showing an empty radio group; subjective questions are read-only with a "graded by the teacher" note; an unknown type is treated as subjective with a development warning. canSubmit gates submission until every blank is filled; the submit button appears only when onSubmit is provided and receives the canonical shape (blanks are always an array, flatten a single blank with encodeBlanks). A present result locks the card and shows the verdict, the correct answer rendered by answerText, and the explanation; correctHint / reason / header slots; blankInput="math" with mathField injects a formula keyboard (MathFieldLikeProps gains an optional disabled). The stem shares the newly extracted QuestionStemBlock with QuestionCard (resolveFigure splitting plus Formula). Three silent "the student cannot answer" bugs from the consumer's prototype (empty radio group for true-false / one input for multiple blanks / object-shaped options filtered away) each have a regression test. Copy comes from the locale (new questionAnswer entries). Companion exports: canSubmit / answerKind / resolveBlankCount.

    The answer-shape type QuestionAnswer is renamed QuestionAnswerValue to make room for the component. The type only ever existed on the unreleased master, so this is not a breaking change.

    Size: the export * upper bound of @hulianui/ui/math measures 184.5KB (only Radio / RadioGroup / Input newly enter this entry), still within the 208KB baseline; the package is sideEffects:false, so consumers importing only Formula or QuestionCard are unaffected after tree-shaking.

    8231de3

    @hulianui/ui/math gains QuestionEditor, a structured editor for one math question. Switching type resets options and answer together (with a confirmation when there is content; the score changes only if it still equals the old default); the stem input shows only the body while figures are written back as a ![](key) block at the end of the stem, resolved through resolveFigure, with the Insert image button appearing only when onUploadFigure is provided; after adding, removing, or reordering options the correct answer is remapped to follow the content; the number of blanks follows ____ in the stem with a one-click align and per-blank equivalent forms; calculation and extended-response questions can switch to a rubric with a running total; validateQuestion issues land on Field.error (only edited fields by default, showAllIssues for submit time); a review bar via issues / onResolveIssue; extra for consumer-private fields; the preview on the right is QuestionCard. No submit button. Copy comes from the locale (new questionEditor entries, including a message table for validateQuestion codes). Companion exports: questionFormulaIssues / shapeIsDirty / switchType / optionCaption / stemBody / joinStemFigures.

    QuestionCard gains resolveFigure: ![](key) references in the stem are split out before typesetting and rendered after the text (the editor preview and the question bank share one path).

    Size: the export * upper bound of @hulianui/ui/math rises from 154.4KB to 180.5KB (Field, Segmented, Checkbox, Switch, Rating, NumberField, Alert, AlertDialog, and Image now live behind this entry) and the baseline is raised to 208KB; the package is sideEffects:false, so consumers importing only Formula or QuestionCard are unaffected after tree-shaking.

    519377c

    Brand badges now support animated and video marks. A GIF, APNG, or animated WebP passed as an img plays as-is; wrapping it in <picture> to give users with reduced motion enabled a static fallback, a muted looping <video>, and a self-drawn <canvas> all now fill the badge with object-cover. Previously the size rule only matched a direct-child img, so adding a <picture> fallback dropped the mark out of the rule and clipped it at its natural size. This also fixes an older defect hidden by square assets: the badge is a grid container, and height:100% on a replaced element used as a grid item does not resolve, so non-square images were only scaled by width and never actually cropped square by object-cover. Media children now use absolute inset-0, so assets of any ratio fill the badge.

    The AdminLayout docs now say to place Brand directly in the brand area: sizing and the reduced-motion fallback for animated logos are Brand's contract, and the logo slot only positions it.

    381aeda

    Fix: when a CheckboxGroup / RadioGroup sits inside a Field, every Checkbox / Radio in the group used to take the Field label as its accessible name (screen readers announced N identically named items). Each item is now named by its own label, the Field label names the group, and description / error still reach every item (Base UI Field.Item under the hood, applied only inside a group within a Field). A single Checkbox inside a Field (not in a group) is still named by the Field label, unchanged.

    26132c0
  3. v0.58.0

    @hulianui/uiFeaturesFixes

    Visual upgrade for the admin family: hierarchy moves from lines to elevation, and jittering numbers are fixed. This changes the default look; interfaces will look different the day you upgrade.

    The changes concentrate on three places. After a screen-by-screen audit these were the only genuinely flat spots in the admin family; the rest (Table header, Descriptions key/value rows, NavMenu group titles, Skeleton, Empty) already held up and were deliberately left alone:

    • `Stat`: raised to border-hairline + shadow-sm (the library's established rule: with a shadow, light mode drops the border for a hairline and dark mode keeps the hairline, matching Card's elevated tier); value goes from 24px to 30px with tracking-tight; label gains font-medium; the corner icon gets a neutral bg-muted plinth; value and trend rows use tabular-nums. Deliberately no hover lift: a Stat is not clickable, and a lift would promise a click that does nothing.
    • `SearchForm`: the container moves from a flat 1px border to border-hairline + shadow-sm. Until now the query area on a list page was flat while the ProTable container below it carried a shadow, so the two blocks sat on different planes with a visible seam between them.
    • `AdminLayout`: the top bar changes from border-b border-border to border-b border-hairline + shadow-sm, separating from the content with elevation instead of a hard line. The sidebar keeps its border-r: a vertical shadow would smear grey into the content area, which reads as dirt, not depth.

    Plus two tabular-number fixes (readability defects, not taste): the "N items total" count in ProTable and the page numbers in Pagination. When the digit count changes, proportional figures make the whole row shift sideways, and in Pagination the sliding indicator lands in the wrong place as well.

    Migration: to keep the old flat look, pass className="shadow-none border-border" to Stat / SearchForm. The Stat value is one step larger and still truncate, so long values in narrow cards (< 200px) get cut off sooner; give them room or switch to Statistic.

    9124187

    Fixes MetaBalls ignoring color / cursorBallColor entirely, and the grab cursor flickering while dragging a Sortable item.

    MetaBalls used to parse the probe element's computed color with an /rgba?\(...\)/ regex. This library's oklch() tokens are downleveled by Lightning CSS, so the computed value is lab(...): the regex never matched, and both color props silently fell back to the neutral grey default. On the docs site the "custom colors" example and the default example rendered the same grey-white blobs. Color resolution now goes through an offscreen 1x1 canvas, the same approach as the other 30-odd WebGL components in the library (the browser handles every color-space conversion). Two hardenings ride along: the probe is mounted inside the component's own subtree, so var() reads the nearest theme island instead of :root; and a mistyped literal or an undefined var(--typo) now falls back to the default color instead of silently inheriting the ancestor text color and painting the blobs black. The color lives only in a shader uniform and is invisible to the DOM, so typecheck, guard and unit tests could not see this defect; real-browser tests now cover it.

    Sortable expressed the grab state as active:cursor-grabbing, meaning "the cursor depends on whatever the pointer is over right now". During a drag the element under the pointer changes every frame (the dragged item's transform lags one frame, the gaps between rows belong to the ul, other rows are mid-shift animation, consumer rows contain input / button), so :active toggled on and off and the browser recomputed the cursor on every input event: the grab icon flickered continuously. The drag now uses position-independent constants: the dragged row, every other row, the ul and document.body are all pinned to grabbing for the duration of the drag and restored on end or unmount. Keyboard drags do not touch the body cursor (no mouse button is held).

    The item being dragged in Sortable also gets the primary semantic color (primary outline, tinted primary background, primary handle). Previously it only had a shadow and a neutral ring, which made the lifted row hard to spot in a long list.

    9a2cc41
  4. v0.12.0

    @hulianui/tokensFeatures

    The preset now ships a safelist that covers container padding, closing an integration failure
    that is very hard to attribute (#336).

    The paddings of Card / Dialog / Drawer / DocumentSheet are the only family in the library
    written as arbitrary values (px-[var(--card-body-px,1.25rem)] and friends). Only 4 of the
    400 components do this, because density has to flow from the size variant through CSS
    variables while still being overridable by className="p-0" via tailwind-merge.

    The cost shows up when a consumer forgets @source. The symptom is not "components have
    no styling": ordinary classes such as px-4, gap-2 and rounded-xl appear in consumer code
    too, so Tailwind emits them anyway and the components get them for free. What vanishes is only
    the family of literals unique to Hulian's source. The net effect is "borders, radii and colors
    are all correct, yet every container lost its padding" - which reads like a component bug, so
    the real cause gets bypassed and a className="p-4" patch lands in product code instead.

    preset-core.css now pins those 30 classes with @source inline(), so they are emitted
    whether or not the consumer scans the component source. Measured cost: 386 bytes. This is
    not a replacement for @source: the other 390 components still depend on it, so it must
    still be configured. A new gate, pnpm check:container-padding, keeps the list and the
    component source in sync in both directions (missing entries and stale ones both fail).

    d311ad3
  5. v0.57.1

    @hulianui/uiFixes

    Two pitfalls added to the Card docs (#336): the root carries no padding of its own and
    content must go inside CardBody (which this library names CardBody, not shadcn/ui's
    CardContent); and when padding is gone card-wide, suspect a missing @source in the
    consumer first - the test is grep card-body-px in the built CSS.

    Component markdown ships with the npm package and MCP's get_component_doc reads the copy in
    node_modules directly, so a documentation gap like this is a runtime gap for agent consumers.

    d311ad3

There are 86 earlier releases. Switch to All releases to view them.