@ionic/core 9.0.9 → 9.0.10-dev.11793383958.19a7d413

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.
Files changed (43) hide show
  1. package/components/ion-input-otp.js +1 -1
  2. package/components/ion-menu.js +1 -1
  3. package/components/ion-router.js +1 -1
  4. package/dist/cjs/ion-input-otp.cjs.entry.js +126 -10
  5. package/dist/cjs/ion-menu_3.cjs.entry.js +25 -7
  6. package/dist/cjs/ion-route_4.cjs.entry.js +121 -16
  7. package/dist/cjs/ionic.cjs.js +1 -1
  8. package/dist/cjs/loader.cjs.js +1 -1
  9. package/dist/collection/components/input-otp/input-otp.ios.css +30 -5
  10. package/dist/collection/components/input-otp/input-otp.js +191 -12
  11. package/dist/collection/components/input-otp/input-otp.md.css +30 -5
  12. package/dist/collection/components/menu/menu.js +25 -8
  13. package/dist/collection/components/route/route.js +9 -1
  14. package/dist/collection/components/route-redirect/route-redirect.js +1 -1
  15. package/dist/collection/components/router/utils/constants.js +2 -0
  16. package/dist/collection/components/router/utils/matching.js +97 -14
  17. package/dist/collection/components/router/utils/path.js +17 -3
  18. package/dist/docs.json +92 -3
  19. package/dist/esm/ion-input-otp.entry.js +128 -12
  20. package/dist/esm/ion-menu_3.entry.js +26 -8
  21. package/dist/esm/ion-route_4.entry.js +121 -16
  22. package/dist/esm/ionic.js +1 -1
  23. package/dist/esm/loader.js +1 -1
  24. package/dist/html.html-data.json +10 -2
  25. package/dist/ionic/ionic.esm.js +1 -1
  26. package/dist/ionic/p-095da8ec.entry.js +4 -0
  27. package/dist/ionic/p-705759fd.entry.js +4 -0
  28. package/dist/ionic/p-99c518de.entry.js +4 -0
  29. package/dist/types/components/input-otp/input-otp-interface.d.ts +1 -0
  30. package/dist/types/components/input-otp/input-otp.d.ts +40 -2
  31. package/dist/types/components/menu/menu.d.ts +7 -0
  32. package/dist/types/components/route/route.d.ts +8 -0
  33. package/dist/types/components/route-redirect/route-redirect.d.ts +3 -1
  34. package/dist/types/components/router/utils/constants.d.ts +2 -0
  35. package/dist/types/components/router/utils/matching.d.ts +10 -0
  36. package/dist/types/components/router/utils/path.d.ts +5 -1
  37. package/dist/types/components.d.ts +32 -6
  38. package/hydrate/index.js +257 -33
  39. package/hydrate/index.mjs +257 -33
  40. package/package.json +1 -1
  41. package/dist/ionic/p-539d6c8e.entry.js +0 -4
  42. package/dist/ionic/p-7ce9c6f8.entry.js +0 -4
  43. package/dist/ionic/p-af11d3c6.entry.js +0 -4
@@ -111,6 +111,7 @@
111
111
  * different selectors for different fill variants.
112
112
  */
113
113
  --highlight-color: var(--highlight-color-focused);
114
+ --border-color: initial;
114
115
  display: block;
115
116
  position: relative;
116
117
  font-size: 0.875rem;
@@ -148,7 +149,7 @@
148
149
  height: var(--height);
149
150
  border-width: var(--border-width);
150
151
  border-style: solid;
151
- border-color: var(--border-color);
152
+ border-color: var(--border-color, var(--internal-border-color));
152
153
  background: var(--background);
153
154
  color: var(--color);
154
155
  font-size: inherit;
@@ -171,6 +172,30 @@
171
172
  display: none;
172
173
  }
173
174
 
175
+ .input-otp-bottom {
176
+ font-size: 0.75rem;
177
+ line-height: 1.25rem;
178
+ text-align: center;
179
+ }
180
+
181
+ .input-otp-bottom .error-text {
182
+ display: none;
183
+ color: var(--highlight-color-invalid);
184
+ }
185
+
186
+ .input-otp-bottom .helper-text {
187
+ display: block;
188
+ color: var(--ion-color-step-700, var(--ion-text-color-step-300, #4d4d4d));
189
+ }
190
+
191
+ :host(.ion-touched.ion-invalid) .input-otp-bottom .error-text {
192
+ display: block;
193
+ }
194
+
195
+ :host(.ion-touched.ion-invalid) .input-otp-bottom .helper-text {
196
+ display: none;
197
+ }
198
+
174
199
  .input-otp-separator {
175
200
  border-radius: var(--separator-border-radius);
176
201
  flex-shrink: 0;
@@ -220,7 +245,7 @@
220
245
  }
221
246
 
222
247
  :host(.input-otp-fill-solid) {
223
- --border-color: var(--ion-color-step-50, var(--ion-background-color-step-50, #f2f2f2));
248
+ --internal-border-color: var(--ion-color-step-50, var(--ion-background-color-step-50, #f2f2f2));
224
249
  --background: var(--ion-color-step-50, var(--ion-background-color-step-50, #f2f2f2));
225
250
  }
226
251
 
@@ -230,7 +255,7 @@
230
255
 
231
256
  :host(.input-otp-fill-outline.input-otp-disabled) {
232
257
  --background: var(--ion-color-step-50, var(--ion-background-color-step-50, #f2f2f2));
233
- --border-color: var(--ion-color-step-100, var(--ion-background-color-step-100, #e6e6e6));
258
+ --internal-border-color: var(--ion-color-step-100, var(--ion-background-color-step-100, #e6e6e6));
234
259
  }
235
260
 
236
261
  :host(.input-otp-disabled),
@@ -249,7 +274,7 @@
249
274
 
250
275
  :host(.input-otp-fill-solid.input-otp-disabled),
251
276
  :host(.input-otp-fill-solid.input-otp-readonly) {
252
- --border-color: var(--ion-color-step-100, var(--ion-background-color-step-100, #e6e6e6));
277
+ --internal-border-color: var(--ion-color-step-100, var(--ion-background-color-step-100, #e6e6e6));
253
278
  --background: var(--ion-color-step-100, var(--ion-background-color-step-100, #e6e6e6));
254
279
  }
255
280
 
@@ -377,5 +402,5 @@
377
402
  }
378
403
 
379
404
  :host(.input-otp-fill-outline) {
380
- --border-color: var(--ion-color-step-300, var(--ion-background-color-step-300, #b3b3b3));
405
+ --internal-border-color: var(--ion-color-step-300, var(--ion-background-color-step-300, #b3b3b3));
381
406
  }
@@ -11,7 +11,6 @@ import { printIonError } from "../../utils/logging/index";
11
11
  import { menuController } from "../../utils/menu-controller/index";
12
12
  import { BACKDROP, GESTURE, getPresentedOverlay } from "../../utils/overlays";
13
13
  import { isPlatform } from "../../utils/platform";
14
- import { hostContext } from "../../utils/theme";
15
14
  import { config } from "../../global/config";
16
15
  import { getIonMode } from "../../global/ionic-global";
17
16
  const iosEasing = 'cubic-bezier(0.32,0.72,0,1)';
@@ -169,7 +168,7 @@ export class Menu {
169
168
  * of the split pane. If the split pane is
170
169
  * hidden then the menu should be too.
171
170
  */
172
- const splitPane = this.el.closest('ion-split-pane');
171
+ const splitPane = this.getSplitPane();
173
172
  if (splitPane !== null) {
174
173
  this.isPaneVisible = await splitPane.isVisible();
175
174
  }
@@ -208,8 +207,8 @@ export class Menu {
208
207
  this.contentEl = undefined;
209
208
  }
210
209
  onSplitPaneChanged(ev) {
211
- const closestSplitPane = this.el.closest('ion-split-pane');
212
- if (closestSplitPane !== null && closestSplitPane === ev.target) {
210
+ const splitPane = this.getSplitPane();
211
+ if (splitPane !== null && splitPane === ev.target) {
213
212
  this.isPaneVisible = ev.detail.visible;
214
213
  this.updateState();
215
214
  }
@@ -407,6 +406,24 @@ export class Menu {
407
406
  _isActive() {
408
407
  return !this.disabled && !this.isPaneVisible;
409
408
  }
409
+ /**
410
+ * Returns the split pane this menu is a side pane of.
411
+ * A menu inside the split pane's main content, such as one
412
+ * in a page, is a regular menu. The menu doesn't need to be
413
+ * a direct child, since it can be wrapped in a component.
414
+ */
415
+ getSplitPane() {
416
+ const splitPane = this.el.closest('ion-split-pane');
417
+ const main = this.el.closest('.split-pane-main');
418
+ /**
419
+ * A split pane can be the main content of an outer split
420
+ * pane, so only this split pane's main content counts.
421
+ */
422
+ if (splitPane === null || (main !== null && main !== splitPane && splitPane.contains(main))) {
423
+ return null;
424
+ }
425
+ return splitPane;
426
+ }
410
427
  canSwipe() {
411
428
  return this.swipeGesture && !this.isAnimating && this._isActive();
412
429
  }
@@ -650,21 +667,21 @@ export class Menu {
650
667
  }
651
668
  }
652
669
  render() {
653
- const { type, disabled, el, isPaneVisible, inheritedAttributes, side } = this;
670
+ const { type, disabled, isPaneVisible, inheritedAttributes, side } = this;
654
671
  const mode = getIonMode(this);
655
672
  /**
656
673
  * If the Close Watcher is enabled then
657
674
  * the ionBackButton listener in the menu controller
658
675
  * will handle closing the menu when Escape is pressed.
659
676
  */
660
- return (h(Host, { key: '5cf26df26d4e539860cbeaea98ec621b98c3fcfc', onKeyDown: shouldUseCloseWatcher() ? null : this.onKeydown, role: "navigation", "aria-label": inheritedAttributes['aria-label'] || 'menu', class: {
677
+ return (h(Host, { key: '7481c8a32a135fb0fd6d5591eecbf3fb0a8e8dde', onKeyDown: shouldUseCloseWatcher() ? null : this.onKeydown, role: "navigation", "aria-label": inheritedAttributes['aria-label'] || 'menu', class: {
661
678
  [mode]: true,
662
679
  [`menu-type-${type}`]: true,
663
680
  'menu-enabled': !disabled,
664
681
  [`menu-side-${side}`]: true,
665
682
  'menu-pane-visible': isPaneVisible,
666
- 'split-pane-side': hostContext('ion-split-pane', el),
667
- } }, h("div", { key: 'f337016e293bb8665c99762e0c389dd6c372bb33', class: "menu-inner", part: "container", ref: (el) => (this.menuInnerEl = el) }, h("slot", { key: '4c3fe331aa42cd57d7ad9bd1f5910f8eeb62d7da' })), h("ion-backdrop", { key: '61af7ea876d81574841e1dd3e81e2352e555d096', ref: (el) => (this.backdropEl = el), class: "menu-backdrop", tappable: false, stopPropagation: false, part: "backdrop" })));
683
+ 'split-pane-side': this.getSplitPane() !== null,
684
+ } }, h("div", { key: '8ac2f0f531db944a46e5fef016d0b18bd7b804cc', class: "menu-inner", part: "container", ref: (el) => (this.menuInnerEl = el) }, h("slot", { key: '1ac3e13ccccf1adb7915b739fc794f2889b626ce' })), h("ion-backdrop", { key: 'f8becbf4de1a13feb57fb87973befecb88510bab', ref: (el) => (this.backdropEl = el), class: "menu-backdrop", tappable: false, stopPropagation: false, part: "backdrop" })));
668
685
  }
669
686
  static get is() { return "ion-menu"; }
670
687
  static get encapsulation() { return "shadow"; }
@@ -6,6 +6,14 @@ export class Route {
6
6
  *
7
7
  * Accepts paths similar to expressjs so that you can define parameters
8
8
  * in the url /foo/:bar where bar would be available in incoming props.
9
+ *
10
+ * A url ending in `*`, such as `*` or `/admin/*`, matches any remaining path,
11
+ * including none, so `/admin/*` also matches `/admin`. It only wins over a
12
+ * route that matches the whole path when it has more fixed segments, so
13
+ * `/admin/*` wins over `/:section/:id` but not over `/admin/:id`. It always
14
+ * wins over a route that matches by ignoring trailing segments, such as
15
+ * `/admin` for `/admin/users`. The matched path is passed in the `pathMatch`
16
+ * prop without a leading `/`. A `*` on a route with child routes is ignored.
9
17
  */
10
18
  this.url = '';
11
19
  }
@@ -47,7 +55,7 @@ export class Route {
47
55
  "optional": false,
48
56
  "docs": {
49
57
  "tags": [],
50
- "text": "Relative path that needs to match in order for this route to apply.\n\nAccepts paths similar to expressjs so that you can define parameters\nin the url /foo/:bar where bar would be available in incoming props."
58
+ "text": "Relative path that needs to match in order for this route to apply.\n\nAccepts paths similar to expressjs so that you can define parameters\nin the url /foo/:bar where bar would be available in incoming props.\n\nA url ending in `*`, such as `*` or `/admin/*`, matches any remaining path,\nincluding none, so `/admin/*` also matches `/admin`. It only wins over a\nroute that matches the whole path when it has more fixed segments, so\n`/admin/*` wins over `/:section/:id` but not over `/admin/:id`. It always\nwins over a route that matches by ignoring trailing segments, such as\n`/admin` for `/admin/users`. The matched path is passed in the `pathMatch`\nprop without a leading `/`. A `*` on a route with child routes is ignored."
51
59
  },
52
60
  "getter": false,
53
61
  "setter": false,
@@ -20,7 +20,7 @@ export class RouteRedirect {
20
20
  "optional": false,
21
21
  "docs": {
22
22
  "tags": [],
23
- "text": "A redirect route, redirects \"from\" a URL \"to\" another URL. This property is that \"from\" URL.\nIt needs to be an exact match of the navigated URL in order to apply.\n\nThe path specified in this value is always an absolute path, even if the initial `/` slash\nis not specified."
23
+ "text": "A redirect route, redirects \"from\" a URL \"to\" another URL. This property is that \"from\" URL.\nIt needs to be an exact match of the navigated URL in order to apply, unless it contains a `*`.\nA `*` matches the rest of the path from that segment on, as long as the path has at least as\nmany segments as `from`, so `/admin/*` matches `/admin/posts` but not `/admin`.\n\nThe path specified in this value is always an absolute path, even if the initial `/` slash\nis not specified."
24
24
  },
25
25
  "getter": false,
26
26
  "setter": false,
@@ -4,3 +4,5 @@
4
4
  export const ROUTER_INTENT_NONE = 'root';
5
5
  export const ROUTER_INTENT_FORWARD = 'forward';
6
6
  export const ROUTER_INTENT_BACK = 'back';
7
+ /** Param that holds the path matched by a trailing `*` in an `ion-route` url. */
8
+ export const WILDCARD_PARAM = 'pathMatch';
@@ -1,6 +1,8 @@
1
1
  /*!
2
2
  * (C) Ionic http://ionicframework.com - MIT License
3
3
  */
4
+ import { WILDCARD_PARAM } from "./constants";
5
+ import { chainToSegments, endsWithWildcard, withoutWildcard } from "./path";
4
6
  /**
5
7
  * Returns whether the given redirect matches the given path segments.
6
8
  *
@@ -41,6 +43,10 @@ export const matchesIDs = (ids, chain) => {
41
43
  if (routeId.id.toLowerCase() !== routeChain.id) {
42
44
  break;
43
45
  }
46
+ // Weight a wildcard route when the outlet reports its `pathMatch`
47
+ if (routeId.params?.[WILDCARD_PARAM] !== undefined && endsWithWildcard(routeChain.segments)) {
48
+ score++;
49
+ }
44
50
  if (routeId.params) {
45
51
  const routeIdParams = Object.keys(routeId.params);
46
52
  // Only compare routes with the chain that have the same number of parameters.
@@ -96,21 +102,28 @@ export const matchesSegments = (segments, chain) => {
96
102
  matchesDefault = true;
97
103
  }
98
104
  else {
99
- for (const segment of chainSegments) {
105
+ const params = {};
106
+ for (const segment of withoutWildcard(chainSegments)) {
100
107
  const data = inputSegments.next();
101
108
  // data param
102
109
  if (segment[0] === ':') {
103
110
  if (data === '') {
104
111
  return null;
105
112
  }
106
- allparams = allparams || [];
107
- const params = allparams[i] || (allparams[i] = {});
108
113
  params[segment.slice(1)] = data;
109
114
  }
110
115
  else if (data !== segment) {
111
116
  return null;
112
117
  }
113
118
  }
119
+ // A trailing `*` on a parent route has no effect. Its children match the rest of the path.
120
+ if (i === chain.length - 1 && endsWithWildcard(chainSegments)) {
121
+ params[WILDCARD_PARAM] = inputSegments.rest().join('/');
122
+ }
123
+ if (Object.keys(params).length > 0) {
124
+ allparams = allparams || [];
125
+ allparams[i] = params;
126
+ }
114
127
  matchesDefault = false;
115
128
  }
116
129
  }
@@ -142,25 +155,38 @@ export const mergeParams = (a, b) => {
142
155
  * Returns the best match or null when no match is found.
143
156
  * When a chain is returned the parameters are updated from the RouteIDs.
144
157
  * That is they contain both the componentProps of the <ion-route> and the parameter segment.
158
+ *
159
+ * When a wildcard route ties with other routes, a route without a wildcard that can build a url wins,
160
+ * then the wildcard route. Otherwise the first route declared wins a tie.
145
161
  */
146
162
  export const findChainForIDs = (ids, chains) => {
147
- let match = null;
148
163
  let maxMatches = 0;
164
+ let tied = [];
149
165
  for (const chain of chains) {
150
166
  const score = matchesIDs(ids, chain);
151
167
  if (score > maxMatches) {
152
- match = chain;
153
168
  maxMatches = score;
169
+ tied = [chain];
170
+ }
171
+ else if (score > 0 && score === maxMatches) {
172
+ tied.push(chain);
154
173
  }
155
174
  }
156
- if (match) {
157
- return match.map((route, i) => ({
158
- id: route.id,
159
- segments: route.segments,
160
- params: mergeParams(route.params, ids[i]?.params),
161
- }));
175
+ const matches = tied.map((chain) => chain.map((route, i) => ({
176
+ id: route.id,
177
+ segments: route.segments,
178
+ params: mergeParams(route.params, ids[i]?.params),
179
+ })));
180
+ if (!matches.some(chainEndsWithWildcard)) {
181
+ return matches.length > 0 ? matches[0] : null;
182
+ }
183
+ return matches.reduce((best, chain) => (tieRank(chain) > tieRank(best) ? chain : best));
184
+ };
185
+ const tieRank = (chain) => {
186
+ if (chainEndsWithWildcard(chain)) {
187
+ return 1;
162
188
  }
163
- return null;
189
+ return chainToSegments(chain) !== null ? 2 : 0;
164
190
  };
165
191
  /**
166
192
  * Finds the best match for the segments in the chains.
@@ -168,10 +194,17 @@ export const findChainForIDs = (ids, chains) => {
168
194
  * Returns the best match or null when no match is found.
169
195
  * When a chain is returned the parameters are updated from the segments.
170
196
  * That is they contain both the componentProps of the <ion-route> and the parameter segments.
197
+ *
198
+ * When wildcard routes match, the one with the most fixed segments wins if it has more fixed segments than every
199
+ * route that matched the whole path. Routes that only matched by ignoring trailing segments don't count.
171
200
  */
172
201
  export const findChainForSegments = (segments, chains) => {
173
202
  let match = null;
174
203
  let bestScore = 0;
204
+ let fullMatch = null;
205
+ let bestFullScore = 0;
206
+ let maxFullFixed = -1;
207
+ let wildcard = null;
175
208
  for (const chain of chains) {
176
209
  const matchedChain = matchesSegments(segments, chain);
177
210
  if (matchedChain !== null) {
@@ -180,9 +213,51 @@ export const findChainForSegments = (segments, chains) => {
180
213
  bestScore = score;
181
214
  match = matchedChain;
182
215
  }
216
+ if (chainEndsWithWildcard(matchedChain)) {
217
+ if (wildcard === null || winsWildcard(matchedChain, wildcard)) {
218
+ wildcard = matchedChain;
219
+ }
220
+ }
221
+ else if (!isPartialMatch(segments, matchedChain)) {
222
+ if (score > bestFullScore) {
223
+ bestFullScore = score;
224
+ fullMatch = matchedChain;
225
+ }
226
+ maxFullFixed = Math.max(maxFullFixed, countFixedSegments(matchedChain));
227
+ }
183
228
  }
184
229
  }
185
- return match;
230
+ // Without a wildcard to match the rest of the path, routes can still ignore trailing segments.
231
+ if (wildcard === null) {
232
+ return match;
233
+ }
234
+ // Compare fixed segments rather than priority, since nesting under default routes raises the priority
235
+ return countFixedSegments(wildcard) > maxFullFixed ? wildcard : fullMatch;
236
+ };
237
+ /** Returns whether the chain matched the path segments only by ignoring some trailing segments. */
238
+ const isPartialMatch = (segments, chain) => {
239
+ const matched = chainToSegments(chain);
240
+ return matched !== null && matched.length < segments.filter((s) => s !== '').length;
241
+ };
242
+ const chainEndsWithWildcard = (chain) => {
243
+ return endsWithWildcard(chain[chain.length - 1].segments);
244
+ };
245
+ /** Returns whether the wildcard chain beats `best` by fixed segments, then by priority. */
246
+ const winsWildcard = (chain, best) => {
247
+ const chainFixed = countFixedSegments(chain);
248
+ const bestFixed = countFixedSegments(best);
249
+ return chainFixed > bestFixed || (chainFixed === bestFixed && computePriority(chain) > computePriority(best));
250
+ };
251
+ const countFixedSegments = (chain) => {
252
+ let count = 0;
253
+ for (const route of chain) {
254
+ for (const segment of withoutWildcard(route.segments)) {
255
+ if (segment !== '' && segment[0] !== ':') {
256
+ count++;
257
+ }
258
+ }
259
+ }
260
+ return count;
186
261
  };
187
262
  /**
188
263
  * Computes the priority of a chain.
@@ -194,12 +269,14 @@ export const findChainForSegments = (segments, chains) => {
194
269
  * - /path/to/page
195
270
  *
196
271
  * The second one will be given a higher priority because "page" is a fixed segment (vs ":where", a parameter segment).
272
+ *
273
+ * A trailing wildcard is not counted.
197
274
  */
198
275
  export const computePriority = (chain) => {
199
276
  let score = 1;
200
277
  let level = 1;
201
278
  for (const route of chain) {
202
- for (const segment of route.segments) {
279
+ for (const segment of withoutWildcard(route.segments)) {
203
280
  if (segment[0] === ':') {
204
281
  score += Math.pow(1, level);
205
282
  }
@@ -221,4 +298,10 @@ export class RouterSegments {
221
298
  }
222
299
  return '';
223
300
  }
301
+ /** Consumes and returns the remaining non empty segments. */
302
+ rest() {
303
+ const rest = this.segments.filter((s) => s !== '');
304
+ this.segments = [];
305
+ return rest;
306
+ }
224
307
  }
@@ -1,7 +1,7 @@
1
1
  /*!
2
2
  * (C) Ionic http://ionicframework.com - MIT License
3
3
  */
4
- import { ROUTER_INTENT_FORWARD } from "./constants";
4
+ import { ROUTER_INTENT_FORWARD, WILDCARD_PARAM } from "./constants";
5
5
  /** Join the non empty segments with "/". */
6
6
  export const generatePath = (segments) => {
7
7
  const path = segments.filter((s) => s.length > 0).join('/');
@@ -34,12 +34,13 @@ export const writeSegments = (history, root, useHash, segments, direction, state
34
34
  *
35
35
  * Notes:
36
36
  * - parameter segments of the form :param are replaced with their value,
37
- * - null is returned when a value is missing for any parameter segment.
37
+ * - null is returned when a value is missing for any parameter segment,
38
+ * - a trailing wildcard is replaced with the path it matched.
38
39
  */
39
40
  export const chainToSegments = (chain) => {
40
41
  const segments = [];
41
42
  for (const route of chain) {
42
- for (const segment of route.segments) {
43
+ for (const segment of withoutWildcard(route.segments)) {
43
44
  if (segment[0] === ':') {
44
45
  const param = route.params && route.params[segment.slice(1)];
45
46
  if (!param) {
@@ -51,9 +52,22 @@ export const chainToSegments = (chain) => {
51
52
  segments.push(segment);
52
53
  }
53
54
  }
55
+ segments.push(...wildcardSegments(route));
54
56
  }
55
57
  return segments;
56
58
  };
59
+ /** Only a `*` in the last segment of a route url is a wildcard. Elsewhere it's matched literally. */
60
+ export const endsWithWildcard = (segments) => {
61
+ return segments[segments.length - 1] === '*';
62
+ };
63
+ /** Returns the path segments matched by the trailing wildcard of the route. */
64
+ const wildcardSegments = (route) => {
65
+ const pathMatch = endsWithWildcard(route.segments) ? route.params?.[WILDCARD_PARAM] : undefined;
66
+ return pathMatch ? pathMatch.split('/').filter((s) => s.length > 0) : [];
67
+ };
68
+ export const withoutWildcard = (segments) => {
69
+ return endsWithWildcard(segments) ? segments.slice(0, -1) : segments;
70
+ };
57
71
  /**
58
72
  * Removes the prefix segments from the path segments.
59
73
  *
package/dist/docs.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "timestamp": "2026-10-21T18:13:17",
2
+ "timestamp": "2026-10-30T18:14:29",
3
3
  "compiler": {
4
4
  "name": "@stencil/core",
5
5
  "version": "4.45.2",
@@ -14038,6 +14038,32 @@
14038
14038
  "getter": false,
14039
14039
  "setter": false
14040
14040
  },
14041
+ {
14042
+ "name": "errorText",
14043
+ "type": "string | undefined",
14044
+ "complexType": {
14045
+ "original": "string",
14046
+ "resolved": "string | undefined",
14047
+ "references": {}
14048
+ },
14049
+ "mutable": false,
14050
+ "attr": "error-text",
14051
+ "reflectToAttr": false,
14052
+ "docs": "Text that is placed under the input boxes and displayed when an error is detected.",
14053
+ "docsTags": [],
14054
+ "values": [
14055
+ {
14056
+ "type": "string"
14057
+ },
14058
+ {
14059
+ "type": "undefined"
14060
+ }
14061
+ ],
14062
+ "optional": true,
14063
+ "required": false,
14064
+ "getter": false,
14065
+ "setter": false
14066
+ },
14041
14067
  {
14042
14068
  "name": "fill",
14043
14069
  "type": "\"outline\" | \"solid\" | undefined",
@@ -14075,6 +14101,64 @@
14075
14101
  "getter": false,
14076
14102
  "setter": false
14077
14103
  },
14104
+ {
14105
+ "name": "helperText",
14106
+ "type": "string | undefined",
14107
+ "complexType": {
14108
+ "original": "string",
14109
+ "resolved": "string | undefined",
14110
+ "references": {}
14111
+ },
14112
+ "mutable": false,
14113
+ "attr": "helper-text",
14114
+ "reflectToAttr": false,
14115
+ "docs": "Text that is placed under the input boxes and displayed when no error is detected.",
14116
+ "docsTags": [],
14117
+ "values": [
14118
+ {
14119
+ "type": "string"
14120
+ },
14121
+ {
14122
+ "type": "undefined"
14123
+ }
14124
+ ],
14125
+ "optional": true,
14126
+ "required": false,
14127
+ "getter": false,
14128
+ "setter": false
14129
+ },
14130
+ {
14131
+ "name": "inputLabelFormatter",
14132
+ "type": "((index: number, length: number) => string) | undefined",
14133
+ "complexType": {
14134
+ "original": "InputOtpLabelFormatter",
14135
+ "resolved": "((index: number, length: number) => string) | undefined",
14136
+ "references": {
14137
+ "InputOtpLabelFormatter": {
14138
+ "location": "import",
14139
+ "path": "./input-otp-interface",
14140
+ "id": "src/components/input-otp/input-otp-interface.ts::InputOtpLabelFormatter",
14141
+ "referenceLocation": "InputOtpLabelFormatter"
14142
+ }
14143
+ }
14144
+ },
14145
+ "mutable": false,
14146
+ "reflectToAttr": false,
14147
+ "docs": "A callback used to format the accessible name of each input box. It\nreceives the 0-based `index` of the box and the total `length`. By default\nthe name is \"Input 1 of 4\", or \"PIN, 1 of 4\" with an `aria-label` of \"PIN\".\n\nSee https://ionicframework.com/docs/troubleshooting/runtime#accessing-this\nif you need to access `this` from within the callback.",
14148
+ "docsTags": [],
14149
+ "values": [
14150
+ {
14151
+ "type": "((index: number, length: number) => string)"
14152
+ },
14153
+ {
14154
+ "type": "undefined"
14155
+ }
14156
+ ],
14157
+ "optional": true,
14158
+ "required": false,
14159
+ "getter": false,
14160
+ "setter": false
14161
+ },
14078
14162
  {
14079
14163
  "name": "inputmode",
14080
14164
  "type": "\"decimal\" | \"email\" | \"none\" | \"numeric\" | \"search\" | \"tel\" | \"text\" | \"url\" | undefined",
@@ -27325,7 +27409,7 @@
27325
27409
  "mutable": false,
27326
27410
  "attr": "url",
27327
27411
  "reflectToAttr": false,
27328
- "docs": "Relative path that needs to match in order for this route to apply.\n\nAccepts paths similar to expressjs so that you can define parameters\nin the url /foo/:bar where bar would be available in incoming props.",
27412
+ "docs": "Relative path that needs to match in order for this route to apply.\n\nAccepts paths similar to expressjs so that you can define parameters\nin the url /foo/:bar where bar would be available in incoming props.\n\nA url ending in `*`, such as `*` or `/admin/*`, matches any remaining path,\nincluding none, so `/admin/*` also matches `/admin`. It only wins over a\nroute that matches the whole path when it has more fixed segments, so\n`/admin/*` wins over `/:section/:id` but not over `/admin/:id`. It always\nwins over a route that matches by ignoring trailing segments, such as\n`/admin` for `/admin/users`. The matched path is passed in the `pathMatch`\nprop without a leading `/`. A `*` on a route with child routes is ignored.",
27329
27413
  "docsTags": [
27330
27414
  {
27331
27415
  "name": "default",
@@ -27389,7 +27473,7 @@
27389
27473
  "mutable": false,
27390
27474
  "attr": "from",
27391
27475
  "reflectToAttr": false,
27392
- "docs": "A redirect route, redirects \"from\" a URL \"to\" another URL. This property is that \"from\" URL.\nIt needs to be an exact match of the navigated URL in order to apply.\n\nThe path specified in this value is always an absolute path, even if the initial `/` slash\nis not specified.",
27476
+ "docs": "A redirect route, redirects \"from\" a URL \"to\" another URL. This property is that \"from\" URL.\nIt needs to be an exact match of the navigated URL in order to apply, unless it contains a `*`.\nA `*` matches the rest of the path from that segment on, as long as the path has at least as\nmany segments as `from`, so `/admin/*` matches `/admin/posts` but not `/admin`.\n\nThe path specified in this value is always an absolute path, even if the initial `/` slash\nis not specified.",
27393
27477
  "docsTags": [],
27394
27478
  "values": [
27395
27479
  {
@@ -37770,6 +37854,11 @@
37770
37854
  "docstring": "Values are converted to strings when emitted which is\nwhy we do not have a `number` type here even though the\n`value` prop accepts a `number` type.",
37771
37855
  "path": "src/components/input/input-interface.ts"
37772
37856
  },
37857
+ "src/components/input-otp/input-otp-interface.ts::InputOtpLabelFormatter": {
37858
+ "declaration": "(index: number, length: number) => string",
37859
+ "docstring": "",
37860
+ "path": "src/components/input-otp/input-otp-interface.ts"
37861
+ },
37773
37862
  "src/components/input-otp/input-otp-interface.ts::InputOtpInputEventDetail": {
37774
37863
  "declaration": "export interface InputOtpInputEventDetail {\n value?: string | null;\n event?: Event;\n}",
37775
37864
  "docstring": "Values are converted to strings when emitted which is\nwhy we do not have a `number` type here even though the\n`value` prop accepts a `number` type.",