Feat/list editors#214
Open
volarname wants to merge 70 commits into
Open
Conversation
- Labs components: AListEditor (flat), ASortableListEditor (flat + reorder), ANestedSortableListEditor (multi-level tree + reorder + drag-and-drop) - Composables: useListEditor, useNestedListEditor - Playground views for all three variants - Unit tests + visual-regression screenshots - i18n keys for sortable (en/cs/sk): reorder, expandAll, collapseAll, unsaved, pendingChanges, and helpers Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Sync AListEditor + ASortableListEditor styling with nested variant: compact tokens (48 px row, 6 px pad-y, 13 px font), 8 px radius, no shadow, form card wrapper, container-query driven desktop layout. - Editing row: blue header only (not whole row), primary rail + gradient extend through the form body and footer, edit button toggles close. - Dirty (--dirty / --unsaved) now wins over editing: header bg, title colour, rail and gradient switch from primary to warning. - Flat variants: form body aligns with the row title (no 42 px nest indent); action buttons density=comfortable + mx-1 class. - Sortable drag preview uses forceFallback; ghost = slim row clone without actions/status, faint primary placeholder at drop target. - Reorder mode arrows follow the same hover-reveal / touch-always behaviour as the default row actions; disabled arrows keep opacity 0.3. - Nested: Reorder button v-if-removed entirely when readonly (was only disabled); entering reorder mode auto-expands all branches. - Playground: first AListEditor/ASortableListEditor demo drops on-item-save so the Cancel/Save toolbar can be inspected both with and without a callback. - Tests updated to reflect the editing row's unified action set. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Drop the custom absolutely-positioned drop-line overlay (lineTop/Left/Right/ hookOriginX) and lean on SortableJS's built-in ghost placeholder, restyled as a primary-tinted strip with a dashed outline at the landing position. Add a "+N children" chip inside the drag clone so branch moves read as a whole subtree rather than a single row. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replace SortableJS's DOM-driven drop mechanics with Atlassian Pragmatic DnD's "instruction" pattern. A pointermove handler during drag hit-tests the hovered row, splits it into Y bands (top 25% sibling-above / mid 50% make-child / bottom 25% sibling-below), and in the bottom band of a last-in-group row reads pointer X to emit a `reparent` that jumps out of ancestor chains in a single continuous drag. maxDepth and cycle violations wrap the desired instruction in `blocked` so the intended target stays visible in a warning colour instead of silently refusing. SortableJS is now only the drag lifecycle + floating clone provider — onMove always returns false, onEnd applies our instruction via `editor.moveTo`. The old per-group DOM reorder is gone along with its `data-parent-id` reconciliation. The drop indicator overlay renders absolute-positioned over `.rows`: - sibling-* / reparent → 2px horizontal line with an 8px terminal dot at the left that bleeds 4px outside the anchor column; `left` offset encodes the target depth - make-child → 2px primary border around the full target row Warning-tinted variants signal blocked intent. Pure instruction logic lives in a new `useDragInstruction` composable so the mapping is unit-testable in isolation. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Moved detection used flat index comparison, which shifted for every row whose position in the linearised tree changed — so an unmoved row got flagged orange whenever a row above it moved. Replace with a parent + sibling-index snapshot: a row is "moved" only when its own parent or its own position within that parent actually changed. Source row vanished during drag because SortableJS with `forceFallback: true` sets inline `display: none` on the dragged element to avoid a duplicate of the floating clone. Override with `display: flex !important` on `.row--chosen` / `.row--drop-source` so the origin slot stays visible (dimmed, opacity 0.4). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Drop indicator left offset was 12 + depth*24, so the dot sat on the row padding edge rather than on the drag handle's visual centre, making the depth of the target drop slightly ambiguous. Re-anchor to the drag handle centre (22 + depth*24, with the dot bleeding 4px left so its visual centre lands exactly on the handle column). While dragging, the source node's whole subtree (source row + every descendant rendered underneath) is now dimmed to 0.4 opacity and made non-hittable via pointer-events: none. Relying on CSS cascade from the source wrapper, descendants pick this up automatically. The overlay no longer flashes a blocked indicator when the pointer strays into your own children — there's no valid drop there, so the indicator stays silent rather than shouting about it. `blocked` is now reserved for maxDepth. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Dropping onto a leaf turned it into a parent but left it collapsed, so the just-moved row vanished into a hidden subtree and the user had to hunt for it. After a make-child drop, add the target's key to the expanded set so the new child is immediately visible. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replace Pragmatic DnD's Y-band gesture set (top 25% above / mid 50% make-child / bottom 25% below, with X active only in the last-in-group reparent edge case) with dnd-kit SortableTree's pure outliner model: pointer Y picks the gap (upper 50% of the hovered row = above it, lower 50% = below), pointer X picks the depth inside that gap. Depth is clamped to [0, anchor.depth + 1] where anchor is the visible row immediately above the gap — so you can never skip more than one level deeper than what's already there. Drag right makes the dragged row a new child (one indent step deeper than the anchor); drag left walks out of ancestors (reparent). `makeChild` now surfaces as a field on the instruction instead of a distinct type, driving the same auto-expand behaviour after the drop. The box overlay for `make-child` is gone — a single line indicator whose horizontal start encodes the target depth is the entire visual vocabulary. It was the only way to keep X meaningful across the whole drag surface (previously X only moved the indicator inside a narrow reparent zone, so "drag right to nest" wasn't discoverable). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Drop indicator refinements: - Depth clamp tightened from [0, prev.depth+1] to [next.depth, prev.depth+1] so the line only offers depths that actually land at the pointer's Y position. Before, dragging far left at depth 5 showed a depth-0 line but the item would insert many rows below visually; now the X axis only moves between depths that correspond to the current gap. - Blocked drops (maxDepth overflow) render nothing — the silent empty space IS the "not here" signal. No warning-tinted ghost of the intent. - Floating clone at the cursor hidden via `display: none !important` on the wrapper-level selector (beats the source's drop-disabled rule that the SortableJS clone inherits). The line and connector carry all the where-it-lands information. - Added a thin vertical connector rail from the drop line up to the row whose level the insert matches (previous sibling for same-depth, or the ancestor being joined as a peer on reparent). Positioned in JS from getBoundingClientRect because CSS Anchor Positioning's reachability rules excluded deeply nested anchors in Chrome; the positions are stable for a drag's lifetime so the computed approach is fine. Moved/dirty detection reworked: - Replaced snapshot-vs-current-index diffing with an explicit `movedKeys` Set. Only rows the user actively moved (drag-drop / arrow buttons / indent/outdent) get flagged unsaved; neighbours whose flat index or sibling order shifted as a side-effect stay clean. - In the nested editor, moving a parent marks the whole subtree as moved — the children travel with the parent visually, so they should read as unsaved too. - `dirty` comparison now strips `position` (both editors) and `parent` (nested editor) before JSON-stringifying. Those fields are rewritten on every side-effect sibling-index shift; including them in the dirty baseline painted ghost unsaved markers on rows the user never touched. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Reorder-mode toolbar: - Pending-changes count, Cancel and Apply move into the card header where the "Reorder" button lives in view mode; the sticky bottom toolbar is gone. `toolbarMode` and `toolbarBottomOffset` props are dropped — full customisation still available via the `reorder-toolbar` slot, now rendered in the header. - Apply button disabled at 0 pending changes so accidental clicks can't fire an empty save. - Apply no longer clears `movedKeys`: the caller still has to persist the new order via their API, and exposed `resetDirtyBaseline` now also resets the moved set, so "mark as saved" is one call after the API returns. - Status label "N pending changes" is `text-body-small`; "No pending changes" adds `text-medium-emphasis` to fade it into the background. - "Unsaved" row pill keeps its dot + warning colour but loses the pill-shaped border — quieter inline after the row title. Nested row actions: - View-mode kebab gets better labels + icons: "Add after this item" with `mdi-playlist-plus`, "Add inside" with `mdi-subdirectory-arrow-right`. Localised into sk/en/cs. - "Add inside" now appends to the end of the parent's children (matches the root-level Add button's append semantic). Drag-and-drop make-child still lands at index 0 because the drop line visually sits in that gap. - Reorder-mode kebab gains a Delete item (error-coloured) so trashing a row no longer requires leaving reorder mode. - Reorder-mode action buttons pick up `mx-1` spacing to match view-mode. - First playground demo is wired to actually insert rows on the `@add` emit via an exposed ref; the new row gets auto-opened inline editor and scrolls into view (double nextTick + `block: 'center'`) so long lists don't drop the append below the viewport. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds two new unit-test files for previously-uncovered composables and extends the three component test suites with coverage for the recently reworked behaviours. New tests: - `useDragInstruction.test.ts` (16 `it`): pure-logic coverage of the drop instruction function — null guards on source/descendant hover, all instruction types (sibling, make-child, reparent, root-top), clamp bounds (`[next.depth, prev.depth + 1]`), subtree exclusion from clamp, refEdge, pointer-X rounding, blocked/maxDepth wrapping. - `useNestedListEditor.test.ts` (57 `it`): comprehensive tree-editor coverage — addItem/deleteItem/updateItem, moveUp/Down/Top/Bottom, indent/outdent with depth + sibling constraints, moveTo cycle + maxDepth guards, viewItems DFS preorder + visibility flags, calculateSubtreeDepth, recalculatePositions (clone + no-mutate). Extended component tests (ASortableListEditor + ANestedSortableListEditor): - reorder-toolbar rendering in the card header (no bottom `.__toolbar`) - Apply button enablement bound to `hasPendingChanges` - `movedKeys` lifecycle: cleared on enter, kept after apply, cleared via `resetDirtyBaseline` - Only actively moved rows flagged (no side-effect index-shift noise) - Dirty comparison ignores position/parent fields - Nested: moving a parent marks its whole subtree as moved - Nested: view-mode kebab order + icons, reorder-mode kebab Delete item - Nested: "Add inside" appends to end of children Fix: `headerVisible` in both sortable components now includes `reorderMode.value` so the relocated toolbar stays visible when the component has no title — previously the moved Cancel/Apply buttons disappeared on reorder entry when title was absent. Totals: 5 suites, 214 tests, all green; TS clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The three list-editor variants (AListEditor, ASortableListEditor,
ANestedSortableListEditor) grew with a lot of copy-pasted logic,
templates, and CSS. Refactor in four stages — each verified against
the 214-test suite (now 244 with the earlier composable coverage) and
TypeScript + Stylelint.
### Stage 1: dirty-baseline + delete-dialog composables
- `composables/useDirtyBaseline.ts` — content-hash map keyed by row id,
with a `stringifyContent` that strips configurable fields
(positionField / parentField) before JSON-stringifying. Each caller
passes its own traversal via `getEntries`.
- `composables/useDeleteDialog.ts` — owns the delete dialog's state
(`deleteDialog`, `deleteTarget`, `deleteInFlight`, `deleteError`) plus
the open/confirm/cancel handlers and `performDelete`. Generic over
the caller's view-item shape; actual mutation happens in caller's
`onDeleted` hook so each editor can also prune per-variant state.
### Stage 2: inline-editing + reorder-mode composables
- `composables/useInlineEditing.ts` — `editingKeys`, `editingSnapshots`,
the pending-auto-open watcher (with double-`nextTick` +
`scrollIntoView({ block: 'center' })` landing for newly-added rows),
and the edit / cancel / commit / close handlers. Accepts a
`rowSelector` because each variant's row DOM anchor differs
(`__row` for flat, `__row-wrapper` for nested).
- `composables/useReorderMode.ts` — mode ref wrapper (accepts a
`defineModel('mode')` from the component so `v-model:mode` stays
intact), `snapshot`, `movedKeys` management, and
`enterReorderMode` / `cancelReorderMode` / `applyReorder` including
the internal `watch(mode, ...)` that handles external mode flips.
Only used by the two sortable variants.
### Stage 3: shared Vue components (under internal/)
- `internal/ALeDeleteDialog.vue` — the VDialog + VCard confirmation
block; accepts a `class` pass-through so each caller supplies its
legacy BEM class and the existing test selectors keep matching.
- `internal/ALeEmptyState.vue` — empty-state block with `blockClass`
prop to namespace the `__empty-title` / `__empty-text` descendants.
- `internal/ALeStatus.vue` — reorder-toolbar status pill (error /
pending / no-changes variants with `text-body-small` +
`text-medium-emphasis` on the idle state).
- `internal/ALeUnsavedLabel.vue` — warning-dot + "Unsaved" translated
text span.
- `internal/ALeDragHandle.vue` — mdi-drag VIcon wrapper; legacy
`__drag-handle` class flows through so SortableJS's handle selector
keeps matching.
### Stage 4: CSS tokens + shared SCSS partials
- `styles/_tokens.scss` — `le-tokens` mixin declaring the 24 `--le-*`
custom properties once, plus `le-shell-container` for the shared
`container-name: le-shell` setup. Unifies the three previous
prefixes (`--ale-`, `--asle-`, `--ansle-`) into a single namespace.
- `styles/_shared.scss` — 14 mixins parameterised by BEM block (via
`#{$block}` interpolation). Covers row primitives, row body, form,
state / empty blocks, header + header-actions, toolbar status,
unsaved-label, status-badge, drag-handle, row-add, action visibility,
container-query desktop rules, mobile breakpoints, and chips layout.
Each editor now calls `@include shared.le-row-primitives('.a-...')`
etc. and keeps only its truly-variant-specific styles inlined.
### Results
- AListEditor.vue: 1532 → 539 lines (-993)
- ASortableListEditor.vue: 2221 → 1009 lines (-1212)
- ANestedSortableListEditor: 2300 → 1444 lines (-856)
- Net editor reduction: ≈-3061 lines
- New shared code (composables + components + SCSS): ≈+1974 lines
- 244/244 tests pass, vue-tsc clean, stylelint clean
Behaviour-preserving: the public API (props, emits, `defineExpose`, slot
contracts) is unchanged everywhere. DOM class names are unchanged so
existing tests and consumer CSS both still match.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Mixin-based sharing worked at the source level but still emitted each rule three times in the compiled CSS (once per variant BEM prefix). Replace with a single `.a-le-*` class namespace applied directly in the templates; each shared style is declared once and matches all three variants via the same class. ### Style layer - `_tokens.scss` — custom properties declared on the three editor roots with a plain multi-selector; no mixin wrapper. - `_shared.scss` — flat nested SCSS targeting `.a-le-*` classes. Nesting used only for pseudo-states (`&:hover`), modifiers (`&--editing`), media queries, and container queries. Zero mixins, zero interpolation. - Each editor's `<style>` block drops `@include`s; variant-specific rules (validation rails, chips padding deltas, tree-toggle, drop overlay, nested-only tokens) stay scoped under the editor's root class (`.a-list-editor`, `.a-sortable-list-editor`, `.a-nested-list-editor`). ### Template layer - Every inner BEM class (`.a-list-editor__row`, `.a-sortable-list-editor__row-main`, `.a-nested-list-editor__toolbar-status`, …) renamed to its `.a-le-*` equivalent in all 4 editor files (incl. ANestedRow) + the 5 `internal/` leaf components. - Internal components emit `.a-le-*` classes directly; the `blockClass` prop on `ALeEmptyState` and the class-pass-through on `ALeDragHandle` / `ALeUnsavedLabel` / `ALeStatus` are gone — one style, one source. - Root variant classes (`.a-list-editor` etc.) are kept to scope variant-specific rules via ancestor selectors. - `--dirty` modifier on AListEditor folded into the shared `--unsaved` name; no tests or consumer CSS depended on the old name. ### Test layer - All three component test files updated: `.a-*-list-editor__*` selectors swapped to `.a-le-*` via `replace_all`. Variant-specific selectors (`.a-nested-list-editor__tree-toggle`, `__toolbar` absence checks) left alone. - Doc comment in `useInlineEditing.ts` updated to match new selectors. ### Results Compiled `common-admin.css` (built with Vite): | metric | before | after | |--------------------|--------:|--------:| | CSS bytes | 83,671 | 55,385 | -34% | Rules | 595 | 475 | -120 | Declarations | 1,286 | 917 | -369 244/244 tests pass, `vue-tsc --noEmit` clean, `yarn lint:stylelint` clean. Behaviour preserved — no DOM structure changes beyond class renames, no new selectors consumers could depend on that didn't exist before. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`A` is the Anzu convention for publicly exported components; the leaf components used only inside the list-editor package don't need it. Keep the prefix on the public trio (AListEditor, ASortableListEditor, ANestedSortableListEditor) and rename the internals for clarity: - ANestedRow.vue → internal/LeNestedRow.vue (also moves into internal/ since it's never imported from outside) - ALeDeleteDialog → LeDeleteDialog - ALeDragHandle → LeDragHandle - ALeEmptyState → LeEmptyState - ALeStatus → LeStatus - ALeUnsavedLabel → LeUnsavedLabel Import paths and template refs updated across the three public editors. 244/244 tests still pass, vue-tsc clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- F1 keyboard navigation: roving tabindex, Esc state machine (release-grab → cancel-reorder), nested ←/→ collapse + indent. New useKeyboardNav composable, focus-visible + grabbed CSS. - F2 move-to-position dialog wired into Sortable + Nested kebabs. - F3 change-parent dialog (Nested only): tree picker with cycle prevention, max-depth check, conditional first/last placement. - F4 unsaved-changes guard: v-model:unsavedKeys on all editors, hasUnsavedChanges/unsavedCount/clearUnsavedState exposed. New useUnsavedChangesGuard composable + AUnsavedConfirmDialog under src/labs/unsavedGuard/. Apply does not auto-clear; parent Save clears via empty-set assignment or clearUnsavedState(). - F6 mobile/touch consistency: useContainerWidth replaces 5 viewport-based reads with container-width detection. Pinned --up/--down/--add-child in narrow blocks. Dropped sortable --menu hover exception and @media (hover: none) block in favor of the --touch class as single source of truth. - Vitest browser mode runs headless by default (set VITEST_HEADED=1 to see the browser). - 62 new tests; 339/339 passing, vue-tsc clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Per-item validation surfaces a red 4 px left rail on rows whose validationState is 'invalid'. Three input paths, resolved in order: (1) provide/inject registry via useListEditorItemValidation, (2) editor prop getValidationState, (3) raw.validationState fallback. - New composable src/labs/listEditor/composables/useListEditorItemValidation.ts lets descendants of the editor (e.g. a per-item form/sentinel) register their validity ref without prop-drilling. - Each editor (flat / sortable / nested) provides ListEditorValidationKey and exposes a new optional getValidationState prop for centralized validation shapes ($each). - Validation rail CSS consolidated in styles/_shared.scss so all three editors share it; nested editor previously had no rail. Red wins over orange unsaved when both apply (z-index 3 over 2 + dedicated --unsaved.--validation-invalid background tint). - 6 new tests covering raw, prop, and registry paths plus priority. - Recipe: todo/sortable/validation-recipe.md (FAQ-shape sentinel pattern, centralized $each pattern, plain raw.validationState shortcut). - 345/345 tests passing, vue-tsc clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- F1 closure polish:
- Focus management on row delete: when the focused row is removed,
focus falls to the next sibling (or new last row), instead of
vanishing to <body>. Snapshots the previous orderedKeys so the old
index can be looked up after the row is gone.
- Keyboard grab visualization: composable now exposes grabbedIndex
and totalCount; each editor template renders a visually-hidden
aria-live region announcing "Grabbed, position N of M" while a
grab is active. CSS strengthens the --grabbed state (3 px primary
outline + tinted background + soft shadow + z-index 4 above the
unsaved rail).
- Releases grab if the grabbed row is removed externally.
- New canonical slot mutation pattern via actions.update:
- All three editors expose actions.update(data) on the #item slot's
actions bundle. Internally calls editor.updateItem(key, data) — the
same canonical mutation entry point used for inline-edit save and
drag/drop commit. Decouples consumers from v-model data shape
(flat array vs nested tree) and routes writes through the editor's
update path so future hooks (dirty rebaseline, validation registry,
etc.) all apply uniformly.
- migration-recipe.md updated with the new pattern: replace
`v-model="raw"` (which ESLint flags as updating an iteration
variable, and silently fails when forms emit a whole-object
update:modelValue) with the explicit
`:model-value="raw" @update:model-value="actions.update"` form.
- d.ts hygiene:
- LeChangeParentDialog Props promoted to a generic exported interface
so the auto-generated lib types can reference it without the
private-name TS4082 error.
- LeMoveToPositionDialog and AUnsavedConfirmDialog Props exported for
the same reason.
- tsconfig.libdts.json drops its unused tsconfig.node.json reference
(project reference required composite=true which conflicts with the
node config's noEmit:true; the reference was never actually used by
vite-plugin-dts anyway).
- Test type fixes:
- useUnsavedChangesGuard tests: explicit (c: unknown[]) annotation on
addSpy.mock.calls.find callbacks (no more implicit any).
- LeChangeParentDialog test: let TS infer resolveLabel /
calculateSubtreeDepth params from the component's erased generic;
cast inside the closure body.
- AListEditorUnsavedKeys test: switched from
InstanceType<typeof AListEditor> (broken for generic components)
to a local AListEditorExposed shape interface.
- New tests:
- useKeyboardNav.test.ts: 9 cases covering focus-on-delete and
grabbedIndex/totalCount.
- AListEditorSlotUpdate.test.ts: 3 cases covering actions.update
presence, write-through to v-model, and per-row scoping.
- i18n: new common.sortable.keyboardGrab.status key for en/sk/cs.
vue-tsc clean across tsconfig.app.json, tsconfig.test.json,
tsconfig.libdts.json. yarn build (lib + d.ts) clean. 357/357 tests
passing.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…s composable
ASortableListEditor gains two opt-in props for the "stacked editors" pattern
where one reorder gesture should drive multiple editors at once:
- embedded: hides own reorder button + Cancel/Apply toolbar, skips the
snapshot/restore (parent's deep snapshot covers nested data), paints
lighter chrome (transparent bg, uppercase title, no card border)
- allowEditInReorder: keeps inline edit alive in reorder mode so the open
row can expose a child editor's drag handles
New useNestedUnsavedKeys composable solves the per-parent unsaved-keys
aggregation with prefix-merged keys (`${parentKey}:${childKey}`), avoiding
the collision when two new rows under different parents share id=0.
Reorder toolbar gets a contextual "Reorder mode" label when no title is
set, plus a header-floating SCSS variant collapses the empty header band
when only the Reorder button would occupy it.
New playground view at /view/quiz-manage demonstrates the pattern with one
shared Reorder driving two stacked editors. 6 new tests cover the embedded
mode lifecycle and allowEditInReorder semantics; 363/363 passing.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…nd vuelidate fix
Adds 4 tests asserting the row rail colors across editing/unsaved combinations
— specifically that orange (warning) wins over blue (primary) on both the
row ::before and the body border-left when a row is editing+unsaved. Guards
against a regression on the embedded inner editor where the active+dirty
state must paint orange end-to-end.
Embedded "Add answer" button now matches the answer-row height
(min-height: var(--le-row-min-height)) and uses border-radius: 6px to align
with the row corners — same shape language as the rows above it instead of
a tiny pill at the bottom-left.
Playground quiz demo: switched the inline Vuelidate rules to the v2-correct
helpers.forEach() form (the bare $each: { ... } syntax was a v1 carry-over
and silently produced no per-row errors), and adapted the error-shape
check in getValidationState — `$errors[index]` is an OBJECT keyed by
property name, not an array, so `.length > 0` always returned undefined.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ding count Adds a SharedReorderRegistryKey provide/inject that lets embedded inner editors push their own movedCount + hasPendingChanges up to the nearest non-embedded outer editor. The outer's reorder toolbar then totals across all levels — drag a question OR drag an answer inside an open question and the same "N pending changes" counter increments either way. Without this, a move inside the embedded answers list never bumped the outer's count and the top-level Apply button stayed disabled until the user also moved a question. Wired automatically — consumers don't pass anything; embedded editors auto-register via inject(SharedReorderRegistryKey, null) on mount. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Six performance/refactoring extractions identified in the audit pass, all
opt-in (no behaviour changes for consumers):
* useUnsavedKeysSync composable — replaces the 35-LOC computed + 2
watchers + setsEqual helper + suppressNext flag duplicated 3× across
editors. ~110 LOC saved.
* useValidationRegistry composable — provide/inject + reactive Map +
resolveValidation priority chain duplicated 3×. ~60 LOC saved.
* resolveCompactText util — pure-function fallback chain extracted from
3 editors. ~36 LOC saved.
* Per-key actions cache — slot consumers now receive a stable identity
for actions.update / actions.edit / etc., eliminating spurious
re-renders that were cascading through deeply-bound child slots.
* Per-key decoratorCache for viewItemsDecorated — reuses the cached
object reference when its base view item AND every flag matches, so
rows whose state didn't change keep stable identity through Vue's
diff.
* dirtyKeys computed extracted out of viewItemsDecorated — depends only
on modelValue, so editing/expanded/loading flag flips no longer
re-stringify every row.
* Drag class names hoisted to internal/constants.ts.
* Dropped dead effectiveCloseVariant on flat + sortable.
* Dropped unused useContainerWidth call from flat (its only consumer
was the dead effectiveCloseVariant).
API polish:
* Slot scope interfaces (RowActions / RowSlotProps / Empty / AddButton /
Header / Toolbar / ReorderToggle) hoisted to top-level alongside Props,
marked `export interface`. vite-plugin-dts now rolls them up correctly.
* defineSlots now uses proper typed slot scopes in all 3 editors
(replacing `type NestedSlotScope = any` in nested; adding defineSlots
to flat + sortable which had none).
* `unsaved` field added to flat editor row scope (= dirty). Mirrors
sortable + nested for cross-variant slot ergonomics.
* `reorderDisabled` → `disableReorder` (consistency with disableDrag,
disableRowClick, disableDeleteConfirm).
* @deprecated JSDoc on legacy nested aliases (addAfterId, addChildToId,
removeById, updateData) pointing to canonical names.
* labs.ts re-exports ReorderModeValue, SharedReorderRegistry,
SharedReorderRegistryKey for consumers building custom toolbars.
CI cleanup:
* Pre-existing oxlint errors in useDeleteDialog (unused TItem) +
useNestedListEditor.test (unused api) fixed.
* Pre-existing eslint errors in 4 internal dialogs (defineProps order,
deprecated text-body-2) fixed.
* Deprecated CSS `clip` → `clip-path: inset(50%)` in shared SR-only.
* .playwright-cli/** ignored from eslint (debug scratch files).
* Stale `eslint-disable` directives removed from test files; new
file-level disables added where mutation-driven test patterns
legitimately need them.
* Unused onItemSave handler removed from playground SortableListEditorView.
367/367 tests passing, vue-tsc clean, vite-plugin-dts rollup clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds direct coverage for the composables extracted in the post-audit
refactor pass (previously tested only transitively through the editor
component tests):
* resolveCompactText (6) — priority chain (compactField → title → name
→ texts.title → text → key → fallback), null/undefined/empty handling
on the configured field, non-string coercion.
* useNestedUnsavedKeys (9) — per-parent set storage, prefix-merge for
id=0 collision avoidance, mixed key types, replace-vs-drop semantics,
reactivity of the merged set.
* useUnsavedKeysSync (10) — internal→external sync, full clear via
onClearAll, per-key clear via onClearKey, suppress-flag bookkeeping
that prevents ping-pong, imperative clearUnsavedState API.
* useDirtyBaseline (13) — initial baseline capture, dirty detection,
excludeFields (single + nested-editor multi), captureDirtyBaseline
rebaseline-all + key cleanup, rebaselineKey per-row, stringifyContent
contract.
* useValidationRegistry (12) — priority order (registry → prop → raw),
invalid-string fallthrough, descendant sentinel registration via
useListEditorItemValidation, unmount unregister, reactive state
propagation, multi-key non-collision, provide/inject contract.
417/417 tests passing; vue-tsc clean; lint clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Bump dev deps (@vue/test-utils, @vueuse/*, axios, eslint, oxlint, postcss, stylelint, vue-i18n, etc.) and add @vue/compiler-dom to satisfy the @vue/test-utils peer. - Swap vite-plugin-dts for unplugin-dts (with @microsoft/api-extractor) in the lib build config. - Switch reorder snapshot refs to shallowRef in ASortableListEditor / ANestedSortableListEditor — sidesteps the UnwrapRef mismatch against useReorderMode's Ref<T | null> signature. - Drop the dead effectiveCloseVariant computed in the flat editors (only the nested editor wires it through), drop unused TItem param on UseDeleteDialogApi, and clean up an unused playground handler + dead test bindings. - Ignore .playwright-cli/** in eslint config.
…in into feat/list-editors # Conflicts: # package.json # src/labs.ts # src/labs/listEditor/AListEditor.vue # src/labs/listEditor/ANestedSortableListEditor.vue # src/labs/listEditor/ASortableListEditor.vue # src/labs/listEditor/composables/useDeleteDialog.ts # src/test/components/ASortableListEditor.test.ts # src/test/composables/useNestedListEditor.test.ts # yarn.lock
…list-editors # Conflicts: # .gitignore # package.json # src/labs.ts # src/labs/listEditor/AListEditor.vue # src/labs/listEditor/ANestedSortableListEditor.vue # src/labs/listEditor/ASortableListEditor.vue # src/labs/listEditor/composables/resolveCompactText.ts # src/labs/listEditor/composables/useDeleteDialog.ts # src/labs/listEditor/composables/useDirtyBaseline.ts # src/labs/listEditor/composables/useDragInstruction.ts # src/labs/listEditor/composables/useInlineEditing.ts # src/labs/listEditor/composables/useKeyboardNav.ts # src/labs/listEditor/composables/useListEditorItemValidation.ts # src/labs/listEditor/composables/useNestedListEditor.ts # src/labs/listEditor/composables/useNestedUnsavedKeys.ts # src/labs/listEditor/composables/useReorderMode.ts # src/labs/listEditor/composables/useUnsavedKeysSync.ts # src/labs/listEditor/composables/useValidationRegistry.ts # src/labs/listEditor/internal/LeChangeParentDialog.vue # src/labs/listEditor/internal/LeMoveToPositionDialog.vue # src/labs/listEditor/internal/LeNestedRow.vue # src/labs/listEditor/styles/_shared.scss # src/labs/unsavedGuard/useUnsavedChangesGuard.ts # src/playground/listEditorView/ListEditorView.vue # src/playground/nestedSortableListEditorView/NestedSortableListEditorView.vue # src/playground/quizManageView/QuizManageQuestionAnswers.vue # src/playground/quizManageView/QuizManageQuestions.vue # src/playground/quizManageView/QuizManageView.vue # src/playground/quizManageView/quizMock.ts # src/playground/sortableListEditorView/SortableListEditorView.vue # src/test/components/AListEditor.test.ts # src/test/components/AListEditorSlotUpdate.test.ts # src/test/components/AListEditorValidation.test.ts # src/test/components/ANestedSortableListEditor.test.ts # src/test/components/ASortableListEditor.test.ts # src/test/components/ASortableListEditorRailColors.test.ts # src/test/components/LeChangeParentDialog.test.ts # src/test/composables/resolveCompactText.test.ts # src/test/composables/useDirtyBaseline.test.ts # src/test/composables/useKeyboardNav.test.ts # src/test/composables/useNestedListEditor.test.ts # src/test/composables/useNestedUnsavedKeys.test.ts # src/test/composables/useUnsavedChangesGuard.test.ts # src/test/composables/useUnsavedKeysSync.test.ts # src/test/composables/useValidationRegistry.test.ts # yarn.lock
Adds a `view-body` slot (with `ViewBodySlotProps`) so consumers can render their own view-mode layout (e.g. a card grid) while reorder mode keeps the editor's row layout. Pass `watchElement: true` to useSortable so SortableJS rebinds when the rows container mounts/unmounts across mode flips. Drop the unused `.a-le-reorder-mode-label` toolbar fallback. Header band now wraps (min-height + flex-wrap, tighter gap/padding) so a long status pill no longer pushes the toolbar buttons off-band; status pill gets ellipsis overflow. New playground view and 8-test spec exercise the slot. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces the hand-rolled `useSortable` plumbing (widgetEl, randomUuid, sortableInstance, forceRerender, widgetHtmlId, forceRerenderWidgetHtml, initSortable) with `<ASortableListEditor>`. The existing tile grid renders through the new `#view-body` slot; reorder mode shows a compact thumb + title + source row via `#item-compact`. AImageDropzone is hidden while in reorder mode. Reorder commits flow through `onReorderApplied` which renumbers positions. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
added 2 commits
May 22, 2026 07:42
Optional prop on AListEditor / ASortableListEditor / ANestedSortableListEditor. When true, dirty/moved are forced false in the row decorator → no orange markers, internalUnsavedKeys stays empty, never feeds the unsaved-keys v-model or the unsaved-changes dialog. Default false — no change anywhere else. Use case: collab-synced editors where data arrives via broadcast and non-moderators can never save (e.g. ArticleWidgetAuthors). + 6 vitest tests covering all three editors (true blocks flagging, false keeps current behavior as a control). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Removed the implicit fallback chain (`title → name → texts.title → text → key`) in `resolveCompactText`. Consumers must now opt in explicitly via the `compactField` prop or the `#item-compact` slot — otherwise the collapsed row title is empty. Previous behavior could surface raw row ids (UUIDs) when none of the well-known fields existed (e.g. the article bookmarks dialog showing `0edb72c6-…` instead of the article headline). - AListEditor / ASortableListEditor / ANestedSortableListEditor: drop the `key` arg + `fallback` option from the call site; template now renders `resolveCompactText(vi.raw)` only. - `resolveCompactText.ts`: simplified to `compactField`-only; returns `''` when not set or empty. - Tests: rewrote `resolveCompactText.test.ts` for the new semantics; component test mount helpers now pass `compactField: 'title'` explicitly (matching the pre-change implicit pick). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…evices In chip mode the only reorder affordance was native HTML5 drag, which doesn't work on touch devices. When `display.platform.touch` is true and reordering isn't disabled, render up/down arrow buttons next to the chip-close X. Drag remains the primary affordance on desktop. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Drag was enabled in chip mode even when the editor was readonly/disabled, letting non-moderators in collab visually reorder rows that wouldn't persist or broadcast. Gate dragEnabled on canInteract (readonly/disabled/loading) so readonly users see no drag handle and SortableJS stays disabled. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
defaultExpanded: - New optional prop on AListEditor. When true, every row renders its #item slot from mount, edit pencil is hidden, default inline Save/Cancel footer is hidden, row click is a no-op. Use for non-sortable editors where the form should always be visible at once (e.g. ThirdPartyTracker, bookmark dialogs). Slot scope `editing` is set to true for every row. - 9 vitest cases covering mount, slot scope, hidden chrome, row-click no-op, newly-added rows. Empty state: - Drop the secondary `<p>` hint text from LeEmptyState — title + add button only. Removes the `text`/`emptyText` props from all three editors (AListEditor / ASortableListEditor / ANestedSortableListEditor) and the `.a-le-empty-text` style. Old "Začnite pridaním novej položky." line is gone. - `.a-le-state--empty` modifier with padding 12px 16px (vs the 32px 16px baseline) tightens the vertical box around the empty hint. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Replace watch+stop with computed so a cached chip reused by Vuetifys positional VDataTable diff (e.g. toggling column visibility) reflects its new props.id instead of showing the previously loaded entrys data.
added 2 commits
June 1, 2026 13:35
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.