@feugene/granularity-dashboard
Записи релизов — авторский текст библиотеки, и он английский. Портал его не переводит: журнал изменений — история чужого репозитория, а переписанная история перестаёт быть историей.
v0.6.02026-08-27
Changed
-
Peer floors on
@feugene/*raised to the current minor. Every peer this package declares on the ecosystem now starts at the version the monorepo actually ships:@feugene/fint-i18n→>=0.7.0 <1.0.0@feugene/granularity→>=0.36.0 <1.0.0@feugene/unocss-preset-granular→>=0.13.0 <1.0.0@feugene/unplugin-granularity→>=0.7.0 <1.0.0
The floors had drifted far behind — some still admitted releases from a year of development ago — and a range that claims support it was never tested against is worse than a narrow one: the install succeeds and the breakage surfaces later, in the consumer’s app.
This is breaking for anyone below a floor. Installing against an older
@feugene/granularitynow produces a peer conflict instead of silence. The fix is to move the core up; nothing in this package’s own API changed.
v0.5.12026-08-22
Fixed
- The grabbed drag handle used a tone as a foreground colour.
--gr-primarygives 3.70 against the dark theme’s surfaces — below AA — so the handle dimmed exactly while being dragged. It uses--gr-primary-textnow.
v0.5.02026-08-20
Added
-
A widget can be dragged from one dashboard to another. Two boards on a page — working and archive, mine and the team’s — had no way to exchange widgets except a “delete here, add there” button the application wrote itself.
The gesture starts as an ordinary in-grid drag and escalates once the pointer leaves its own grid and lands over another one. Merely leaving the edge does not count: on a long page the pointer exits the grid constantly, and a widget that detached on every scroll-past would be unusable.
While the widget is being carried it is out of the source layout — neighbours compact, and the preview honestly shows what will remain. The markup is still rendered by the application, so the element hides itself rather than losing its grid placement.
-
itemTransferOut— the source grid removes the widget from its own layout and says so. This departs from the package’s usual “the grid says where, the application places it” rule, and deliberately: removal is unambiguous — unlike insertion it needs no markup — and leaving it to the application would mean the same threeremoveItemlines in every consumer, with a widget on two dashboards at once for anyone who forgot.Only after a successful landing. Releasing between grids, pressing
Esc, or coming back into the source grid carry nothing away — and the return does not interrupt the gesture: the in-grid drag continues from where it was. -
transferableon the grid — giving away and receiving are separate permissions. An archive board accepts widgets but hands none back.staticwidgets do not travel; they have no handle to begin with. -
GrDashboardTransfergainedsource: 'dashboard'andfrom— the id of the grid a widget came from, so a receiver can tell a catalogue drop from a neighbour’s widget.
Changed
useDashboardTransfercan now run a session it does not own:adopt,moveTo,releaseandhasTargetAtdrive the same machinery asstart(target resolution, per-frame flush,Esc, cleanup) while the points come from outside. A cross-grid transfer grows out of a drag the source grid already owns, and starting a second gesture on top of the first would split one stream ofpointermovebetween two state machines.
v0.4.02026-08-20
Added
-
A widget can size itself to its content.
auto-heightonGrDashboardItemmeasures what the widget actually renders and asks the grid for as many rows as it needs. Until now the height was a number the application had to guess on the user’s behalf, and it guessed wrong every time the data changed: a twenty-row table got clipped byoverflow: auto, a single big number left dead space underneath.What is measured is a wrapper around the content, not the widget body. The body’s height is dictated by the grid cell, and its
scrollHeightismax(content, container)— a widget could grow to fit its content but, once taller than it, would report its own height and never shrink back. The wrapper is only rendered when the prop is on, so widgets without it keep the DOM they had.Height is rounded up to a whole row: the grid is integral (
docs/model.md, invariant 1), and a few pixels of slack at the bottom beat a clipped last row. This is the one place that rounds up — dragging the corner still snaps to the nearest cell, because there the user is aiming.minHandmaxHstill apply,staticstill refuses, and the measured height goes into the layout through the sameresizeItemas every other size change — collisions and compaction included. The resize corner on such a widget changes width only: a height set by hand would be overwritten by the next measurement. -
itemAutoResize— emitted when content, rather than a person, changed a widget’s height. The change still travels throughupdate:layout(the layout stays the single source of truth), but an application that treats an edit as “unsaved changes” would otherwise prompt after a plain data load.itemResizeremains user-only.Measurements arriving in one observer callback are applied as a single batch: ten widgets settling at once produce one
update:layout, not ten.
Documentation
- Exporting and importing a layout as a file — a recipe in
docs/model.md. The package side was already complete (serializeLayout/parseLayoutcarry a version); what was missing was the thirty lines every application writes again:Blobdownload,<input type="file">upload, and what to do with a file that does not parse —nullis a working answer, not a failure.
v0.3.02026-08-19
Added
-
Horizontal compaction.
compactnow takes'horizontal'and'both'alongside'vertical'and'none'.'horizontal'slides widgets to the left edge and leaves their rows alone;'both'runs the two passes until neither moves anything. The iteration is not ceremony: a widget that slides left frees the cell above its neighbour, so a single up-then-left pass is not idempotent, andcompact(compact(l)) === compact(l)is an invariant of this package. Overlapping input is still separated downwards in every mode — rows are unbounded, whilex + w <= colsmeans a rightward push is not always resolvable. The mode travels throughmoveItem,resizeItem,addItem,removeItem,deriveLayoutandlayoutForexactly as before: no signature changed. -
GrDashboardItemSettings— a dialog for one widget’s settings, built on the coreGrDialog. Its own content is the widget size in cells; everything the application owns arrives through the default slot. The size fields are bounded by what the grid will actually accept —min(maxW, cols - x), notcols, because a widget grows rightwards and stops at the edge — and the change is committed through the grid, so compaction,preventCollisionandstaticall still apply. A refusal keeps the dialog open with a message instead of closing over a change that never happened. -
GrDashboardItemgainedshowSettingsand asettingsemit;GrDashboardre-emits it asitemSettings(id). The gear button lands where#editActionslands — the header when there is one, the overlay panel otherwise — and, unlike#actions, does not bring a header into existence. It exists only inmode="edit". -
Widgets can be dragged from the catalog into the grid.
GrDashboardPalettegaineddraggable(on by default), a#ghostslot and slot props on#item;GrDashboardgaineddroppableand anitemDropevent. The button stays exactly where it was and remains the keyboard path — dragging is an addition on top of a working route, not a replacement for it, because a catalog you can only drag from is a catalog you cannot use from a keyboard. The drop reports where it landed and the application places the widget, same as with the button: a grid that wrote the layout itself would produce a widget the application never rendered markup for.breakpointand the grid’s ownoptionsride along in the event so thataddItemreproduces exactly the cell the placeholder showed. Dragging by touch is deliberately not supported: it would needtouch-action: noneon a tile inside a list that has to scroll on a phone, and the button already does the job there. -
useDashboardTransfer— the model behind it, on a new subpath. The catalog sits outside<GrDashboard>and cannot sit inside (the grid root is a CSS Grid; any direct child becomes a cell), so the channel is module-level state next to the component tree rather than provide/inject. Nothing is ever written to it on the server: targets register fromonMounted, a session starts frompointerdown. -
cellFromPointin the./layoutsubpath: which cell a point falls into, treating the point as the centre of the incoming widget — the ghost hangs under the cursor, and the place has to land under it. -
GrDashboardContextgainedrequestSettings,canResizeandresizeItemTo. The last is the only programmatic resize path and returns whether the layout actually changed:resizeItemrefuses silently by handing back the original layout, and a caller that showed a form would otherwise read the refusal as success.
Changed
- In
'horizontal'and'both'a widget cannot be parked in empty space to the right — it slides back to the left edge. This mirrors the existing “you cannot drop a widget into empty space below”;compact: 'none'is still the free grid. docs/model.mdno longer states that horizontal holes are deliberate. That paragraph described the absence of this feature, not a decision about it.
Fixed
- Moving and resizing from the keyboard did not emit
itemMove/itemResize. Both paths committed the layout and announced the result but stayed silent on the dedicated events, so an application listening to them saw pointer gestures only. Cancelling a keyboard move now emits the return trip too: every arrow press before it already reported a move, and without this the event trail ended somewhere the widget no longer is.
v0.2.22026-08-19
Changed
- Control-scale font sizes now ship a paired line height. Every place that sets a control
font size now sets the matching
leading-*next to it, from the core’s new--gr-control-leading-*steps. Before this the line height was inherited from the host application’sbody, and inherited as an absolute value — so how airy a caption looked was decided by someone else’s CSS reset. Requires core>=0.27.0.
v0.2.12026-08-18
Changed
- Release-only bump: the workspace playground apps still pinned the core at
^0.20.0, so yarn resolved a published copy for them instead of linking the workspace, and their uno config scanned that copy’sdist. The pins are updated to the current range; nothing in this package’s runtime changed.
v0.2.02026-08-18
Fixed
compact: 'none'was ignored on every breakpoint the app had not laid out by hand. A layout is derived from a neighbouring breakpoint whenever one is missing, and that derivation hardcoded vertical compaction — so an app that had deliberately switched compaction off got it back the moment the viewport reached a breakpoint without its own layout, silently and only there. The mode now travels with the derivation (deriveLayouttakes it as an optional fourth argument,layoutForthroughoptions.compact), defaulting to'vertical'so existing callers are unaffected.
Added
- Per-component documentation under
docs/components/— one page per component, plus adocs/components.mdindex. Until now the only per-component description lived in the showcase app (companionPackages.ts), so it never reached the published tarball. defineComponentDocsGatefrom@feugene/granularity-test-kitwired in: a component without a page can no longer ship.
0.1.0 - 2026-08-14
Added
- First release. A widget grid for the
@feugene/granularitydesign system: users arrange widgets themselves, and the arrangement survives a reload. No runtime dependencies — collisions, compaction and grid geometry are integer arithmetic. GrDashboard— the grid. Holds a layout per breakpoint (v-model:layout), separates the view mode from the edit mode, and moves and resizes widgets by pointer and from the keyboard. Props:cols,breakpoints,initialBreakpoint,rowHeight,gap,mode,draggable,resizable,compact,preventCollision,lazy,ariaLabel. Emitsupdate:layout,layoutChange,itemMove,itemResize,breakpointChange.GrDashboardItem— a widget on the grid, built on the coreGrCard. Declares its own size bounds (minW,minH,maxW,maxH,static), takes slotsdefault,header,actions,editActions,footerandskeleton.- A widget may have no header at all. The header appears only when there is something to put
in it — a
title, aheaderslot oractions. A widget holding a map or a single big number needs none, and entering the edit mode no longer grows one just to host the drag handle: the handle lives in a panel laid over the top of the content, so switching modes shifts nothing. The panel follows hover and focus-within, and stays visible where hovering does not exist (hover: none); hidden, it remains in the DOM and in the tab order. padding(none | xs | sm | md | lg) — content insets; defaults to thesizestep.nonegives the widget over to its content edge to edge, which is what a table wants. The footer keeps its own insets: it is a utility strip, not content.draggableandresizable— per-widget overrides of the grid-wide rules.:resizable="false"drops the resize corner while the widget can still be moved,:draggable="false"does the opposite;staticstays stronger than both because it is about the layout, not the interface. The grid enforces them itself rather than merely hiding a handle: the gesture and the keyboard both go through its context.overflow(auto | hidden) —hiddendrops both the scrollbar and the body’sTabstop (the body joins the tab order only when it actually overflows).editActions— actions that belong to the edit mode (remove the widget, open its settings). They show up only inmode="edit", in the header when there is one and in the overlay panel when there is not, whileactionsstays for product buttons that are visible at all times.- A widget with neither
titlenorariaLabelno longer claimsrole="group": a group without a name is announced as plain “group” and tells a screen reader nothing.
- A widget may have no header at all. The header appears only when there is something to put
in it — a
GrDashboardToolbar— mode switch and layout reset. Works both inside the grid and outside it, throughv-model:mode.GrDashboardPalette— a catalogue of widgets that can be added. Adding is a plain button, so the keyboard path exists by construction rather than as an afterthought.useDashboardLayout— layout persistence behind a storage interface, with a schema version, a migration hook and debounced writes; a ready-madelocalStorageadapter ships with it. The storage is never read duringsetup, so hydration cannot diverge../layoutsubpath — the pure arithmetic of the layout with no Vue and no DOM: collisions, vertical compaction, moving and resizing, grid geometry, breakpoint derivation and tolerant (de)serialisation. Usable on its own, including on the server.- Accessibility. WAI-ARIA has no pattern for a widget grid, so the contract is invented and
written down in
docs/a11y.md. The grid is a singleTabstop with a roving tabindex across the drag handles;Spacepicks a widget up, arrows move it a cell at a time,Esccancels,Spacedrops it; the resize handle is its own stop with arrows,Shiftfor a large step andHome/Endfor the bounds. Only keyboard moves are announced — a stream of announcements per pointer move makes a screen reader useless. - i18n — the
grDashboardblock withen,ruandesdictionaries: handle names, toolbar labels, catalogue strings and announcement templates. - Theme tokens —
--gr-dashboard-*and--gr-dashboard-frame-*, all resolving to core roles rather than to values.