fix(tree-select): the popup pinned only its lower width bound (min-w-[var(--anchor-width)]) and had no upper one, so its width was shrink-to-fit - which means the longest label in the entire tree. Three things stack up: truncate on a row does not constrain intrinsic width while its container has no upper bound; collapsed subtrees are still in the DOM (squeezed to zero height, and an overflow: hidden box still derives its max-content size from its contents); and the popup had no max-w. One long node name nobody ever expanded was therefore enough to push the popup wider than the viewport, where the positioner slid it against the left edge, completely detached from its trigger (measured by the consumer: a 203-character node name produced a 1536px popup in a 1280px viewport). The upper bound is now min(32rem, var(--available-width)): pinning it to the viewport alone is not enough, since a 288px trigger with that node name still produced a 1270px popup covering half the page, and 32rem keeps the popup at the scale of its trigger. Wide fields are not squeezed, because min-width always beats max-width in CSS. The same popup recipe lives in Select and Combobox (same missing upper bound, same truncate on items), so both are pinned too. TreeSelect also gains popupClassName (className only ever reached the trigger, leaving no room even for a stopgap) and passes virtual through (the inner Tree has had virtual scrolling all along, with no way in from TreeSelect - and a textbook outline with tens of thousands of nodes is more common inside a collapsed picker than in an always-visible tree) (#359)
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.
- 8b8e941
- acc418a
feat(question-editor): the three fields of the metrics row (difficulty, score, estimated time) were always rendered, with no way out. Yet what a question is worth is usually not a property of the question but of its place on a paper - the same question is 8 points on a midterm and 5 on a unit test, so the score belongs to the paper-question pair. A product modelled that way had to show an entry page with a score input that must not take effect: whatever the teacher types either contradicts the product ("this question is worth 3 points" inside a question bank) or gets dropped silently on submit, and both are worse than not showing it. The same holds for
estimatedMinutes. The editor now takeshiddenFields(an array of"difficulty" | "score" | "estimatedMinutes"): hidden fields are not rendered, the row collapses to the columns it still has (one hidden field leaves two columns; hiding all three drops the row), and the editor writes nothing to them - switching question type no longer recomputesscorefrom the default table, because writing a field the user cannot see is a silent data change. The values themselves still pass in and out throughQuestionuntouched, andvalidateQuestionis unchanged. Only those three can be hidden: stem, options, answer and explanation are what makes a question a question. The one workaround available before was flatteningdefaultScoreByTypeto zeros, which only stops the editor from inventing a score - the input stayed (#358) - 7d92cb4
feat(page-header):
metaalways rendered as the<ul>below the title row, with no say in where it sits. With only two or three short factual values (Shanghai Science and Technology Edition-4 chapters-0 questions-105MB source file) that row is nearly empty and simply makes the header taller, even though those values are already the same size and colour assubTitleand read as one string. The newmetaPlacement("block", the default, or"inline") hangs the list inside the title group and drops itsmt-2, placing it aftersubTitleand beforetagsso status chips never split that run of muted small text. With asubTitlepresent the component adds one more separator between it and the first entry - otherwise the first gap is a space while the rest are dots, which reads as an oversight; without asubTitleit adds none, since the title is another size and weight and a dot right after it reads as if the title were an entry too. Both placements keep exactly the same semantics: stillul/li, still skipping empty entries, separator stillaria-hidden. This continues #240 - that one settled whether a header can carrymeta, this one settles where it goes - so consumers no longer have tojoin(" · ")an edition name and its facts intosubTitleto get them on one line, which throws away the list semantics, the empty-entry skipping and the decorative separator (#357) - 1b7a63c
fix(pro-table): with controlled row selection (
rowSelectionplusonRowSelectionChange),clearSelectiondid nothing at all. It was hard-wired to clear the internal state while controlled mode renders from the prop, so the batch bar stayed, rows stayed selected and the consumer's state never moved - and the button was there, clickable, with a hover state, exactly as quiet a failure as #202. All three exits are the same function and were broken together: the "clear" link built into the batch bar, theclearSelectionhanded tobatchActions, andactionRef.clearSelection(). It now follows the same controlled/uncontrolled split as the rest of the selection state (controlled goes throughonRowSelectionChange, uncontrolled still clears the internal state). TheactionRefhandle also moved onto a ref: it used to be rebuilt only whenmanagedchanged, freezingonReloadandonRowSelectionChangeat their first-render identity, and inline arrow props are the norm - so the imperative path called a stale callback, which matters much more onceclearSelectionreads controlled-ness from props (#356) - 3d0b0b5
feat(question-editor): the figure strip used to be nothing but thumbnails, a delete button and "insert image", so consumers migrating off their own hand-written form lost three things they had. All three are back: every thumbnail carries a "move earlier / move later" pair (a figure's position is the order it is written at the end of the stem, which is also the display order in paper preview, the student view and docx export; reordering used to mean deleting everything and uploading it again in order), clicking a thumbnail opens it full size in
ImageViewer(80px is enough to tell which figure it is, not enough to read the letters labelling a geometry diagram), and a failed upload row keeps the originalFilebehind a "retry" button (it used to offer nothing but dismissing the notice and picking the file again). Reordering comes back throughonChangeas a wholeQuestionrather than a newonSortFigures: the order is already part ofstem, the editor holds both the stem andfigureFilter, and a second channel would only give figure order two sources of truth. Buttons rather than drag: the same interaction this editor already uses for option order, keyboard and screen reader accessible by construction, and no reason to hand dnd-kit to every@hulianui/ui/mathconsumer for one thumbnail strip. Type, size and count limits are still the consumer's tothrowinsideonUploadFigure(#354)3d0b0b5fix(question-editor): the preview under the stem input was always
<Formula>, which does not understand markdown image syntax. Since 0.64.0figureFilter(#353) carves inline formula images out of the figure set and leaves those references in the stem body by design, so the preview printedverbatim as source. The stem preview now renders through the same path as everything else in the library (the blockQuestionCardandQuestionAnswershare): figures are drawn andmacrosis passed along, so what the teacher sees under the input is what the display end will render. WithoutresolveFigureit still falls back to the default preview (that path cannot resolve figures either), and when the body has neither formulas nor figures the preview box is hidden entirely. Two things ride along: alt text for a stem figure now prefers the alt written on the reference itself (the LaTeX inside the brackets ofsays more than "Figure 2"), falling back to numbering throughfigureAltonly when there is none; andMathTextarea'srenderPreviewnow decides for itself - returningnullmeans there is nothing worth previewing, instead of the box appearing only when the value contains$(#355)
There are 102 earlier releases. Switch to All releases to view them.