@nxgt/material-core 0.10.0 → 0.11.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,80 @@
1
1
  # @nxgt/material-core
2
2
 
3
+ ## 0.11.1
4
+
5
+ ### Patch Changes
6
+
7
+ - [#607](https://github.com/softistx/nxgt-material/pull/607) [`3b671a5`](https://github.com/softistx/nxgt-material/commit/3b671a5b8d0109fa645d0de52bc716104f7f8831) Thanks [@SteveGT96](https://github.com/SteveGT96)! - Two fixes seen on a phone and on the collapsed sidebar (softistx/nxgt-material#605).
8
+
9
+ - **`SidebarMenuItem` no tooltip beside a visible label.** The collapsed rail showed each label under its icon and also opened a tooltip repeating it, above the entry, over the entry above it (the selected pill). An entry that shows its label, collapsed or expanded, now has no tooltip; its accessible name is the label. A `tooltip` you pass is still shown, now to the right of the entry (`side="right"`) so it never covers a neighbour, and an icon-only entry (no label, an `aria-label`) shows its `aria-label` there. Keyboard focus shows it as before. `Tooltip` (React) takes a `side` prop. In `@nxgt/material-vue`, `SidebarMenuItem` now binds its attributes (`aria-label`, `onClick`...) to the link or button itself.
10
+ - **`Toaster` on a phone starts below the app bar.** Under 600px with no `position`, toasts sat at the very top and covered the app shell's sticky bar (menu button, title, account control), which could not be reached while a toast showed. They now start 80px down (`mobileOffset` `{ top: 80 }`, from `@nxgt/material-core/touch-hover`). Desktop is unchanged, and a `position` or a `mobileOffset` you pass is kept as is.
11
+
12
+ Migration: an app that relied on the toast at the very top of a phone, or on the collapsed rail's label tooltip, sets `<Toaster mobileOffset={{ top: 16 }} />` (Vue: `:mobile-offset="{ top: 16 }"`) or passes `tooltip` on the entry.
13
+
14
+ ## 0.11.0
15
+
16
+ ### Minor Changes
17
+
18
+ - [#574](https://github.com/softistx/nxgt-material/pull/574) [`c00f8c3`](https://github.com/softistx/nxgt-material/commit/c00f8c366e35b7769560bb86b964ae0705bcc60f) Thanks [@SteveGT96](https://github.com/SteveGT96)! - Add `RepeatableGroup` and `RepeatableGroupFormField` (Storybook
19
+ `Form fields/RepeatableGroup`): rows of fields a user adds, removes and, with
20
+ `reorderable`, moves up and down. `RepeatableGroupFormField` wraps the form
21
+ library's `useFieldArray` (react-hook-form in React, vee-validate in Vue) and
22
+ renders each row from `renderRow` (React) or the `row` slot (Vue), whose
23
+ `path` names the row's fields. The rows are a list (`ul` / `li`) in a group
24
+ named by `label`; each row's buttons are named after it ("Remove period 2",
25
+ "Move period 2 up"), from the new `material.repeatable-group.*` keys in
26
+ English and French. An added row's first field takes the focus, a removed row
27
+ hands it to its neighbour or to Add, and a moved row keeps it on its move
28
+ button; each change is announced politely. `max` makes Add `aria-disabled`,
29
+ described by "Maximum reached (n)"; `min` disables Remove; `emptyTitle` /
30
+ `emptyDescription` show an `EmptyState` with no row. The array's own error
31
+ shows under the rows and describes the group; each row's fields show their
32
+ own. One column on a phone, a responsive grid wider up, no inline style.
33
+
34
+ `@nxgt/material-core` gains `/repeatable-group`: the focus rules
35
+ (`focusAfterRemove`, `focusAfterMove`, `focusRowTarget`), the limits
36
+ (`canAddRow`, `canRemoveRow`, `rowMoveTarget`) and the `RowAnnouncement`
37
+ descriptor both libraries use.
38
+
39
+ ```tsx
40
+ <RepeatableGroupFormField
41
+ control={control}
42
+ name="periods"
43
+ label="Periods"
44
+ rowLabel="period"
45
+ max={6}
46
+ reorderable
47
+ newRow={() => ({ name: '' })}
48
+ renderRow={({ index }) => (
49
+ <TextFormField control={control} name={`periods.${index}.name`} label="Name" />
50
+ )}
51
+ />
52
+ ```
53
+
54
+ ```vue
55
+ <RepeatableGroupFormField
56
+ name="periods"
57
+ label="Periods"
58
+ row-label="period"
59
+ :max="6"
60
+ reorderable
61
+ :new-row="() => ({ name: '' })"
62
+ >
63
+ <template #row="{ path }">
64
+ <TextFormField :name="`${path}.name`" label="Name" />
65
+ </template>
66
+ </RepeatableGroupFormField>
67
+ ```
68
+
69
+ Fixes [#516](https://github.com/softistx/nxgt-material/issues/516).
70
+
71
+ Migration: an app with a local field-array list marked temporary for this
72
+ request ([#516](https://github.com/softistx/nxgt-material/issues/516)) — a `useFieldArray` whose rows are mapped by hand, with its own
73
+ Add and per-row Remove buttons — can delete it and render
74
+ `RepeatableGroupFormField` instead: the array's `name`, `newRow` for what Add
75
+ appends, and the row's fields moved into `renderRow` / the `row` slot. Pass no
76
+ `shouldFocus` to `append`: the group moves the focus itself.
77
+
3
78
  ## 0.10.0
4
79
 
5
80
  ### Minor Changes
package/README.md CHANGED
@@ -8,7 +8,8 @@ half (page geometry, scroll spy, text search), the currency field's decimals,
8
8
  the file-size formatter, the slider's tick marks, the emoji list's groups,
9
9
  the date range presets, a debounce, accent-insensitive text matching and a race-free async search (`/debounce`, `/text-match`, `/async-search`), the file preview's kind detection, the fields' blur
10
10
  rule, a field's first reachable control (`/focus-control`) and its `aria-describedby`
11
- (`/described-by`), the page behind a modal layer made inert
11
+ (`/described-by`), where the focus goes in a repeatable row group
12
+ (`/repeatable-group`), the page behind a modal layer made inert
12
13
  (`/inert-others`), Escape pressed while an overlay closes
13
14
  (`/escape-forward`), the Toaster's guard against a tap's emulated hover
14
15
  (`/touch-hover`), the focus a toast's action hands back (`/toast-focus`), a
@@ -88,6 +89,7 @@ built-in.
88
89
  | `/currency-digits` | `currencyFractionDigits` — the decimals `CurrencyField` shows |
89
90
  | `/field-blur-layers` | `layerOf`, `openerOf`, `FieldLayers` — whose overlay a layer of the page is, for a field's blur |
90
91
  | `/focus-control` | `firstControl`, `focusHandle`, `FocusHandle` — the first control of a field a user can reach, and a stand-in element that focuses it for a form library |
92
+ | `/repeatable-group` | `focusAfterRemove`, `focusAfterMove`, `focusRowTarget`, `rowMoveTarget`, `canAddRow`, `canRemoveRow`, `RowFocusTarget`, `RowMoveDirection`, `RowAnnouncement`, `ROW_SLOT`, `ROW_FIELDS_SLOT`, `ADD_ATTRIBUTE` — where the focus goes after a row of a `RepeatableGroup` is added, removed or moved, and what its live region says |
91
93
  | `/escape-forward` | `forwardEscape` — Escape pressed while an overlay closes, pressed again for the overlay below once it has gone |
92
94
  | `/described-by` | `describedBy` — an `aria-describedby` value from the caller's ids and a field's help and error ids |
93
95
  | `/inert-others` | `inertOthers`, `INERT_MARKER` — the rest of the page `inert` behind a modal layer, live regions kept, nested layers counted |
@@ -811,6 +813,44 @@ firstControl(roots)?.focus(); // the first date input, not a hidden one
811
813
  field.ref(focusHandle(roots)); // React Hook Form: shouldFocusError reaches it
812
814
  ```
813
815
 
816
+ ### Rows added, removed and moved: `/repeatable-group`
817
+
818
+ What both libraries' `RepeatableGroup` decides, as plain functions.
819
+ `focusAfterRemove(removed, remaining)` names where the focus goes once a row
820
+ is gone: the row that took its place, else the one before, else the Add
821
+ button. `focusAfterMove(to, direction, count)` keeps it on the move button
822
+ that was pressed, or on the row's other one when the row reached either end.
823
+ `focusRowTarget(root, target)` moves the focus there, reading the DOM when
824
+ called (rows are `[data-slot="repeatable-group-row"]`, the move buttons carry
825
+ `data-move`, the Add button `data-repeatable-group-add`). `canAddRow(count, max)`, `canRemoveRow(count, min)` and
826
+ `rowMoveTarget(index, direction, count)` are the limits (`RowMoveDirection`
827
+ is `'up' | 'down'`), and `RowAnnouncement` is what the live region says
828
+ (1-based positions). The DOM markers are exported as `ROW_SLOT`
829
+ (`'repeatable-group-row'`), `ROW_FIELDS_SLOT` (`'repeatable-group-fields'`)
830
+ and `ADD_ATTRIBUTE` (`'data-repeatable-group-add'`).
831
+
832
+ ```ts
833
+ import {
834
+ canAddRow,
835
+ canRemoveRow,
836
+ focusAfterRemove,
837
+ focusRowTarget,
838
+ type RowAnnouncement,
839
+ rowMoveTarget,
840
+ } from '@nxgt/material-core/repeatable-group';
841
+
842
+ const addable = canAddRow(rows.length, 6); // false at 6 rows
843
+ const removable = canRemoveRow(rows.length, 1); // false at 1 row
844
+ const up = rowMoveTarget(index, 'up', rows.length); // null for the first row
845
+
846
+ remove(index);
847
+ await nextTick(); // or after React commits
848
+ focusRowTarget(groupElement, focusAfterRemove(index, rows.length));
849
+
850
+ const said: RowAnnouncement = { kind: 'removed', index: index + 1 };
851
+ status.textContent = said.kind === 'removed' ? `Removed period ${said.index}` : '';
852
+ ```
853
+
814
854
  ### Escape while an overlay closes: `/escape-forward`
815
855
 
816
856
  Radix's and reka's dismissable layers give Escape to the highest layer only,
@@ -0,0 +1,41 @@
1
+ // src/lib/focus-control.ts
2
+ var TABBABLE = 'input:not([type="hidden"]):not(:disabled):not([tabindex="-1"]), textarea:not(:disabled):not([tabindex="-1"]), select:not(:disabled):not([tabindex="-1"]), button:not(:disabled):not([tabindex="-1"]), [contenteditable="true"], [tabindex]:not([tabindex="-1"])';
3
+ function firstControl2(roots) {
4
+ for (const root of roots) {
5
+ const controls = root.matches(TABBABLE) ? [root] : root.querySelectorAll(TABBABLE);
6
+ for (const control of controls) {
7
+ if (control.getClientRects().length)
8
+ return control;
9
+ }
10
+ }
11
+ return null;
12
+ }
13
+ function focusHandle2(roots) {
14
+ return {
15
+ focus: () => {
16
+ firstControl2(roots)?.focus();
17
+ setTimeout(() => {
18
+ const list = [...roots];
19
+ const active = list[0]?.ownerDocument.activeElement ?? null;
20
+ if (!list.some((root) => root.contains(active))) {
21
+ firstControl2(list)?.focus();
22
+ }
23
+ });
24
+ },
25
+ select: () => {
26
+ const control = firstControl2(roots);
27
+ if (control && "select" in control) {
28
+ control.select();
29
+ }
30
+ },
31
+ setCustomValidity: () => {
32
+ return;
33
+ },
34
+ reportValidity: () => true
35
+ };
36
+ }
37
+
38
+ export { firstControl2, focusHandle2 };
39
+
40
+ //# debugId=084F2A6FA5DCD68964756E2164756E21
41
+ //# sourceMappingURL=index-407b6zs0.js.map
@@ -0,0 +1,10 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/lib/focus-control.ts"],
4
+ "sourcesContent": [
5
+ "/** What a user can Tab to inside a field. */\nconst TABBABLE =\n\t'input:not([type=\"hidden\"]):not(:disabled):not([tabindex=\"-1\"]), textarea:not(:disabled):not([tabindex=\"-1\"]), select:not(:disabled):not([tabindex=\"-1\"]), button:not(:disabled):not([tabindex=\"-1\"]), [contenteditable=\"true\"], [tabindex]:not([tabindex=\"-1\"])';\n\n/**\n * The first control of `roots` a user can reach, in document order: one a\n * user can Tab to that is rendered — not a `display: none` input a library\n * keeps for forms. A root that is itself such a control is its own first\n * control. `null` when none is. Reads the DOM when called.\n */\nexport function firstControl(roots: Iterable<Element>): HTMLElement | null {\n\tfor (const root of roots) {\n\t\tconst controls = root.matches(TABBABLE)\n\t\t\t? [root as HTMLElement]\n\t\t\t: root.querySelectorAll<HTMLElement>(TABBABLE);\n\t\tfor (const control of controls) {\n\t\t\tif (control.getClientRects().length) return control;\n\t\t}\n\t}\n\treturn null;\n}\n\n/** What a form library calls on the element a field registered. */\nexport type FocusHandle = {\n\tfocus: () => void;\n\tselect: () => void;\n\tsetCustomValidity: (message: string) => void;\n\treportValidity: () => boolean;\n};\n\n/**\n * A stand-in element for a field made of several controls, for a form\n * library that focuses an invalid field by calling `focus()` on what the\n * field registered (React Hook Form's `field.ref`). `focus()` focuses the\n * first control of `roots` a user can reach, looked up when it is called —\n * `roots` is read live, so pass the field's own set — and again a turn\n * later when focus is not in the field by then: the error that triggered it\n * may re-render the field and replace its control. `select()` selects that\n * control's text when it has any. Native validation is not used:\n * `setCustomValidity` does nothing and `reportValidity` returns `true`.\n */\nexport function focusHandle(roots: Iterable<Element>): FocusHandle {\n\treturn {\n\t\tfocus: () => {\n\t\t\tfirstControl(roots)?.focus();\n\t\t\tsetTimeout(() => {\n\t\t\t\tconst list = [...roots];\n\t\t\t\tconst active = list[0]?.ownerDocument.activeElement ?? null;\n\t\t\t\tif (!list.some((root) => root.contains(active))) {\n\t\t\t\t\tfirstControl(list)?.focus();\n\t\t\t\t}\n\t\t\t});\n\t\t},\n\t\tselect: () => {\n\t\t\tconst control = firstControl(roots);\n\t\t\tif (control && 'select' in control) {\n\t\t\t\t(control as HTMLInputElement).select();\n\t\t\t}\n\t\t},\n\t\tsetCustomValidity: () => undefined,\n\t\treportValidity: () => true,\n\t};\n}\n"
6
+ ],
7
+ "mappings": ";AACA,IAAM,WACL;AAQM,SAAS,aAAY,CAAC,OAA8C;AAAA,EAC1E,WAAW,QAAQ,OAAO;AAAA,IACzB,MAAM,WAAW,KAAK,QAAQ,QAAQ,IACnC,CAAC,IAAmB,IACpB,KAAK,iBAA8B,QAAQ;AAAA,IAC9C,WAAW,WAAW,UAAU;AAAA,MAC/B,IAAI,QAAQ,eAAe,EAAE;AAAA,QAAQ,OAAO;AAAA,IAC7C;AAAA,EACD;AAAA,EACA,OAAO;AAAA;AAsBD,SAAS,YAAW,CAAC,OAAuC;AAAA,EAClE,OAAO;AAAA,IACN,OAAO,MAAM;AAAA,MACZ,cAAa,KAAK,GAAG,MAAM;AAAA,MAC3B,WAAW,MAAM;AAAA,QAChB,MAAM,OAAO,CAAC,GAAG,KAAK;AAAA,QACtB,MAAM,SAAS,KAAK,IAAI,cAAc,iBAAiB;AAAA,QACvD,IAAI,CAAC,KAAK,KAAK,CAAC,SAAS,KAAK,SAAS,MAAM,CAAC,GAAG;AAAA,UAChD,cAAa,IAAI,GAAG,MAAM;AAAA,QAC3B;AAAA,OACA;AAAA;AAAA,IAEF,QAAQ,MAAM;AAAA,MACb,MAAM,UAAU,cAAa,KAAK;AAAA,MAClC,IAAI,WAAW,YAAY,SAAS;AAAA,QAClC,QAA6B,OAAO;AAAA,MACtC;AAAA;AAAA,IAED,mBAAmB,MAAG;AAAA,MAAG;AAAA;AAAA,IACzB,gBAAgB,MAAM;AAAA,EACvB;AAAA;",
8
+ "debugId": "084F2A6FA5DCD68964756E2164756E21",
9
+ "names": []
10
+ }
@@ -1,43 +1,11 @@
1
- // src/lib/focus-control.ts
2
- var TABBABLE = 'input:not([type="hidden"]):not(:disabled):not([tabindex="-1"]), textarea:not(:disabled):not([tabindex="-1"]), select:not(:disabled):not([tabindex="-1"]), button:not(:disabled):not([tabindex="-1"]), [contenteditable="true"], [tabindex]:not([tabindex="-1"])';
3
- function firstControl(roots) {
4
- for (const root of roots) {
5
- const controls = root.matches(TABBABLE) ? [root] : root.querySelectorAll(TABBABLE);
6
- for (const control of controls) {
7
- if (control.getClientRects().length)
8
- return control;
9
- }
10
- }
11
- return null;
12
- }
13
- function focusHandle(roots) {
14
- return {
15
- focus: () => {
16
- firstControl(roots)?.focus();
17
- setTimeout(() => {
18
- const list = [...roots];
19
- const active = list[0]?.ownerDocument.activeElement ?? null;
20
- if (!list.some((root) => root.contains(active))) {
21
- firstControl(list)?.focus();
22
- }
23
- });
24
- },
25
- select: () => {
26
- const control = firstControl(roots);
27
- if (control && "select" in control) {
28
- control.select();
29
- }
30
- },
31
- setCustomValidity: () => {
32
- return;
33
- },
34
- reportValidity: () => true
35
- };
36
- }
1
+ import {
2
+ firstControl2,
3
+ focusHandle2
4
+ } from "../chunks/index-407b6zs0.js";
37
5
  export {
38
- firstControl,
39
- focusHandle
6
+ firstControl2 as firstControl,
7
+ focusHandle2 as focusHandle
40
8
  };
41
9
 
42
- //# debugId=F6055B3989727FBE64756E2164756E21
10
+ //# debugId=3CD7CAE6F251A45964756E2164756E21
43
11
  //# sourceMappingURL=focus-control.js.map
@@ -1,10 +1,9 @@
1
1
  {
2
2
  "version": 3,
3
- "sources": ["../src/lib/focus-control.ts"],
3
+ "sources": [],
4
4
  "sourcesContent": [
5
- "/** What a user can Tab to inside a field. */\nconst TABBABLE =\n\t'input:not([type=\"hidden\"]):not(:disabled):not([tabindex=\"-1\"]), textarea:not(:disabled):not([tabindex=\"-1\"]), select:not(:disabled):not([tabindex=\"-1\"]), button:not(:disabled):not([tabindex=\"-1\"]), [contenteditable=\"true\"], [tabindex]:not([tabindex=\"-1\"])';\n\n/**\n * The first control of `roots` a user can reach, in document order: one a\n * user can Tab to that is rendered — not a `display: none` input a library\n * keeps for forms. A root that is itself such a control is its own first\n * control. `null` when none is. Reads the DOM when called.\n */\nexport function firstControl(roots: Iterable<Element>): HTMLElement | null {\n\tfor (const root of roots) {\n\t\tconst controls = root.matches(TABBABLE)\n\t\t\t? [root as HTMLElement]\n\t\t\t: root.querySelectorAll<HTMLElement>(TABBABLE);\n\t\tfor (const control of controls) {\n\t\t\tif (control.getClientRects().length) return control;\n\t\t}\n\t}\n\treturn null;\n}\n\n/** What a form library calls on the element a field registered. */\nexport type FocusHandle = {\n\tfocus: () => void;\n\tselect: () => void;\n\tsetCustomValidity: (message: string) => void;\n\treportValidity: () => boolean;\n};\n\n/**\n * A stand-in element for a field made of several controls, for a form\n * library that focuses an invalid field by calling `focus()` on what the\n * field registered (React Hook Form's `field.ref`). `focus()` focuses the\n * first control of `roots` a user can reach, looked up when it is called —\n * `roots` is read live, so pass the field's own set — and again a turn\n * later when focus is not in the field by then: the error that triggered it\n * may re-render the field and replace its control. `select()` selects that\n * control's text when it has any. Native validation is not used:\n * `setCustomValidity` does nothing and `reportValidity` returns `true`.\n */\nexport function focusHandle(roots: Iterable<Element>): FocusHandle {\n\treturn {\n\t\tfocus: () => {\n\t\t\tfirstControl(roots)?.focus();\n\t\t\tsetTimeout(() => {\n\t\t\t\tconst list = [...roots];\n\t\t\t\tconst active = list[0]?.ownerDocument.activeElement ?? null;\n\t\t\t\tif (!list.some((root) => root.contains(active))) {\n\t\t\t\t\tfirstControl(list)?.focus();\n\t\t\t\t}\n\t\t\t});\n\t\t},\n\t\tselect: () => {\n\t\t\tconst control = firstControl(roots);\n\t\t\tif (control && 'select' in control) {\n\t\t\t\t(control as HTMLInputElement).select();\n\t\t\t}\n\t\t},\n\t\tsetCustomValidity: () => undefined,\n\t\treportValidity: () => true,\n\t};\n}\n"
6
5
  ],
7
- "mappings": ";AACA,IAAM,WACL;AAQM,SAAS,YAAY,CAAC,OAA8C;AAAA,EAC1E,WAAW,QAAQ,OAAO;AAAA,IACzB,MAAM,WAAW,KAAK,QAAQ,QAAQ,IACnC,CAAC,IAAmB,IACpB,KAAK,iBAA8B,QAAQ;AAAA,IAC9C,WAAW,WAAW,UAAU;AAAA,MAC/B,IAAI,QAAQ,eAAe,EAAE;AAAA,QAAQ,OAAO;AAAA,IAC7C;AAAA,EACD;AAAA,EACA,OAAO;AAAA;AAsBD,SAAS,WAAW,CAAC,OAAuC;AAAA,EAClE,OAAO;AAAA,IACN,OAAO,MAAM;AAAA,MACZ,aAAa,KAAK,GAAG,MAAM;AAAA,MAC3B,WAAW,MAAM;AAAA,QAChB,MAAM,OAAO,CAAC,GAAG,KAAK;AAAA,QACtB,MAAM,SAAS,KAAK,IAAI,cAAc,iBAAiB;AAAA,QACvD,IAAI,CAAC,KAAK,KAAK,CAAC,SAAS,KAAK,SAAS,MAAM,CAAC,GAAG;AAAA,UAChD,aAAa,IAAI,GAAG,MAAM;AAAA,QAC3B;AAAA,OACA;AAAA;AAAA,IAEF,QAAQ,MAAM;AAAA,MACb,MAAM,UAAU,aAAa,KAAK;AAAA,MAClC,IAAI,WAAW,YAAY,SAAS;AAAA,QAClC,QAA6B,OAAO;AAAA,MACtC;AAAA;AAAA,IAED,mBAAmB,MAAG;AAAA,MAAG;AAAA;AAAA,IACzB,gBAAgB,MAAM;AAAA,EACvB;AAAA;",
8
- "debugId": "F6055B3989727FBE64756E2164756E21",
6
+ "mappings": "",
7
+ "debugId": "3CD7CAE6F251A45964756E2164756E21",
9
8
  "names": []
10
9
  }
@@ -0,0 +1,70 @@
1
+ /** Which way a row moves: towards the first row, or towards the last. */
2
+ export type RowMoveDirection = 'up' | 'down';
3
+ /**
4
+ * Where the focus goes once a RepeatableGroup's rows have changed: a row's
5
+ * first field, the Add button, or one of a row's move buttons. Indexes are
6
+ * 0-based, in the rows as they are after the change.
7
+ */
8
+ export type RowFocusTarget = {
9
+ kind: 'field';
10
+ index: number;
11
+ } | {
12
+ kind: 'add';
13
+ } | {
14
+ kind: 'move';
15
+ index: number;
16
+ direction: RowMoveDirection;
17
+ };
18
+ /**
19
+ * What the group's polite live region says about a change. `index`, `from`
20
+ * and `to` are 1-based: they are what a user reads ("period 2").
21
+ */
22
+ export type RowAnnouncement = {
23
+ kind: 'added';
24
+ index: number;
25
+ } | {
26
+ kind: 'removed';
27
+ index: number;
28
+ } | {
29
+ kind: 'moved';
30
+ from: number;
31
+ to: number;
32
+ };
33
+ /** Another row can be added: no `max`, or fewer rows than it. */
34
+ export declare const canAddRow: (count: number, max?: number) => boolean;
35
+ /** A row can be removed: no `min`, or more rows than it. */
36
+ export declare const canRemoveRow: (count: number, min?: number) => boolean;
37
+ /**
38
+ * The index the row at `index` moves to, or `null` when it cannot move that
39
+ * way (the first row up, the last one down).
40
+ */
41
+ export declare function rowMoveTarget(index: number, direction: RowMoveDirection, count: number): number | null;
42
+ /**
43
+ * Where the focus goes once the row at `removed` is gone and `remaining`
44
+ * rows are left: the row that took its place, else the row before it (the
45
+ * last one was removed), else the Add button (no row is left).
46
+ */
47
+ export declare function focusAfterRemove(removed: number, remaining: number): RowFocusTarget;
48
+ /**
49
+ * Where the focus goes once a row moved `direction` to `to`: the button that
50
+ * moved it, so the user can press it again — unless the row can no longer
51
+ * move that way (it reached the first or the last place, and that button is
52
+ * now disabled), then the other move button of the same row.
53
+ */
54
+ export declare function focusAfterMove(to: number, direction: RowMoveDirection, count: number): RowFocusTarget;
55
+ /** The `data-slot` of each row and of its fields. */
56
+ export declare const ROW_SLOT = "repeatable-group-row";
57
+ export declare const ROW_FIELDS_SLOT = "repeatable-group-fields";
58
+ /**
59
+ * The attribute that marks the Add button: an attribute of its own, as the
60
+ * button keeps the `data-slot` of the library's Button.
61
+ */
62
+ export declare const ADD_ATTRIBUTE = "data-repeatable-group-add";
63
+ /**
64
+ * Moves the focus to `target` inside the group's `root`: the first control a
65
+ * user can reach in the row's fields, the Add button (`data-repeatable-group-add`), or the row's move
66
+ * button (`data-move="up"` / `"down"`). Reads the DOM when called; returns
67
+ * whether something took the focus.
68
+ */
69
+ export declare function focusRowTarget(root: Element, target: RowFocusTarget): boolean;
70
+ //# sourceMappingURL=repeatable-group.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"repeatable-group.d.ts","sourceRoot":"","sources":["../../src/lib/repeatable-group.ts"],"names":[],"mappings":"AAEA,yEAAyE;AACzE,MAAM,MAAM,gBAAgB,GAAG,IAAI,GAAG,MAAM,CAAC;AAE7C;;;;GAIG;AACH,MAAM,MAAM,cAAc,GACvB;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GAChC;IAAE,IAAI,EAAE,KAAK,CAAA;CAAE,GACf;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,gBAAgB,CAAA;CAAE,CAAC;AAEhE;;;GAGG;AACH,MAAM,MAAM,eAAe,GACxB;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GAChC;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GAClC;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,MAAM,CAAA;CAAE,CAAC;AAE/C,iEAAiE;AACjE,eAAO,MAAM,SAAS,GAAI,OAAO,MAAM,EAAE,MAAM,MAAM,KAAG,OACvB,CAAC;AAElC,4DAA4D;AAC5D,eAAO,MAAM,YAAY,GAAI,OAAO,MAAM,EAAE,MAAM,MAAM,KAAG,OAC1B,CAAC;AAElC;;;GAGG;AACH,wBAAgB,aAAa,CAC5B,KAAK,EAAE,MAAM,EACb,SAAS,EAAE,gBAAgB,EAC3B,KAAK,EAAE,MAAM,GACX,MAAM,GAAG,IAAI,CAGf;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAC/B,OAAO,EAAE,MAAM,EACf,SAAS,EAAE,MAAM,GACf,cAAc,CAGhB;AAED;;;;;GAKG;AACH,wBAAgB,cAAc,CAC7B,EAAE,EAAE,MAAM,EACV,SAAS,EAAE,gBAAgB,EAC3B,KAAK,EAAE,MAAM,GACX,cAAc,CAIhB;AAED,qDAAqD;AACrD,eAAO,MAAM,QAAQ,yBAAyB,CAAC;AAC/C,eAAO,MAAM,eAAe,4BAA4B,CAAC;AACzD;;;GAGG;AACH,eAAO,MAAM,aAAa,8BAA8B,CAAC;AAEzD;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,OAAO,EAAE,MAAM,EAAE,cAAc,GAAG,OAAO,CAK7E"}
@@ -0,0 +1,59 @@
1
+ import {
2
+ firstControl2
3
+ } from "../chunks/index-407b6zs0.js";
4
+
5
+ // src/lib/repeatable-group.ts
6
+ var canAddRow = (count, max) => max === undefined || count < max;
7
+ var canRemoveRow = (count, min) => min === undefined || count > min;
8
+ function rowMoveTarget(index, direction, count) {
9
+ const to = direction === "up" ? index - 1 : index + 1;
10
+ return to < 0 || to >= count ? null : to;
11
+ }
12
+ function focusAfterRemove(removed, remaining) {
13
+ if (remaining <= 0)
14
+ return { kind: "add" };
15
+ return { kind: "field", index: Math.min(removed, remaining - 1) };
16
+ }
17
+ function focusAfterMove(to, direction, count) {
18
+ const stays = rowMoveTarget(to, direction, count) !== null;
19
+ const other = direction === "up" ? "down" : "up";
20
+ return { kind: "move", index: to, direction: stays ? direction : other };
21
+ }
22
+ var ROW_SLOT = "repeatable-group-row";
23
+ var ROW_FIELDS_SLOT = "repeatable-group-fields";
24
+ var ADD_ATTRIBUTE = "data-repeatable-group-add";
25
+ function focusRowTarget(root, target) {
26
+ const el = targetElement(root, target);
27
+ if (!el)
28
+ return false;
29
+ el.focus();
30
+ return el.ownerDocument.activeElement === el;
31
+ }
32
+ function targetElement(root, target) {
33
+ if (target.kind === "add") {
34
+ return root.querySelector(`[${ADD_ATTRIBUTE}]`);
35
+ }
36
+ const rows = root.querySelectorAll(`[data-slot="${ROW_SLOT}"]`);
37
+ const row = rows[target.index];
38
+ if (!row)
39
+ return null;
40
+ if (target.kind === "move") {
41
+ return row.querySelector(`[data-move="${target.direction}"]`);
42
+ }
43
+ const fields = row.querySelector(`[data-slot="${ROW_FIELDS_SLOT}"]`);
44
+ return firstControl2([fields ?? row]);
45
+ }
46
+ export {
47
+ ADD_ATTRIBUTE,
48
+ ROW_FIELDS_SLOT,
49
+ ROW_SLOT,
50
+ canAddRow,
51
+ canRemoveRow,
52
+ focusAfterMove,
53
+ focusAfterRemove,
54
+ focusRowTarget,
55
+ rowMoveTarget
56
+ };
57
+
58
+ //# debugId=EF31DAFB676F33B564756E2164756E21
59
+ //# sourceMappingURL=repeatable-group.js.map
@@ -0,0 +1,10 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/lib/repeatable-group.ts"],
4
+ "sourcesContent": [
5
+ "import { firstControl } from './focus-control';\n\n/** Which way a row moves: towards the first row, or towards the last. */\nexport type RowMoveDirection = 'up' | 'down';\n\n/**\n * Where the focus goes once a RepeatableGroup's rows have changed: a row's\n * first field, the Add button, or one of a row's move buttons. Indexes are\n * 0-based, in the rows as they are after the change.\n */\nexport type RowFocusTarget =\n\t| { kind: 'field'; index: number }\n\t| { kind: 'add' }\n\t| { kind: 'move'; index: number; direction: RowMoveDirection };\n\n/**\n * What the group's polite live region says about a change. `index`, `from`\n * and `to` are 1-based: they are what a user reads (\"period 2\").\n */\nexport type RowAnnouncement =\n\t| { kind: 'added'; index: number }\n\t| { kind: 'removed'; index: number }\n\t| { kind: 'moved'; from: number; to: number };\n\n/** Another row can be added: no `max`, or fewer rows than it. */\nexport const canAddRow = (count: number, max?: number): boolean =>\n\tmax === undefined || count < max;\n\n/** A row can be removed: no `min`, or more rows than it. */\nexport const canRemoveRow = (count: number, min?: number): boolean =>\n\tmin === undefined || count > min;\n\n/**\n * The index the row at `index` moves to, or `null` when it cannot move that\n * way (the first row up, the last one down).\n */\nexport function rowMoveTarget(\n\tindex: number,\n\tdirection: RowMoveDirection,\n\tcount: number,\n): number | null {\n\tconst to = direction === 'up' ? index - 1 : index + 1;\n\treturn to < 0 || to >= count ? null : to;\n}\n\n/**\n * Where the focus goes once the row at `removed` is gone and `remaining`\n * rows are left: the row that took its place, else the row before it (the\n * last one was removed), else the Add button (no row is left).\n */\nexport function focusAfterRemove(\n\tremoved: number,\n\tremaining: number,\n): RowFocusTarget {\n\tif (remaining <= 0) return { kind: 'add' };\n\treturn { kind: 'field', index: Math.min(removed, remaining - 1) };\n}\n\n/**\n * Where the focus goes once a row moved `direction` to `to`: the button that\n * moved it, so the user can press it again — unless the row can no longer\n * move that way (it reached the first or the last place, and that button is\n * now disabled), then the other move button of the same row.\n */\nexport function focusAfterMove(\n\tto: number,\n\tdirection: RowMoveDirection,\n\tcount: number,\n): RowFocusTarget {\n\tconst stays = rowMoveTarget(to, direction, count) !== null;\n\tconst other: RowMoveDirection = direction === 'up' ? 'down' : 'up';\n\treturn { kind: 'move', index: to, direction: stays ? direction : other };\n}\n\n/** The `data-slot` of each row and of its fields. */\nexport const ROW_SLOT = 'repeatable-group-row';\nexport const ROW_FIELDS_SLOT = 'repeatable-group-fields';\n/**\n * The attribute that marks the Add button: an attribute of its own, as the\n * button keeps the `data-slot` of the library's Button.\n */\nexport const ADD_ATTRIBUTE = 'data-repeatable-group-add';\n\n/**\n * Moves the focus to `target` inside the group's `root`: the first control a\n * user can reach in the row's fields, the Add button (`data-repeatable-group-add`), or the row's move\n * button (`data-move=\"up\"` / `\"down\"`). Reads the DOM when called; returns\n * whether something took the focus.\n */\nexport function focusRowTarget(root: Element, target: RowFocusTarget): boolean {\n\tconst el = targetElement(root, target);\n\tif (!el) return false;\n\tel.focus();\n\treturn el.ownerDocument.activeElement === el;\n}\n\nfunction targetElement(\n\troot: Element,\n\ttarget: RowFocusTarget,\n): HTMLElement | null {\n\tif (target.kind === 'add') {\n\t\treturn root.querySelector<HTMLElement>(`[${ADD_ATTRIBUTE}]`);\n\t}\n\tconst rows = root.querySelectorAll(`[data-slot=\"${ROW_SLOT}\"]`);\n\tconst row = rows[target.index];\n\tif (!row) return null;\n\tif (target.kind === 'move') {\n\t\treturn row.querySelector<HTMLElement>(`[data-move=\"${target.direction}\"]`);\n\t}\n\tconst fields = row.querySelector(`[data-slot=\"${ROW_FIELDS_SLOT}\"]`);\n\treturn firstControl([fields ?? row]);\n}\n"
6
+ ],
7
+ "mappings": ";;;;;AAyBO,IAAM,YAAY,CAAC,OAAe,QACxC,QAAQ,aAAa,QAAQ;AAGvB,IAAM,eAAe,CAAC,OAAe,QAC3C,QAAQ,aAAa,QAAQ;AAMvB,SAAS,aAAa,CAC5B,OACA,WACA,OACgB;AAAA,EAChB,MAAM,KAAK,cAAc,OAAO,QAAQ,IAAI,QAAQ;AAAA,EACpD,OAAO,KAAK,KAAK,MAAM,QAAQ,OAAO;AAAA;AAQhC,SAAS,gBAAgB,CAC/B,SACA,WACiB;AAAA,EACjB,IAAI,aAAa;AAAA,IAAG,OAAO,EAAE,MAAM,MAAM;AAAA,EACzC,OAAO,EAAE,MAAM,SAAS,OAAO,KAAK,IAAI,SAAS,YAAY,CAAC,EAAE;AAAA;AAS1D,SAAS,cAAc,CAC7B,IACA,WACA,OACiB;AAAA,EACjB,MAAM,QAAQ,cAAc,IAAI,WAAW,KAAK,MAAM;AAAA,EACtD,MAAM,QAA0B,cAAc,OAAO,SAAS;AAAA,EAC9D,OAAO,EAAE,MAAM,QAAQ,OAAO,IAAI,WAAW,QAAQ,YAAY,MAAM;AAAA;AAIjE,IAAM,WAAW;AACjB,IAAM,kBAAkB;AAKxB,IAAM,gBAAgB;AAQtB,SAAS,cAAc,CAAC,MAAe,QAAiC;AAAA,EAC9E,MAAM,KAAK,cAAc,MAAM,MAAM;AAAA,EACrC,IAAI,CAAC;AAAA,IAAI,OAAO;AAAA,EAChB,GAAG,MAAM;AAAA,EACT,OAAO,GAAG,cAAc,kBAAkB;AAAA;AAG3C,SAAS,aAAa,CACrB,MACA,QACqB;AAAA,EACrB,IAAI,OAAO,SAAS,OAAO;AAAA,IAC1B,OAAO,KAAK,cAA2B,IAAI,gBAAgB;AAAA,EAC5D;AAAA,EACA,MAAM,OAAO,KAAK,iBAAiB,eAAe,YAAY;AAAA,EAC9D,MAAM,MAAM,KAAK,OAAO;AAAA,EACxB,IAAI,CAAC;AAAA,IAAK,OAAO;AAAA,EACjB,IAAI,OAAO,SAAS,QAAQ;AAAA,IAC3B,OAAO,IAAI,cAA2B,eAAe,OAAO,aAAa;AAAA,EAC1E;AAAA,EACA,MAAM,SAAS,IAAI,cAAc,eAAe,mBAAmB;AAAA,EACnE,OAAO,cAAa,CAAC,UAAU,GAAG,CAAC;AAAA;",
8
+ "debugId": "EF31DAFB676F33B564756E2164756E21",
9
+ "names": []
10
+ }
@@ -5,6 +5,13 @@
5
5
  export declare const TOASTER_PHONE_QUERY = "(max-width: 600px)";
6
6
  /** Where toasts go on a phone when the caller names no `position`. */
7
7
  export declare const TOASTER_PHONE_POSITION = "top-center";
8
+ /**
9
+ * How far below the top of a phone the toasts start when the caller names no
10
+ * `position`: past the app shell's sticky bar (64px high, 8px from the top),
11
+ * so the menu button, the title and the account control stay reachable while
12
+ * a toast shows. The caller's own `mobileOffset` replaces it.
13
+ */
14
+ export declare const TOASTER_PHONE_OFFSET_TOP = 80;
8
15
  type Listen = Pick<Document, 'addEventListener' | 'removeEventListener'>;
9
16
  /**
10
17
  * Keeps a tap from hovering whatever matches `selector`: while the last
@@ -1 +1 @@
1
- {"version":3,"file":"touch-hover.d.ts","sourceRoot":"","sources":["../../src/lib/touch-hover.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,eAAO,MAAM,mBAAmB,uBAAuB,CAAC;AAExD,sEAAsE;AACtE,eAAO,MAAM,sBAAsB,eAAe,CAAC;AAYnD,KAAK,MAAM,GAAG,IAAI,CAAC,QAAQ,EAAE,kBAAkB,GAAG,qBAAqB,CAAC,CAAC;AAEzE;;;;;;;;;;;;GAYG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,IAAI,CAqB1E"}
1
+ {"version":3,"file":"touch-hover.d.ts","sourceRoot":"","sources":["../../src/lib/touch-hover.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,eAAO,MAAM,mBAAmB,uBAAuB,CAAC;AAExD,sEAAsE;AACtE,eAAO,MAAM,sBAAsB,eAAe,CAAC;AAEnD;;;;;GAKG;AACH,eAAO,MAAM,wBAAwB,KAAK,CAAC;AAY3C,KAAK,MAAM,GAAG,IAAI,CAAC,QAAQ,EAAE,kBAAkB,GAAG,qBAAqB,CAAC,CAAC;AAEzE;;;;;;;;;;;;GAYG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,IAAI,CAqB1E"}
@@ -1,6 +1,7 @@
1
1
  // src/lib/touch-hover.ts
2
2
  var TOASTER_PHONE_QUERY = "(max-width: 600px)";
3
3
  var TOASTER_PHONE_POSITION = "top-center";
4
+ var TOASTER_PHONE_OFFSET_TOP = 80;
4
5
  var HOVER_EVENTS = ["mouseover", "mouseenter", "mousemove"];
5
6
  var POINTER_EVENTS = ["pointerdown", "pointermove", "pointerup"];
6
7
  function ignoreTouchHover(doc, selector) {
@@ -30,10 +31,11 @@ function ignoreTouchHover(doc, selector) {
30
31
  };
31
32
  }
32
33
  export {
34
+ TOASTER_PHONE_OFFSET_TOP,
33
35
  TOASTER_PHONE_POSITION,
34
36
  TOASTER_PHONE_QUERY,
35
37
  ignoreTouchHover
36
38
  };
37
39
 
38
- //# debugId=F1500A469841898064756E2164756E21
40
+ //# debugId=5B9CEF7EA94C41D764756E2164756E21
39
41
  //# sourceMappingURL=touch-hover.js.map
@@ -2,9 +2,9 @@
2
2
  "version": 3,
3
3
  "sources": ["../src/lib/touch-hover.ts"],
4
4
  "sourcesContent": [
5
- "/**\n * A phone, as sonner sees one: under 600px wide it lays the toasts out\n * across the whole width (`@media (max-width: 600px)` in its styles).\n */\nexport const TOASTER_PHONE_QUERY = '(max-width: 600px)';\n\n/** Where toasts go on a phone when the caller names no `position`. */\nexport const TOASTER_PHONE_POSITION = 'top-center';\n\n/**\n * The mouse events a browser emulates after a tap (\"compatibility mouse\n * events\"): a tap reads as a mouse arriving over what was tapped and staying\n * there, with no `mouseleave` until the next tap lands somewhere else.\n */\nconst HOVER_EVENTS = ['mouseover', 'mouseenter', 'mousemove'] as const;\n\n/** The pointer events that say which kind of pointer is in use. */\nconst POINTER_EVENTS = ['pointerdown', 'pointermove', 'pointerup'] as const;\n\ntype Listen = Pick<Document, 'addEventListener' | 'removeEventListener'>;\n\n/**\n * Keeps a tap from hovering whatever matches `selector`: while the last\n * pointer was a finger (`pointerType: 'touch'`), the hover events the browser\n * emulates for it (`mouseover`, `mouseenter`, `mousemove`) stop at `doc`, in\n * the capture phase, before any handler inside the matching element hears\n * them. A mouse or a pen hovers as before: its first `pointermove` ends the\n * touch. `mouseout` and `mouseleave` always pass, so a hover that did start\n * still ends.\n *\n * Toasts pause while hovered (WCAG 2.2.1), and a tap on one used to leave it\n * \"hovered\", so paused, until the next tap elsewhere. Returns the function\n * that removes the listeners; nothing is read before an event arrives.\n */\nexport function ignoreTouchHover(doc: Listen, selector: string): () => void {\n\tlet touch = false;\n\tconst onPointer = (event: Event) => {\n\t\ttouch = (event as PointerEvent).pointerType === 'touch';\n\t};\n\tconst onHover = (event: Event) => {\n\t\tif (!touch) return;\n\t\tconst target = event.target as Partial<Element> | null;\n\t\tif (typeof target?.closest !== 'function') return;\n\t\tif (target.closest(selector)) event.stopPropagation();\n\t};\n\tconst options = { capture: true, passive: true };\n\tfor (const type of POINTER_EVENTS)\n\t\tdoc.addEventListener(type, onPointer, options);\n\tfor (const type of HOVER_EVENTS) doc.addEventListener(type, onHover, options);\n\treturn () => {\n\t\tfor (const type of POINTER_EVENTS)\n\t\t\tdoc.removeEventListener(type, onPointer, options);\n\t\tfor (const type of HOVER_EVENTS)\n\t\t\tdoc.removeEventListener(type, onHover, options);\n\t};\n}\n"
5
+ "/**\n * A phone, as sonner sees one: under 600px wide it lays the toasts out\n * across the whole width (`@media (max-width: 600px)` in its styles).\n */\nexport const TOASTER_PHONE_QUERY = '(max-width: 600px)';\n\n/** Where toasts go on a phone when the caller names no `position`. */\nexport const TOASTER_PHONE_POSITION = 'top-center';\n\n/**\n * How far below the top of a phone the toasts start when the caller names no\n * `position`: past the app shell's sticky bar (64px high, 8px from the top),\n * so the menu button, the title and the account control stay reachable while\n * a toast shows. The caller's own `mobileOffset` replaces it.\n */\nexport const TOASTER_PHONE_OFFSET_TOP = 80;\n\n/**\n * The mouse events a browser emulates after a tap (\"compatibility mouse\n * events\"): a tap reads as a mouse arriving over what was tapped and staying\n * there, with no `mouseleave` until the next tap lands somewhere else.\n */\nconst HOVER_EVENTS = ['mouseover', 'mouseenter', 'mousemove'] as const;\n\n/** The pointer events that say which kind of pointer is in use. */\nconst POINTER_EVENTS = ['pointerdown', 'pointermove', 'pointerup'] as const;\n\ntype Listen = Pick<Document, 'addEventListener' | 'removeEventListener'>;\n\n/**\n * Keeps a tap from hovering whatever matches `selector`: while the last\n * pointer was a finger (`pointerType: 'touch'`), the hover events the browser\n * emulates for it (`mouseover`, `mouseenter`, `mousemove`) stop at `doc`, in\n * the capture phase, before any handler inside the matching element hears\n * them. A mouse or a pen hovers as before: its first `pointermove` ends the\n * touch. `mouseout` and `mouseleave` always pass, so a hover that did start\n * still ends.\n *\n * Toasts pause while hovered (WCAG 2.2.1), and a tap on one used to leave it\n * \"hovered\", so paused, until the next tap elsewhere. Returns the function\n * that removes the listeners; nothing is read before an event arrives.\n */\nexport function ignoreTouchHover(doc: Listen, selector: string): () => void {\n\tlet touch = false;\n\tconst onPointer = (event: Event) => {\n\t\ttouch = (event as PointerEvent).pointerType === 'touch';\n\t};\n\tconst onHover = (event: Event) => {\n\t\tif (!touch) return;\n\t\tconst target = event.target as Partial<Element> | null;\n\t\tif (typeof target?.closest !== 'function') return;\n\t\tif (target.closest(selector)) event.stopPropagation();\n\t};\n\tconst options = { capture: true, passive: true };\n\tfor (const type of POINTER_EVENTS)\n\t\tdoc.addEventListener(type, onPointer, options);\n\tfor (const type of HOVER_EVENTS) doc.addEventListener(type, onHover, options);\n\treturn () => {\n\t\tfor (const type of POINTER_EVENTS)\n\t\t\tdoc.removeEventListener(type, onPointer, options);\n\t\tfor (const type of HOVER_EVENTS)\n\t\t\tdoc.removeEventListener(type, onHover, options);\n\t};\n}\n"
6
6
  ],
7
- "mappings": ";AAIO,IAAM,sBAAsB;AAG5B,IAAM,yBAAyB;AAOtC,IAAM,eAAe,CAAC,aAAa,cAAc,WAAW;AAG5D,IAAM,iBAAiB,CAAC,eAAe,eAAe,WAAW;AAiB1D,SAAS,gBAAgB,CAAC,KAAa,UAA8B;AAAA,EAC3E,IAAI,QAAQ;AAAA,EACZ,MAAM,YAAY,CAAC,UAAiB;AAAA,IACnC,QAAS,MAAuB,gBAAgB;AAAA;AAAA,EAEjD,MAAM,UAAU,CAAC,UAAiB;AAAA,IACjC,IAAI,CAAC;AAAA,MAAO;AAAA,IACZ,MAAM,SAAS,MAAM;AAAA,IACrB,IAAI,OAAO,QAAQ,YAAY;AAAA,MAAY;AAAA,IAC3C,IAAI,OAAO,QAAQ,QAAQ;AAAA,MAAG,MAAM,gBAAgB;AAAA;AAAA,EAErD,MAAM,UAAU,EAAE,SAAS,MAAM,SAAS,KAAK;AAAA,EAC/C,WAAW,QAAQ;AAAA,IAClB,IAAI,iBAAiB,MAAM,WAAW,OAAO;AAAA,EAC9C,WAAW,QAAQ;AAAA,IAAc,IAAI,iBAAiB,MAAM,SAAS,OAAO;AAAA,EAC5E,OAAO,MAAM;AAAA,IACZ,WAAW,QAAQ;AAAA,MAClB,IAAI,oBAAoB,MAAM,WAAW,OAAO;AAAA,IACjD,WAAW,QAAQ;AAAA,MAClB,IAAI,oBAAoB,MAAM,SAAS,OAAO;AAAA;AAAA;",
8
- "debugId": "F1500A469841898064756E2164756E21",
7
+ "mappings": ";AAIO,IAAM,sBAAsB;AAG5B,IAAM,yBAAyB;AAQ/B,IAAM,2BAA2B;AAOxC,IAAM,eAAe,CAAC,aAAa,cAAc,WAAW;AAG5D,IAAM,iBAAiB,CAAC,eAAe,eAAe,WAAW;AAiB1D,SAAS,gBAAgB,CAAC,KAAa,UAA8B;AAAA,EAC3E,IAAI,QAAQ;AAAA,EACZ,MAAM,YAAY,CAAC,UAAiB;AAAA,IACnC,QAAS,MAAuB,gBAAgB;AAAA;AAAA,EAEjD,MAAM,UAAU,CAAC,UAAiB;AAAA,IACjC,IAAI,CAAC;AAAA,MAAO;AAAA,IACZ,MAAM,SAAS,MAAM;AAAA,IACrB,IAAI,OAAO,QAAQ,YAAY;AAAA,MAAY;AAAA,IAC3C,IAAI,OAAO,QAAQ,QAAQ;AAAA,MAAG,MAAM,gBAAgB;AAAA;AAAA,EAErD,MAAM,UAAU,EAAE,SAAS,MAAM,SAAS,KAAK;AAAA,EAC/C,WAAW,QAAQ;AAAA,IAClB,IAAI,iBAAiB,MAAM,WAAW,OAAO;AAAA,EAC9C,WAAW,QAAQ;AAAA,IAAc,IAAI,iBAAiB,MAAM,SAAS,OAAO;AAAA,EAC5E,OAAO,MAAM;AAAA,IACZ,WAAW,QAAQ;AAAA,MAClB,IAAI,oBAAoB,MAAM,WAAW,OAAO;AAAA,IACjD,WAAW,QAAQ;AAAA,MAClB,IAAI,oBAAoB,MAAM,SAAS,OAAO;AAAA;AAAA;",
8
+ "debugId": "5B9CEF7EA94C41D764756E2164756E21",
9
9
  "names": []
10
10
  }
package/docs/roadmap.md CHANGED
@@ -76,6 +76,10 @@ Nothing is in progress.
76
76
 
77
77
  ## Shipped
78
78
 
79
+ - **A repeatable row group's focus rules** (`/repeatable-group`) — where the
80
+ focus goes after a row is added, removed or moved, the add and remove
81
+ limits, and what the live region announces, behind both libraries'
82
+ `RepeatableGroup`. 0.11.0.
79
83
  - **Two-step verification helpers and printing one element** (`/two-factor`,
80
84
  `/print-only`) — a TOTP secret grouped to be typed by hand, the recovery
81
85
  codes as a text file, saving a text file, and printing one element of the
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@nxgt/material-core",
3
- "version": "0.10.0",
4
- "description": "The framework-free core of @nxgt/material and @nxgt/material-vue: the PDF document model and its react-pdf renderer, print formatters, PDF file helpers, the PDF viewer's pure modules, the currency digits, the file-size formatter, the field-blur layers, a field's first focusable control, the aria-describedby joiner, the inert background of a modal layer, Escape forwarded while an overlay closes, the Toaster's touch-hover guard and its focus hand-back, the page's Content Security Policy nonce, an object built on first use, the zod issue messages, the date range presets, the debounce, accent-insensitive text matching and the race-free async search behind Autocomplete and SearchField, the submit guard, the code error map, the two-factor helpers (a grouped TOTP secret, the recovery codes' file, a text download), printing one element of the page, the file preview's kind detection and text reader, the pdfkit fonts Vite plugin, a CSP-safe Vite assetsInlineLimit and the Node-only pdfkit fonts resolution for Next.js and Nuxt builds",
3
+ "version": "0.11.1",
4
+ "description": "The framework-free core of @nxgt/material and @nxgt/material-vue: the PDF document model and its react-pdf renderer, print formatters, PDF file helpers, the PDF viewer's pure modules, the currency digits, the file-size formatter, the field-blur layers, a field's first focusable control, a repeatable row group's focus and announcement rules, the aria-describedby joiner, the inert background of a modal layer, Escape forwarded while an overlay closes, the Toaster's touch-hover guard and its focus hand-back, the page's Content Security Policy nonce, an object built on first use, the zod issue messages, the date range presets, the debounce, accent-insensitive text matching and the race-free async search behind Autocomplete and SearchField, the submit guard, the code error map, the two-factor helpers (a grouped TOTP secret, the recovery codes' file, a text download), printing one element of the page, the file preview's kind detection and text reader, the pdfkit fonts Vite plugin, a CSP-safe Vite assetsInlineLimit and the Node-only pdfkit fonts resolution for Next.js and Nuxt builds",
5
5
  "license": "MIT",
6
6
  "bugs": {
7
7
  "email": "stevegires@gmail.com"
@@ -103,6 +103,11 @@
103
103
  "import": "./dist/lib/focus-control.js",
104
104
  "default": "./dist/lib/focus-control.js"
105
105
  },
106
+ "./repeatable-group": {
107
+ "types": "./dist/lib/repeatable-group.d.ts",
108
+ "import": "./dist/lib/repeatable-group.js",
109
+ "default": "./dist/lib/repeatable-group.js"
110
+ },
106
111
  "./escape-forward": {
107
112
  "types": "./dist/lib/escape-forward.d.ts",
108
113
  "import": "./dist/lib/escape-forward.js",
@@ -268,6 +273,7 @@
268
273
  "src/lib/currency-digits.ts",
269
274
  "src/lib/field-blur-layers.ts",
270
275
  "src/lib/focus-control.ts",
276
+ "src/lib/repeatable-group.ts",
271
277
  "src/lib/escape-forward.ts",
272
278
  "src/lib/described-by.ts",
273
279
  "src/lib/touch-hover.ts",