Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
75 commits
Select commit Hold shift + click to select a range
1914dca
Invert the wire-core → wire-forms action/modal form coupling behind a…
ONyklicek Jul 24, 2026
c289dc8
Upgrade TipTap to v3
ONyklicek Jul 25, 2026
74da9c8
Document the TipTap v3 upgrade in the changelog
ONyklicek Jul 25, 2026
f66181a
Unify the active row between pointer and keyboard in record actions
ONyklicek Jul 26, 2026
b2b4303
Keep the wire-forms bridge reference in core as plain text
ONyklicek Jul 26, 2026
2fd9ff7
Guard the record-actions dist bundle against source drift
ONyklicek Jul 26, 2026
91f855f
Add selection-gesture preview fixtures to the workbench
ONyklicek Jul 26, 2026
eef2a68
Characterize the selection gestures in a CDP driver
ONyklicek Jul 26, 2026
4662ce5
Fix the mobile driver's expectation for a pre-selected page
ONyklicek Jul 26, 2026
6d2d235
Reject a reserved onKey() binding out loud
ONyklicek Jul 26, 2026
c0879a2
Reserve every key the grid navigation claims
ONyklicek Jul 26, 2026
fb5b845
Grant grid semantics to selectable tables
ONyklicek Jul 26, 2026
19eb53a
Mount the delegated controller on every grid
ONyklicek Jul 26, 2026
be28ee4
Read the matching count from the DOM the morph refreshes
ONyklicek Jul 26, 2026
b9fb825
Normalize the entangled selection mode on the server
ONyklicek Jul 26, 2026
b92f502
Stop toggleAll() from inverting an all-matching selection
ONyklicek Jul 26, 2026
491efa2
Make the page gesture edit the exclusions in all mode, everywhere
ONyklicek Jul 26, 2026
1394504
Extract the selection component into a packaged wireRecordSelection
ONyklicek Jul 26, 2026
e194511
Pin the extracted selection component with its own test net
ONyklicek Jul 26, 2026
8fde063
Move the range anchor into the selection component, versioned
ONyklicek Jul 26, 2026
be9e38a
Range gestures stop rewriting the selection mode
ONyklicek Jul 26, 2026
5ccb981
Ranges write base ∪ block instead of replacing the selection
ONyklicek Jul 26, 2026
193444a
Bring the range gestures to the mouse
ONyklicek Jul 26, 2026
8cdffa6
Grow the keyboard: Home/End, PageUp/PageDown, mod+Shift+arrows
ONyklicek Jul 26, 2026
e674790
Backspace aliases Delete, Shift+F10 opens the menu, focus follows
ONyklicek Jul 26, 2026
eb51ae4
Mark the selection column with data-select-cell
ONyklicek Jul 26, 2026
a71ef61
Promote the drag-over-rows helpers into core support
ONyklicek Jul 26, 2026
351aeff
Sweep: drag down the checkbox column to select
ONyklicek Jul 27, 2026
2f6670b
Record the selection-gestures rollout progress through phase V
ONyklicek Jul 27, 2026
5efaab0
Let JS open the canonical modal shells via an openOn window event
ONyklicek Jul 27, 2026
28e54f1
Record the step-23 commit hash in the rollout progress
ONyklicek Jul 27, 2026
4aad86c
Harden openOn against attribute-name injection in the modal shells
ONyklicek Jul 27, 2026
fd368fa
Let a table describe its own keyboard gestures
ONyklicek Jul 27, 2026
270c4f6
Record steps 23 and 24 in the rollout progress
ONyklicek Jul 27, 2026
c7f8f2a
Show the table's keyboard shortcuts on ?
ONyklicek Jul 27, 2026
d334f71
Record phase VI (steps 23-25) in the rollout progress
ONyklicek Jul 27, 2026
b217ca8
Key each teleported modal by its id so two of a kind can coexist
ONyklicek Jul 27, 2026
9d0f867
Record the teleport-key fix and the painted-scrim gotcha
ONyklicek Jul 27, 2026
2c05921
Give the Select option modals distinct teleport keys
ONyklicek Jul 27, 2026
a20e61e
Describe the grid and its selection to assistive technology
ONyklicek Jul 27, 2026
4b7d3bc
Record step 26 in the rollout progress
ONyklicek Jul 27, 2026
efce0ab
Make the selection cell a real target and the active row visible with…
ONyklicek Jul 27, 2026
09961e7
Record step 27 in the rollout progress
ONyklicek Jul 27, 2026
52d567d
Document the selection gestures in both languages
ONyklicek Jul 27, 2026
340b865
Refresh the preview screenshots
ONyklicek Jul 27, 2026
e350d15
Pin the stacked-selection preview's page order to its seeded selection
ONyklicek Jul 27, 2026
aae38a1
Close out the selection-gestures rollout after step 28
ONyklicek Jul 27, 2026
c21531a
Mirror a column reorder onto the body by name, not by index
ONyklicek Jul 27, 2026
bafbeda
Record the column-reorder fix in the rollout progress
ONyklicek Jul 27, 2026
bda75c5
Raise the table coverage floor to 88%
ONyklicek Jul 27, 2026
971b65a
Record the raised coverage floor
ONyklicek Jul 27, 2026
369bfbe
Add a gesture lab: every new capability on one table, with a live rea…
ONyklicek Jul 27, 2026
8c28d2d
Give modals the focus, and give it back when they close
ONyklicek Jul 27, 2026
66b4f76
Record the modal focus fix in the rollout progress
ONyklicek Jul 27, 2026
5ebec4e
Extend the focus trap to the image cropper, and make it self-correcting
ONyklicek Jul 27, 2026
c324a40
Point the image-upload driver at the path it is actually testing
ONyklicek Jul 27, 2026
a1d180d
Make the two stale drivers measure the menu that is actually open
ONyklicek Jul 27, 2026
e845620
Stop a stray Chrome from hanging the driver sweep
ONyklicek Jul 27, 2026
acb4c11
Update CHANGELOG.md
ONyklicek Jul 27, 2026
38ccdaf
Test the server half of column reordering, and raise the sortable flo…
ONyklicek Jul 27, 2026
631ee54
Cover the shortcut, hover and field vocabularies, and raise both floo…
ONyklicek Jul 27, 2026
941c5df
Make minDate/maxDate mean the same thing in every shape
ONyklicek Jul 27, 2026
ca027e8
Send the table's Alpine bundles with the lazy placeholder
ONyklicek Jul 27, 2026
1cc22c7
Say in the lazy-loading docs which render ships the Alpine bundles
ONyklicek Jul 27, 2026
db78c2f
Name the real gate for the record controller in the lazy docs
ONyklicek Jul 27, 2026
ad340c7
Note in the boost guidelines what lazy() does not defer
ONyklicek Jul 27, 2026
d44ca06
Let a table switch its desktop gestures off, and fall back to buttons…
ONyklicek Jul 27, 2026
76ce6f4
Stop a mobile fallback button from answering the keyboard, and docume…
ONyklicek Jul 27, 2026
75087bf
Wait to be asked before answering an arrow key or a drag
ONyklicek Jul 27, 2026
2343888
Write the gesture layer down properly, in both languages and in boost
ONyklicek Jul 27, 2026
d793c3b
Point the AI routing files at the gesture layer
ONyklicek Jul 27, 2026
e65f83a
Let a Shift+click stay a click until the table says otherwise
ONyklicek Jul 28, 2026
94946c4
Show all three gesture states in the lab, and ask for gestures only w…
ONyklicek Jul 28, 2026
4efd8e8
Sort the changelog now that the gesture layer waits to be asked
ONyklicek Jul 28, 2026
8c11fbf
Merge branch '1.x' into 1.14.0
ONyklicek Jul 28, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions AI_COMPONENT_CATALOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -455,6 +455,15 @@ Main concerns:
- `Concerns\HasSqlDebug`
- `Concerns\HasSubRows`
- `Concerns\HasRecordTriggers` — record-action trigger vocabulary (on the `RecordAction` wrapper)
- `Concerns\HasGestures` — the table's side of the gesture layer

Gesture layer (which desktop pointer/keyboard gestures a table offers — OPT-IN):

- `Support\TableGestures` — the canonical vocabulary: `keyboard` (3-state), `rangeSelection`, `dragSelect`, `contextMenu`, `shortcutHelp`, `fillHandle`; `defaults()` (shipped: keyboard + dragSelect OFF) / `all()` / `none()` / `fromConfig()`
- `Concerns\HasGestures` — `Table::gestures(bool|Closure|TableGestures)`, `getGestures()`, and the effective readers `usesDragSelect()` / `usesRangeSelection()` / `usesShortcutHelp()` / `usesActiveRowMarker()` / `getGestureConfig()`
- Consumers to keep honest: `Table::usesGridSemantics()`, `mountsRecordActionController()`, `hasRowContextMenu()`, `isFillHandleEnabled()`, `Support\TableShortcutLegend`
- Project default: `config('wire-table.defaults.gestures')` — `null` shipped default / `true` all / `false` none / map
- Rule: a capability is a **permission, not a trigger**, and an explicitly declared record action is outside the layer (only `onKey()` needs the keyboard)

Record actions (whole-row interaction — click/dblclick/right-click/keyboard):

Expand All @@ -464,6 +473,7 @@ Record actions (whole-row interaction — click/dblclick/right-click/keyboard):
- `Actions\RecordActionResolver` — trigger→name pointer map, context-menu + row-button contributions, keyboard primary/secondary/shortcuts
- JS controller: `packages/table/resources/js/record-actions.js` → `wireRecordActions` (one per `<tbody>`); delivered via `packages/table/dist/wire-table-records.js` + `partials/record-actions-assets`
- Deprecated alias: `Table::rowContextMenu()` → prefer `recordAction()->onContextMenu()`
- Mobile fallback: `Table::getMobileRowActionsForDisplay()` / `hasMobileActions()` / `recordActionButtonsOnMobile(bool)` — behaviour-only bindings render as ordinary buttons on a stacked card (copies with `HasKeyboardShortcut::withoutKeyboardShortcut()`, since a rendered button binds its shortcut as a *window* listener)

Services:

Expand Down
9 changes: 9 additions & 0 deletions AI_RECIPES.md
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,15 @@ composer test:table
Use for sorting, pagination, search, row actions, selection, modals, grouping,
sub-rows, polling, summaries, and Livewire state behavior.

Gesture-layer rule (keyboard nav, ranges, drag sweep, context menu, `?` help,
fill handle): never add a local flag for one. `Support\TableGestures` +
`Concerns\HasGestures` own the decision, the layer is **opt-in** (keyboard and
drag sweep are off until a table calls `->gestures()`), and any fixture, preview
or test that asserts grid semantics has to ask for them first. A new capability
is a setter plus an `allows*()` reader there, an effective `uses*()` reader that
folds in the prerequisite, and a line in `getGestureConfig()` if the client
needs it.

Read first:

- `packages/table/src/Concerns/WithTable.php`
Expand Down
31 changes: 31 additions & 0 deletions CHANGELOG.md

Large diffs are not rendered by default.

9 changes: 9 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,9 @@ Before changing shared behavior, ask:
`architecture/table.md`
- Sortable behavior and table extension points:
`architecture/sortable.md`
- Row interaction — keyboard grid navigation, ranges, the drag sweep, the row
context menu, `?` help, the fill handle (all opt-in through `Table::gestures()`):
`architecture/table.md` § Gesture layer, then `docs/table/gestures.md`
- Anything spanning package boundaries:
`architecture/integrations.md`
- Shared design-system ownership, canonical UI semantics, color/size/icon surface work:
Expand Down Expand Up @@ -167,6 +170,12 @@ vendor/bin/testbench serve --host=127.0.0.1 --port=8085
npm run dev
npm run build

# Browser gate. The CDP drivers in workbench/scripts are the only check over
# Alpine/Livewire behaviour — Pest sees the markup, not what the browser does
# with it. Starts its own preview server, or reuses one already running.
npm run verify:drivers # all of them
npm run verify:drivers -- selection # only those matching a name

php docs-site/build.php
npm run docs:changed -- --dry-run
npm run docs:refresh
Expand Down
638 changes: 638 additions & 0 deletions architecture/plans/table-selection-gestures-implementation.md

Large diffs are not rendered by default.

208 changes: 208 additions & 0 deletions architecture/plans/table-selection-gestures-progress.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
---
title: Rollout výběrových gest — stav provedení
date: 2026-07-27
plan: architecture/plans/table-selection-gestures-rollout.md
status: HOTOVO — všech 28 kroků provedeno a zacommitováno
---

# Stav provedení rolloutu

Jeden krok = jeden commit. Každý krok prošel bránou (composer test:table,
Integration, analyse, lint, CDP drivery; coverage při zásahu do `src/*.php`).

## Hotové kroky → commity

| Krok | Commit | Poznámka |
|---|---|---|
| 0 | `f66181a` + `b2b4303` | WIP aktivního řádku + pint fixup (docblock core→forms bez importu) |
| 1 | `2fd9ff7` | drift test `wire-table-records.js` |
| 2 | `91f855f` | GestureRow (40 řádků), 3 preview varianty, `SortablePreview->selectable()` |
| 3 | `eef2a68` + `4662ce5` | `verify-selection-gestures.mjs` (C1–C13) + oprava očekávání mobilního driveru |
| 4 | `6d2d235` | `onKey()` rezervovanou klávesu vyhazuje (`TableConfigurationException`) |
| 5 | `c0879a2` | rozšířený rezervovaný seznam (bez `backspace`), CHANGELOG |
| 6 | `fb5b845` | `usesGridSemantics()` vlastní „je to grid“; mount přišpendlen přes `mountsRecordActionController()` |
| 7 | `19eb53a` | mount controlleru na každém gridu; CHANGELOG (BC) |
| 8 | `be28ee4` | `matching` z `data-matching` getterem (morph-safe) |
| 9 | `b9fb825` | serverová normalizace `selection.mode` (jen korupce; validní tvary nechává) |
| 10 | `b92f502` | `toggleAll()` v all módu neinvertuje |
| 11 | `491efa2` | kanonika: stránkové gesto v all módu edituje výjimky, mód neopouští (otočeny 2 aserce v `CanSelectRecordsTest`) |
| 12 | `1394504` | extrakce `wireRecordSelection` → `dist/wire-table-selection.js`; inline fallback ze zdrojáku |
| 13 | `e194511` | `SelectionAssetTest` + `SelectionRenderTest` |
| 14 | `8fde063` | kotva/rozsahy do selection komponenty; `data-selection-version="1"`; stale view → hlasitý console.error |
| 15 | `be9e38a` | rozsahy nepřepisují `mode`; `selectPage` = union + guard v all módu (C9 otočeno) |
| 16 | `5ccb981` | `base ∪ blok` se snapshotem; invalidace: setAnchor/clearAnchor pár, `$watch('mode')`, MutationObserver jen při zmizení kotvy |
| 17 | `193444a` | `Shift`/`mod`/`mod`+`Shift` klik (additive flag v `selectRange`) |
| 18 | `8cdffa6` | `Home`/`End`, `PageUp`/`PageDown` (pitch × skutečně scrollující předek), `mod`+`Shift`+šipky, fix `mod`+`A` shiftKey |
| 19 | `e674790` | `Backspace` alias v JS matcheru, `Shift`+`F10` (+ `_menuFromKey` obrana, headless neověřitelná), fokus do menu a zpět |
| 20 | `eb51ae4` | `data-select-cell` (td, karta, oba poziční spacery) |
| 21 | `a71ef61` | `createAutoScroller` + `bodyRows`/`rowAtY` → `core/resources/js/support/`; core dist rebuild |
| 22 | `351aeff` | sweep v `record-actions.js` (arm→engage, capture click-kill, jen myš, additive, morph guard, reduced-motion) |
| 23 | `5efaab0` + `4aad86c` | `openOn:` ve 3 shellech + Htmlable objektech + View komponentách; preview `core-open-on`; `verify-modal-open-on.mjs` 14/14; hardening proti injekci do jména atributu |
| 24 | `fd368fa` | `ShortcutLabelFormatter` + `ShortcutHint` (core Foundation) + `TableShortcutLegend` (table Support) + `Table::shortcutLegend()`; i18n EN+CS |
| 25 | `c7f8f2a` | `?` otevírá nápovědu (`shortcut-help` + `shortcut-help-modal` partial, `kb.help` v controlleru); driver 62/62 |
| 25b | `b217ca8` + `2c05921` | teleport `wire:key` z `$id` (dva Modal shelly v jedné komponentě); + předexistující díra ve forms select-option modalech |
| 26 | `a20e61e` | ARIA grid (`aria-rowcount`/`aria-rowindex` přes celou sadu, `aria-multiselectable`, bindnuté `aria-selected`) + `aria-live` region; driver 70/70 |

| 27 | `efce0ab` | klikatelná plocha = celá buňka, `[data-select-cell]` v `INTERACTIVE`, marker + pruh (kontrast 4.3/4.79 light, 3.98/7.95 dark); fix Shift+klik na checkbox; driver 77/77 |

| 28 | `52d567d` + `340b865` + `e350d15` | docs EN+CZ (nová `selection.md` + revize `record-actions.md`), upgrade guide, boost guidelines + mirror; screenshoty; fixture fix stacked-selection |

## Rozhodnutí učiněná při provádění (nad rámec plánu)

- **Krok 6/7 split:** interim vlastník mountu `Table::mountsRecordActionController()`;
v kroku 7 rozšířen o `usesGridSemantics()` a zůstává jako trvalý vlastník.
- **Krok 9:** records se maže JEN při korekci neznámého módu. Validní keys↔all
přechody jdou z klienta párově (mode+records) a wipe by zápis rozbil.
- **Krok 11:** kanonika „stránkové gesto v all módu edituje výjimky a mód
neopouští“ — `selectAllRecords()`/`deselectPageRecords()` v all módu zůstávají
v all; zúžení na stránku zůstává jen explicitní `selectOnlyPageRecords()`.
- **Krok 12:** `record-selection.js` je záměrně **bez importů** — asset partial
ho při chybějícím bundlu inlinuje doslovně (`SelectionAssetTest` to hlídá).
- **Krok 22:** sweep proto žije v `record-actions.js` (bundluje se s importy
z core supportu), ne ve `wireRecordSelection`; listenery pointerdown/click-kill
jsou na selection rootu, move/up na dokumentu.
- **Trailing click po sweepu:** flag musí přežít déle než `setTimeout(0)` —
click může přijít v pozdějším tasku; backstop je 150 ms, primárně one-shot
clear v capture handleru.
- **Krok 23:** `openOn` se ctí JEN při `wireModel === null` (jediný vlastník
`show`); detekce „bez bindingu“ na component path musí jít přes
`WireDirective::value()` — chybějící `wire:model` vrací directive s value
`false` a `filled(false)` je `true`. `x-on:{event}` je pozice **jména
atributu**, kde Blade escapuje jen uvozovky → mezera by vložila nový atribut;
proto whitelist `[a-zA-Z][a-zA-Z0-9_-]*` (nález background security review).
- **Krok 24:** `formatShortcutLabel()` v `HasKeyboardShortcut` deleguje na
`ShortcutLabelFormatter` — mění to i label akcí v `dropdown-item.blade.php`
a `header-action.blade.php` (šipky nově glyfy). `Foundation/ValueObjects/`
byl nový adresář, ale `AI_CODING_STANDARD.md:165` ho předepisuje.
- **Krok 25:** jméno `openOn` eventu je `wire-table-shortcut-help-` + prvních
12 znaků `md5($component->getId())`. Hash **musí** být lowercase: listener
sedí v **jménu atributu** (`x-on:{event}.window`) a DOM jména atributů
lowercasuje → syrové Livewire ID s velkými písmeny by se nikdy netrefilo
(CDP to odhalilo: modal v DOM, `show` zůstal false). Per-komponentní jméno
je nutné, aby `?` na stránce s více tabulkami neotevřel všechny nápovědy.
Mac/non-Mac labely: server rendruje Ctrl variantu, `x-text` ji na Macu
přepíše (platforma je klientský fakt) — headless Chrome na macOS hlásí Mac,
takže driver musí očekávat obě sady.
- **Krok 25 → oprava `b217ca8`:** shelly měly `wire:key` teleportu natvrdo
(`wire-modal-modal`). Nápověda to porušila: tabulka s formulářovou akcí
rendruje **dva Modal shelly v jedné Livewire komponentě** a morph klíčuje
podle `wire:key` → záměna obsahu. Klíč teď bere `$id`, když je zadané
(bez `$id` beze změny); nápověda posílá jako `id` jméno svého eventu.

- **Krok 28:** `boost:sync-docs` zrcadlí `docs/`, ale **ne** guidelines — ty se
editují ručně a mirror se pak přesyncuje. Screenshoty: workbench seed generuje
časy relativně k dnešku, takže každý `docs:refresh` změní datum na většině
snímků (šum, ne regrese) — a `workbench:build` reseedem posunul pořadí na
první stránce, což shodilo 2 checky `verify-mobile-selection.mjs`; fixture
proto dostal `defaultSort('id')`.
- **Krok 26:** `aria-rowindex` je pozice v CELÉ sadě, ne na stránce → nutné
vyzvednout `$from`/`$to`/`$total` z patičky do preambule (patička se rendruje
až po těle) a `$headerRowCount` musí započítat řádek column filtrů.
`aria-selected` MUSÍ být binding (`:aria-selected`) — statická hodnota se
morphem vrátí na serverovou pravdu. Live region: v DOM od prvního renderu
a **prázdný** (region oznamuje jen ZMĚNY obsahu; naplněný při bootu neřekne
nic a při bootu s předvybranými řádky by četl výběr, který uživatel neudělal)
→ `announceReady` se zapíná až prvním `$watch('selected'/'mode')`. Hlášky
chodí hotové z PHP (překlad je serverová věc), čísla se dosazují v JS.

- **Krok 27:** marker je `::before` overlay na `[&>td:first-of-type]`, NE border.
Důvody: (a) border posouvá obsah a rezervace transparentním borderem nejde —
`border-transparent` je v CSS ZA `border-primary-600`, takže by vždy vyhrála
bez ohledu na pořadí ve `class`; (b) overlay nastavuje vlastnost, kterou
klidové řádky vůbec nenastavují → žádný souboj v kaskádě. **`first-of-type`,
ne `first-child`** — prvním dítětem `<tr>` je teleport `<template>`
kontextového menu (jinak selektor nesedí na nic a vypadá to jako chyba
Tailwindu). Klikatelná plocha: handler JEN na `<td>`, button vlastní nemá
(klik i Enter/Space z něj bublají) — dva handlery = dvojí toggle, a `.stop`
na buttonu by zabil nastavení kotvy v controlleru.

## Objevené gotchas (platí i pro další kroky)

- Blade `x-data` atribut: dvojité uvozovky v JS komentáři ukousnou atribut
(Alpine Expression Error) — komentáře v x-data bez `"`.
- `overflow-x-auto` wrapper má computed `overflow-y: auto` (CSS páruje osy) —
„scrollující předek“ musí splnit i `scrollHeight > clientHeight`.
- Mid-sweep se objeví bulk bar a posune layout → CDP drag musí přeměřovat
waypointy (driver `drag()` to dělá).
- Perf testy v table sadě umí flaknout pod zátěží stroje (1× za rollout);
composer `process-timeout` 300 s zabije pest na vytíženém stroji — spustit
`vendor/bin/pest --configuration packages/core/phpunit.xml` přímo.
- `verify-mobile-selection.mjs`: stacked-selection preview má předseedovaný
výběr → první tap na strip odznačuje (spravené očekávání, 13/13).
- CDP checky počítající prvky (backdropy, markery) musí filtrovat na
**vykreslené**, ne na přítomné v DOM: zavřený modal si backdrop v dokumentu
nechává (`display:none`). `verify-nested-modal.mjs` na tom spadl, jakmile na
stránku přibyla nápověda (`painted=1 of 2`).

## Gesture lab — ruční i automatické ověření

`/previews/gesture-lab` (+ `-paged`) je jediná tabulka se **vším** zapnutým:
výběrová gesta, record actions (click/dblclick/onKey/contextmenu), přeřazování
sloupců, filtr sloupce, `?` nápověda, mobilní karty. Vedle tabulky je živý panel
(`data-testid="lab-*"`) se stavem, který gesta řídí — mode, výběr, kotva, base,
aktivní řádek, poslední hlášení live regionu a pořadí sloupců.

Jednotlivé drivery izolují jednu vlastnost; `verify-gesture-lab.mjs` (23/23) jde
naopak po **švech mezi nimi**: sweep → klávesnicový rozsah nad jedním výběrem,
akce nad označeným řádkem, přeřazení sloupce s aktivním výběrem (checkbox se
nesmí hnout, výběr přežít), nápověda vypisující akce této tabulky, `all` mód
a modifikátorové kliky. Driver na konci volá `resetColumnOrder()` — pořadí
sloupců se persistuje per user, takže bez úklidu by druhý běh startoval z cizího
stavu.

**Gotchas z labu:** panel nesmí číst live region/hlavičku getterem (plain DOM
není reaktivní závislost → getter se přepočítá jen náhodou; nutný
`MutationObserver`). Lab má filtrovatelný sloupec, takže má **dva** hlavičkové
řádky → `aria-rowcount` je 42, tělo začíná na indexu 3. Nápověda uvádí kontextové
menu jako jedno gesto, ne jednotlivé akce v něm.

## Stav sítě

- `verify-selection-gestures.mjs` — **77/77** (C1–C13 + myš, klávesnice, sweep,
selection-only, reduced-motion, sortable koexistence, `?` nápověda, ARIA +
live region, kontrast markeru, velikost cíle)
- `verify-nested-modal.mjs` 8/8, `verify-confirmation-object.mjs` 8/8,
`verify-modal-layering.mjs` 13/13 (modalové regrese po kroku 23/25)
- `verify-gesture-lab.mjs` — **23/23** (integrace napříč vlastnostmi),
`verify-column-reorder.mjs` 12/12
- `verify-record-active-row.mjs` 18/18, `verify-record-actions.mjs` 14/14,
`verify-record-actions-dual.mjs` 5/5, `verify-mobile-selection.mjs` 13/13,
`verify-fill-handle.mjs` 26/26, `verify-modal-open-on.mjs` 14/14
- PHP: celkem 4621 (table 1667, core 1751, forms 908, sortable 39), Integration 39;
analyse + lint OK; coverage diff 100 %, floors OK

## Rollout dokončen

Všech 28 kroků je hotových. Závěrečná brána (2026-07-27): `composer test`
4621 prošlo / 2 přeskočeno, Integration 39, analyse + lint OK, coverage diff
100 % a floors OK, **10 CDP driverů zelených** (selection-gestures 77/77,
record-active-row 18/18, record-actions 14/14, record-actions-dual 5/5,
mobile-selection 13/13, fill-handle 26/26, modal-open-on 14/14, nested-modal
8/8, confirmation-object 8/8, modal-layering 13/13), `npm run docs:check` OK
(228 souborů, obě lokalizace), `verify-api-docs` OK.

### Dořešeno po rolloutu

- `reorderBodyColumns()` — opraveno v `c21531a`. Skutečná příčina NEBYLA
selection buňka (ta je v hlavičce i těle symetricky), ale **teleport
`<template>`** kontextového menu na začátku `<tr>`: první datový sloupec je
v hlavičce na indexu 2, ale v těle na 3, takže se přeřazení trefilo do
checkboxu a ten skončil mezi datovými sloupci. Nově se páruje podle
`data-column`, ne podle indexu; sub-rows a group headery se nechávají být.
Nový driver `verify-column-reorder.mjs` (12/12) + preview `sortable-columns`.
**Gotcha:** Livewire re-render po `reorderColumns` poškozený DOM přepíše,
takže bug je normálně vidět jen jako poskok — TRVALÝ zůstane tam, kde server
pořadí neuloží (nepřihlášený uživatel, chybějící migrace). Driver proto měří
i s odpojeným voláním Livewiru, jinak by regresi neodhalil.

- **Fokus kolem modalu** — opraveno v `8c28d2d`. Modal shelly fokus vůbec
neřešily: zůstal na řádku ZA dialogem, takže Tab procházel stránku za modalem
(dialog nešel ovládat klávesnicí) a po zavření zůstal fokus na checkboxu →
šipky mrtvé (naměřeno ArrowDown 6 → 6), dokud uživatel neklikl na řádek. Nový
sdílený partial `modals/partials/focus-trap.blade.php` — expression-only (bez
závislosti na JS bundlu) a **bez dvojitých uvozovek**, které by Alpine atribut
usekly. 4 regresní checky v gesture labu, ověřeno že bez opravy padají.

- Coverage floor pro `table` zvednut 87 → 88 % (`bda75c5`). Rezerva je ~0,3 bodu
a floor gate je v CI **blokující**, přičemž CI měří pcov a lokál xdebug — při
poklesu pokrytí to spadne dřív než u ostatních balíčků.
Loading