rei-kit 3.1.0 → 3.2.0

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.
@@ -6,21 +6,65 @@
6
6
  * asked for less motion, keeps assistive tech to the finished value, and
7
7
  * stops when hovered or focused if it moves on its own. The utilities that go
8
8
  * with them are in `rei-kit/motion.css`, which both presets include.
9
+ *
10
+ * @example
11
+ * ```vue
12
+ * <NumberTicker :value="balance" :format="{ style: 'currency', currency: 'TRY' }" />
13
+ * ```
14
+ *
15
+ * @see {@link CountUp} — the near-neighbour this is mistaken for
9
16
  */
10
17
  export { default as NumberTicker } from './NumberTicker.vue';
11
- /** A number that counts up to its value once it scrolls into view. */
18
+ /**
19
+ * A number that counts up to its value once it scrolls into view.
20
+ *
21
+ * @example
22
+ * ```vue
23
+ * <CountUp :value="12480" />
24
+ * ```
25
+ *
26
+ * @see {@link NumberTicker} — the near-neighbour this is mistaken for
27
+ */
12
28
  export { default as CountUp } from './CountUp.vue';
13
29
  /**
14
30
  * One word in a sentence that changes on its own: "Build it *glass*", then
15
31
  * *brutal*, then *soft*.
32
+ *
33
+ * @example
34
+ * ```vue
35
+ * <h1>Build it <TextRotate :words="['glass', 'brutal', 'soft']" /></h1>
36
+ * ```
16
37
  */
17
38
  export { default as TextRotate } from './TextRotate.vue';
18
39
  /**
19
40
  * Text that types itself out, and — given several lines — deletes each one
20
41
  * and types the next.
42
+ *
43
+ * @example
44
+ * ```vue
45
+ * <TypeWriter :text="['Write it once.', 'Change the look.', 'Ship it.']" />
46
+ * ```
21
47
  */
22
48
  export { default as TypeWriter } from './TypeWriter.vue';
23
- /** Content that fades or rises into place as it scrolls into view. */
49
+ /**
50
+ * Content that fades or rises into place as it scrolls into view.
51
+ *
52
+ * @example
53
+ * ```vue
54
+ * <BaseReveal v-for="(item, i) in items" :key="item" :delay="i * 80">
55
+ * <BaseCard>{{ item }}</BaseCard>
56
+ * </BaseReveal>
57
+ * ```
58
+ */
24
59
  export { default as BaseReveal } from './BaseReveal.vue';
25
- /** A row that scrolls sideways without end: logos, testimonials, a ticker. */
60
+ /**
61
+ * A row that scrolls sideways without end: logos, testimonials, a ticker.
62
+ *
63
+ * @example
64
+ * ```vue
65
+ * <BaseMarquee :duration="40">
66
+ * <BaseBadge v-for="name in names" :key="name">{{ name }}</BaseBadge>
67
+ * </BaseMarquee>
68
+ * ```
69
+ */
26
70
  export { default as BaseMarquee } from './BaseMarquee.vue';
@@ -14,9 +14,49 @@ export { useSnooze } from './use-snooze';
14
14
  /**
15
15
  * The card that offers to install the app, and remembers being turned
16
16
  * down.
17
+ *
18
+ * @example
19
+ * ```vue
20
+ * <InstallPrompt
21
+ * storage-key="my-app-install"
22
+ * title="Install My app"
23
+ * body="Open it from your home screen, even offline."
24
+ * action="Install"
25
+ * later="Not now"
26
+ * />
27
+ * ```
17
28
  */
18
29
  export { default as InstallPrompt } from './InstallPrompt.vue';
19
- /** The way back to installing after the card has been dismissed. */
30
+ /**
31
+ * The way back to installing after the card has been dismissed.
32
+ *
33
+ * @example
34
+ * ```vue
35
+ * <InstallSettings
36
+ * title="App"
37
+ * label="Install on this device"
38
+ * installed-label="Installed"
39
+ * body="Opens from your home screen, works offline."
40
+ * ios-body="Tap Share, then Add to Home Screen."
41
+ * action="Install"
42
+ * />
43
+ * ```
44
+ */
20
45
  export { default as InstallSettings } from './InstallSettings.vue';
21
- /** The card that says a new version is waiting. */
46
+ /**
47
+ * The card that says a new version is waiting.
48
+ *
49
+ * @example
50
+ * ```vue
51
+ * <UpdatePrompt
52
+ * :open="needRefresh"
53
+ * title="A new version is ready"
54
+ * body="Reload to get it."
55
+ * action="Reload"
56
+ * dismiss-label="Later"
57
+ * @update="reload"
58
+ * @dismiss="needRefresh = false"
59
+ * />
60
+ * ```
61
+ */
22
62
  export { default as UpdatePrompt } from './UpdatePrompt.vue';
package/dist/styles.css CHANGED
@@ -209,12 +209,12 @@
209
209
  cannot reach slot content, which belongs to the caller's scope, so
210
210
  `.tour-forward-*` and `.tour-backward-*` ship unscoped in
211
211
  `rei-kit/shell/mobile.css` where the caller's slide can actually see them. */
212
- .tour-enter-active[data-v-8c293eec],
213
- .tour-leave-active[data-v-8c293eec] {
212
+ .tour-enter-active[data-v-8daef303],
213
+ .tour-leave-active[data-v-8daef303] {
214
214
  transition: opacity var(--duration-base) ease;
215
215
  }
216
- .tour-enter-from[data-v-8c293eec],
217
- .tour-leave-to[data-v-8c293eec] {
216
+ .tour-enter-from[data-v-8daef303],
217
+ .tour-leave-to[data-v-8daef303] {
218
218
  opacity: 0;
219
219
  }
220
220
 
@@ -763,24 +763,24 @@
763
763
  /* The buttons are out of the Tab order: the keyboard already has the arrows,
764
764
  and three stops for one number is two too many. They stay reachable by
765
765
  pointer and by touch, which is who they are for. */
766
- .rk-number[data-v-082bd3d7] {
766
+ .rk-number[data-v-d8a8fb7e] {
767
767
  display: flex;
768
768
  align-items: center;
769
769
  height: 2.75rem;
770
770
  border-radius: var(--radius-card);
771
771
  padding: 0 0.25rem;
772
772
  }
773
- .rk-number[data-v-082bd3d7]:focus-within {
773
+ .rk-number[data-v-d8a8fb7e]:focus-within {
774
774
  outline: 2px solid var(--color-primary);
775
775
  outline-offset: 1px;
776
776
  }
777
- .rk-number.is-invalid[data-v-082bd3d7] {
777
+ .rk-number.is-invalid[data-v-d8a8fb7e] {
778
778
  --surface-border-color: var(--color-negative);
779
779
  }
780
- .rk-number.is-disabled[data-v-082bd3d7] {
780
+ .rk-number.is-disabled[data-v-d8a8fb7e] {
781
781
  opacity: 0.5;
782
782
  }
783
- .rk-number-field[data-v-082bd3d7] {
783
+ .rk-number-field[data-v-d8a8fb7e] {
784
784
  min-width: 0;
785
785
  flex: 1;
786
786
  background: transparent;
@@ -790,7 +790,7 @@
790
790
  color: var(--color-ink);
791
791
  outline: none;
792
792
  }
793
- .rk-number-step[data-v-082bd3d7] {
793
+ .rk-number-step[data-v-d8a8fb7e] {
794
794
  display: grid;
795
795
  width: 2.25rem;
796
796
  height: 2.25rem;
@@ -800,10 +800,10 @@
800
800
  color: var(--color-ink);
801
801
  transition: background-color var(--duration-fast) var(--ease-standard);
802
802
  }
803
- .rk-number-step[data-v-082bd3d7]:hover:not(:disabled) {
803
+ .rk-number-step[data-v-d8a8fb7e]:hover:not(:disabled) {
804
804
  background: var(--color-muted);
805
805
  }
806
- .rk-number-step[data-v-082bd3d7]:disabled {
806
+ .rk-number-step[data-v-d8a8fb7e]:disabled {
807
807
  opacity: 0.35;
808
808
  }
809
809
 
@@ -1769,7 +1769,7 @@ to {
1769
1769
  color: var(--color-ink-soft);
1770
1770
  }
1771
1771
 
1772
- .rk-color-row[data-v-5ca23aa9] {
1772
+ .rk-color-row[data-v-721fcae8] {
1773
1773
  display: flex;
1774
1774
  flex-wrap: wrap;
1775
1775
  align-items: center;
@@ -1779,7 +1779,7 @@ to {
1779
1779
  /* The well is what is seen; the input inside it is the whole target. A
1780
1780
  native colour input cannot be styled past its swatch in every browser, so
1781
1781
  it is stretched over a box that can be. */
1782
- .rk-color-well[data-v-5ca23aa9] {
1782
+ .rk-color-well[data-v-721fcae8] {
1783
1783
  position: relative;
1784
1784
  display: block;
1785
1785
  width: 2.75rem;
@@ -1789,7 +1789,7 @@ to {
1789
1789
  border: 1px solid var(--color-hair);
1790
1790
  background: var(--rk-color);
1791
1791
  }
1792
- .rk-color-native[data-v-5ca23aa9] {
1792
+ .rk-color-native[data-v-721fcae8] {
1793
1793
  position: absolute;
1794
1794
  inset: 0;
1795
1795
  width: 100%;
@@ -1799,14 +1799,14 @@ to {
1799
1799
  }
1800
1800
 
1801
1801
  /* The ring goes on the well, since the input it belongs to is invisible. */
1802
- .rk-color-well[data-v-5ca23aa9]:has(.rk-color-native:focus-visible) {
1802
+ .rk-color-well[data-v-721fcae8]:has(.rk-color-native:focus-visible) {
1803
1803
  outline: 2px solid var(--color-primary);
1804
1804
  outline-offset: 2px;
1805
1805
  }
1806
- .rk-color-native[data-v-5ca23aa9]:disabled {
1806
+ .rk-color-native[data-v-721fcae8]:disabled {
1807
1807
  cursor: not-allowed;
1808
1808
  }
1809
- .rk-color-hex[data-v-5ca23aa9] {
1809
+ .rk-color-hex[data-v-721fcae8] {
1810
1810
  width: 7.5rem;
1811
1811
  height: 2.75rem;
1812
1812
  border-radius: var(--radius-card);
@@ -1817,19 +1817,19 @@ to {
1817
1817
  font-size: 1rem;
1818
1818
  color: var(--color-ink);
1819
1819
  }
1820
- .rk-color-hex[data-v-5ca23aa9]:focus-visible {
1820
+ .rk-color-hex[data-v-721fcae8]:focus-visible {
1821
1821
  outline: 2px solid var(--color-primary);
1822
1822
  outline-offset: -1px;
1823
1823
  }
1824
- .rk-color-hex[data-v-5ca23aa9]:disabled {
1824
+ .rk-color-hex[data-v-721fcae8]:disabled {
1825
1825
  opacity: 0.5;
1826
1826
  }
1827
- .rk-color-swatches[data-v-5ca23aa9] {
1827
+ .rk-color-swatches[data-v-721fcae8] {
1828
1828
  display: flex;
1829
1829
  flex-wrap: wrap;
1830
1830
  gap: 0.375rem;
1831
1831
  }
1832
- .rk-color-swatch[data-v-5ca23aa9] {
1832
+ .rk-color-swatch[data-v-721fcae8] {
1833
1833
  display: block;
1834
1834
  width: 1.75rem;
1835
1835
  height: 1.75rem;
@@ -1838,25 +1838,25 @@ to {
1838
1838
  background: var(--rk-color);
1839
1839
  transition: transform var(--duration-fast) var(--ease-standard);
1840
1840
  }
1841
- .rk-color-swatch[data-v-5ca23aa9]:hover:not(:disabled) {
1841
+ .rk-color-swatch[data-v-721fcae8]:hover:not(:disabled) {
1842
1842
  transform: scale(1.1);
1843
1843
  }
1844
1844
 
1845
1845
  /* A ring around the chosen one rather than a tick: a tick has to be dark or
1846
1846
  light to be seen, and the swatch beneath it is whatever the app chose. */
1847
- .rk-color-swatch.is-chosen[data-v-5ca23aa9] {
1847
+ .rk-color-swatch.is-chosen[data-v-721fcae8] {
1848
1848
  box-shadow:
1849
1849
  0 0 0 2px var(--color-surface),
1850
1850
  0 0 0 4px var(--color-primary);
1851
1851
  }
1852
- .rk-color-swatch[data-v-5ca23aa9]:disabled {
1852
+ .rk-color-swatch[data-v-721fcae8]:disabled {
1853
1853
  opacity: 0.5;
1854
1854
  }
1855
1855
  @media (prefers-reduced-motion: reduce) {
1856
- .rk-color-swatch[data-v-5ca23aa9] {
1856
+ .rk-color-swatch[data-v-721fcae8] {
1857
1857
  transition: none;
1858
1858
  }
1859
- .rk-color-swatch[data-v-5ca23aa9]:hover:not(:disabled) {
1859
+ .rk-color-swatch[data-v-721fcae8]:hover:not(:disabled) {
1860
1860
  transform: none;
1861
1861
  }
1862
1862
  }
@@ -2054,7 +2054,7 @@ to {
2054
2054
  color: var(--color-ink);
2055
2055
  }
2056
2056
 
2057
- .rk-tags[data-v-1fd08921] {
2057
+ .rk-tags[data-v-572364bd] {
2058
2058
  display: flex;
2059
2059
  min-height: 2.75rem;
2060
2060
  cursor: text;
@@ -2067,18 +2067,18 @@ to {
2067
2067
 
2068
2068
  /* `:has(input:focus-visible)` rather than `:focus-within`: the ring belongs
2069
2069
  to the field, not to the chips' remove buttons, which carry their own. */
2070
- .rk-tags[data-v-1fd08921]:has(.rk-tags-field:focus-visible) {
2070
+ .rk-tags[data-v-572364bd]:has(.rk-tags-field:focus-visible) {
2071
2071
  outline: 2px solid var(--color-primary);
2072
2072
  outline-offset: 1px;
2073
2073
  }
2074
- .rk-tags.is-invalid[data-v-1fd08921] {
2074
+ .rk-tags.is-invalid[data-v-572364bd] {
2075
2075
  --surface-border-color: var(--color-negative);
2076
2076
  }
2077
- .rk-tags.is-disabled[data-v-1fd08921] {
2077
+ .rk-tags.is-disabled[data-v-572364bd] {
2078
2078
  cursor: not-allowed;
2079
2079
  opacity: 0.5;
2080
2080
  }
2081
- .rk-tags-field[data-v-1fd08921] {
2081
+ .rk-tags-field[data-v-572364bd] {
2082
2082
  min-width: 6rem;
2083
2083
  flex: 1;
2084
2084
  background: transparent;
@@ -2138,10 +2138,10 @@ to {
2138
2138
  transform: translateY(4px) scale(0.98);
2139
2139
  }
2140
2140
 
2141
- .rk-time[data-v-ad58b4f3] {
2141
+ .rk-time[data-v-2ae98769] {
2142
2142
  display: block;
2143
2143
  }
2144
- .rk-time-field[data-v-ad58b4f3] {
2144
+ .rk-time-field[data-v-2ae98769] {
2145
2145
  display: flex;
2146
2146
  width: 100%;
2147
2147
  height: 2.75rem;
@@ -2154,16 +2154,16 @@ to {
2154
2154
  font-variant-numeric: tabular-nums;
2155
2155
  color: var(--color-ink);
2156
2156
  }
2157
- .rk-time-field.is-empty[data-v-ad58b4f3] {
2157
+ .rk-time-field.is-empty[data-v-2ae98769] {
2158
2158
  color: var(--color-ink-soft);
2159
2159
  }
2160
- .rk-time-field.is-invalid[data-v-ad58b4f3] {
2160
+ .rk-time-field.is-invalid[data-v-2ae98769] {
2161
2161
  --surface-border-color: var(--color-negative);
2162
2162
  }
2163
- .rk-time-field[data-v-ad58b4f3]:disabled {
2163
+ .rk-time-field[data-v-2ae98769]:disabled {
2164
2164
  opacity: 0.5;
2165
2165
  }
2166
- .rk-time-columns[data-v-ad58b4f3] {
2166
+ .rk-time-columns[data-v-2ae98769] {
2167
2167
  display: grid;
2168
2168
  grid-template-columns: minmax(0, 1.4fr) minmax(0, 1fr);
2169
2169
  gap: 0.5rem;
@@ -2232,16 +2232,16 @@ to {
2232
2232
  margin-top: 0.5rem;
2233
2233
  }
2234
2234
 
2235
- .rk-date[data-v-42390973] {
2235
+ .rk-date[data-v-2c91e3bb] {
2236
2236
  display: block;
2237
2237
  }
2238
2238
 
2239
2239
  /* Wider than a popover's default: a calendar and a column of presets side by
2240
2240
  side do not fit in 24rem, and the month title wraps in two. */
2241
- .rk-date[data-v-42390973] .rk-popover-panel {
2241
+ .rk-date[data-v-2c91e3bb] .rk-popover-panel {
2242
2242
  max-width: min(34rem, calc(100vw - 1rem));
2243
2243
  }
2244
- .rk-date-wrap[data-v-42390973] {
2244
+ .rk-date-wrap[data-v-2c91e3bb] {
2245
2245
  display: flex;
2246
2246
  width: 100%;
2247
2247
  height: 2.75rem;
@@ -2249,7 +2249,7 @@ to {
2249
2249
  border-radius: var(--radius-card);
2250
2250
  padding-inline-end: 0.375rem;
2251
2251
  }
2252
- .rk-date-field[data-v-42390973] {
2252
+ .rk-date-field[data-v-2c91e3bb] {
2253
2253
  display: flex;
2254
2254
  min-width: 0;
2255
2255
  flex: 1;
@@ -2262,16 +2262,16 @@ to {
2262
2262
  font-size: 1rem;
2263
2263
  color: var(--color-ink);
2264
2264
  }
2265
- .rk-date-field.is-empty[data-v-42390973] {
2265
+ .rk-date-field.is-empty[data-v-2c91e3bb] {
2266
2266
  color: var(--color-ink-soft);
2267
2267
  }
2268
- .rk-date-wrap.is-invalid[data-v-42390973] {
2268
+ .rk-date-wrap.is-invalid[data-v-2c91e3bb] {
2269
2269
  --surface-border-color: var(--color-negative);
2270
2270
  }
2271
- .rk-date-field[data-v-42390973]:disabled {
2271
+ .rk-date-field[data-v-2c91e3bb]:disabled {
2272
2272
  opacity: 0.5;
2273
2273
  }
2274
- .rk-date-clear[data-v-42390973] {
2274
+ .rk-date-clear[data-v-2c91e3bb] {
2275
2275
  display: grid;
2276
2276
  width: 1.375rem;
2277
2277
  height: 1.375rem;
@@ -2280,27 +2280,27 @@ to {
2280
2280
  border-radius: 9999px;
2281
2281
  color: var(--color-ink-soft);
2282
2282
  }
2283
- .rk-date-clear[data-v-42390973]:hover {
2283
+ .rk-date-clear[data-v-2c91e3bb]:hover {
2284
2284
  background: var(--color-muted);
2285
2285
  color: var(--color-ink);
2286
2286
  }
2287
- .rk-date-clear[data-v-42390973]:focus-visible {
2287
+ .rk-date-clear[data-v-2c91e3bb]:focus-visible {
2288
2288
  outline: 2px solid var(--color-primary);
2289
2289
  outline-offset: 1px;
2290
2290
  }
2291
2291
 
2292
2292
  /* As wide as its contents rather than as narrow as they will go: in a flex
2293
2293
  panel the calendar shrinks, and a squeezed month title wraps in two. */
2294
- .rk-date-panel[data-v-42390973] {
2294
+ .rk-date-panel[data-v-2c91e3bb] {
2295
2295
  display: flex;
2296
2296
  width: max-content;
2297
2297
  max-width: 100%;
2298
2298
  gap: 0.75rem;
2299
2299
  }
2300
- .rk-date-panel[data-v-42390973] .rk-cal {
2300
+ .rk-date-panel[data-v-2c91e3bb] .rk-cal {
2301
2301
  width: 17rem;
2302
2302
  }
2303
- .rk-date-presets[data-v-42390973] {
2303
+ .rk-date-presets[data-v-2c91e3bb] {
2304
2304
  display: flex;
2305
2305
  min-width: 8rem;
2306
2306
  flex-direction: column;
@@ -2308,7 +2308,7 @@ to {
2308
2308
  border-inline-end: 1px solid var(--surface-border-color);
2309
2309
  padding-inline-end: 0.75rem;
2310
2310
  }
2311
- .rk-date-preset[data-v-42390973] {
2311
+ .rk-date-preset[data-v-2c91e3bb] {
2312
2312
  width: 100%;
2313
2313
  border-radius: var(--radius-cell);
2314
2314
  padding: 0.375rem 0.5rem;
@@ -2317,14 +2317,14 @@ to {
2317
2317
  color: var(--color-ink);
2318
2318
  transition: background-color var(--duration-fast) var(--ease-standard);
2319
2319
  }
2320
- .rk-date-preset[data-v-42390973]:hover {
2320
+ .rk-date-preset[data-v-2c91e3bb]:hover {
2321
2321
  background: var(--color-muted);
2322
2322
  }
2323
2323
  @media (max-width: 30rem) {
2324
- .rk-date-panel[data-v-42390973] {
2324
+ .rk-date-panel[data-v-2c91e3bb] {
2325
2325
  flex-direction: column;
2326
2326
  }
2327
- .rk-date-presets[data-v-42390973] {
2327
+ .rk-date-presets[data-v-2c91e3bb] {
2328
2328
  flex-direction: row;
2329
2329
  flex-wrap: wrap;
2330
2330
  border-inline-end: 0;
@@ -2332,7 +2332,7 @@ to {
2332
2332
  padding-inline-end: 0;
2333
2333
  padding-bottom: 0.5rem;
2334
2334
  }
2335
- .rk-date-preset[data-v-42390973] {
2335
+ .rk-date-preset[data-v-2c91e3bb] {
2336
2336
  width: auto;
2337
2337
  }
2338
2338
  }
@@ -489,7 +489,8 @@ var BaseInput_default = /* @__PURE__ */ defineComponent({
489
489
  },
490
490
  type: { default: "text" },
491
491
  size: { default: "md" },
492
- variant: { default: "default" }
492
+ variant: { default: "default" },
493
+ fieldId: { default: () => void 0 }
493
494
  },
494
495
  emits: ["update:modelValue"],
495
496
  setup(__props, { emit: __emit }) {
@@ -538,6 +539,7 @@ var BaseInput_default = /* @__PURE__ */ defineComponent({
538
539
  const model = useBoundValue(() => __props.modelValue, (value) => emit("update:modelValue", value));
539
540
  return (_ctx, _cache) => {
540
541
  return openBlock(), createBlock(FormField_default, {
542
+ "field-id": __props.fieldId,
541
543
  class: normalizeClass(unref(fieldClass)),
542
544
  style: normalizeStyle(unref(fieldStyle)),
543
545
  label: __props.label,
@@ -573,6 +575,7 @@ var BaseInput_default = /* @__PURE__ */ defineComponent({
573
575
  ] }), null, 16, _hoisted_4)), [[vModelDynamic, unref(model)]])]),
574
576
  _: 3
575
577
  }, 8, [
578
+ "field-id",
576
579
  "class",
577
580
  "style",
578
581
  "label",
@@ -655,4 +658,4 @@ function useAnnounce() {
655
658
  //#endregion
656
659
  export { toRedirectPath as _, useFieldAttrs as a, useToast as c, isThemePreference as d, readStoredTheme as f, safeRedirect as g, tapFeedback as h, BaseInput_default as i, useOnline as l, useTheme as m, useAnnounce as n, FormField_default as o, setThemeStorageKey as p, GoogleButton_default as r, BaseAlert_default as s, announce as t, applyTheme as u };
657
660
 
658
- //# sourceMappingURL=use-announce-QN44Whqc.js.map
661
+ //# sourceMappingURL=use-announce-D8SKsiqn.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"use-announce-D8SKsiqn.js","names":["$slots","$slots"],"sources":["../src/utils/redirect.ts","../src/utils/haptics.ts","../src/composables/use-theme.ts","../src/composables/use-online.ts","../src/composables/use-toast.ts","../src/components/BaseAlert.vue","../src/components/BaseAlert.vue","../src/components/FormField.vue","../src/components/FormField.vue","../src/composables/use-field-attrs.ts","../src/components/BaseInput.vue","../src/components/BaseInput.vue","../src/components/GoogleButton.vue","../src/components/GoogleButton.vue","../src/composables/use-announce.ts"],"sourcesContent":["/**\n * What a router hands back for one query key.\n *\n * Inlined rather than imported from vue-router: the shape is `string | null`\n * either way, and a helper this small should not drag a router into the\n * package's dependencies.\n */\nexport type QueryValue = string | null\n\n/**\n * Resolves a `?redirect=` query value into a safe in-app path.\n *\n * Only same-origin paths are accepted. Anything else falls back to `/`,\n * so a crafted link cannot bounce a user from the real login page to a\n * phishing clone.\n *\n * Pure: takes the query value instead of reading the router, so it also\n * works inside navigation guards and can be unit tested.\n *\n * @param target - Raw `route.query.redirect` value. May be a string, an\n * array (repeated query key), `null`, or `undefined`.\n * @returns A path starting with a single `/`. Defaults to `/`.\n *\n * @example\n * ```ts\n * // in a view\n * await router.push(safeRedirect(route.query.redirect))\n *\n * // in a guard\n * return safeRedirect(to.query.redirect)\n * ```\n *\n * @example\n * ```ts\n * safeRedirect('/week') // '/week'\n * safeRedirect('https://evil.com') // '/'\n * safeRedirect('//evil.com') // '/' (protocol-relative URL)\n * safeRedirect(['/a', '/b']) // '/'\n * safeRedirect(undefined) // '/'\n * ```\n */\nexport function safeRedirect(target: QueryValue | QueryValue[] | undefined): string {\n if (typeof target === 'string' && target.startsWith('/') && !target.startsWith('//')) {\n return target\n }\n\n return '/'\n}\n\n/**\n * Where to send somebody back to after signing in, without the fragment.\n *\n * Supabase's implicit OAuth flow hands the browser back with the access and\n * refresh tokens in the URL fragment. A fragment is client-side only — it is\n * never sent to a server. Copied into a query parameter it stops being one:\n * `/login?redirect=/%23access_token=…` is sent on the very next request and\n * lands in the host's access logs, in `Referer` headers and in browser history.\n *\n * So the redirect keeps the path and the query and drops everything from the\n * `#`. There is nothing after it worth returning to anyway.\n *\n * One of the two phone apps had this and the other was passing `fullPath`\n * straight through, which is the leak above with nothing in the way of it. That\n * is the shape of bug a shared kit exists to end: it was fixed once, in the app\n * whose author happened to think of it.\n *\n * @example\n * ```ts\n * toRedirectPath('/ledger?direction=out') // '/ledger?direction=out'\n * toRedirectPath('/#access_token=abc') // '/'\n * ```\n */\nexport function toRedirectPath(fullPath: string): string {\n const [path] = fullPath.split('#')\n\n return path === undefined || path === '' ? '/' : path\n}\n","/**\n * A short vibration for a confirmed tap.\n *\n * Optional chaining is not decoration: iOS Safari has no `vibrate` at all, and\n * calling it unguarded would throw on every marked day.\n *\n * @param duration - Milliseconds. Keep it under ~15ms; longer reads as an alert.\n */\nexport function tapFeedback(duration = 10): void {\n navigator.vibrate?.(duration)\n}\n","import { ref, watch } from 'vue'\nimport type { Ref } from 'vue'\n\n/** What the user asked for; `system` follows the OS. */\nexport type ThemePreference = 'system' | 'light' | 'dark'\n\n/**\n * Namespaced by the app, not by this package.\n *\n * Two rei-kit apps served from the same origin would otherwise share one theme\n * setting — and during development on localhost, they will be.\n */\nlet storageKey = 'rei-theme'\n\nexport function isThemePreference(value: unknown): value is ThemePreference {\n return value === 'system' || value === 'light' || value === 'dark'\n}\n\n/** Reads the stored preference, falling back to `system`. */\nexport function readStoredTheme(): ThemePreference {\n try {\n const stored = localStorage.getItem(storageKey)\n\n return isThemePreference(stored) ? stored : 'system'\n } catch {\n return 'system'\n }\n}\n\nfunction storeTheme(preference: ThemePreference): void {\n try {\n localStorage.setItem(storageKey, preference)\n } catch {\n // Private mode or blocked storage: the choice just will not persist.\n }\n}\n\n/**\n * Does the environment prefer a dark scheme?\n *\n * `matchMedia` is checked for on its own rather than inferred from `document`.\n * Having one does not imply having the other: jsdom supplies a document and no\n * `matchMedia`, so a consumer's component test that so much as mounts something\n * calling `useTheme` threw — and some embedded webviews are the same. Where\n * there is nothing to ask, the answer is no rather than an exception.\n */\nfunction prefersDarkScheme(): boolean {\n return typeof window !== 'undefined' && typeof window.matchMedia === 'function'\n ? window.matchMedia('(prefers-color-scheme: dark)').matches\n : false\n}\n\n/**\n * Adds or removes `.dark` on `<html>`, resolving `system` against the OS.\n *\n * A no-op without a document. There is no OS preference to read on a server and\n * no `<html>` to write to, so a prerender leaves the class off and the app\n * decides the theme before hydration — see the note in the README.\n */\nexport function applyTheme(preference: ThemePreference): void {\n if (typeof document === 'undefined') return\n\n const isDark = preference === 'dark' || (preference === 'system' && prefersDarkScheme())\n\n document.documentElement.classList.toggle('dark', isDark)\n}\n\n/**\n * The shared preference, created on first use rather than at import.\n *\n * Lazy on purpose: reading storage at import time would lock in the default key\n * before an app had a chance to set its own, leaving the controller reading one\n * key and writing another.\n */\nlet preference: Ref<ThemePreference> | null = null\n\nfunction controller(): Ref<ThemePreference> {\n if (preference) return preference\n\n preference = ref<ThemePreference>(readStoredTheme())\n\n watch(\n preference,\n (next) => {\n storeTheme(next)\n applyTheme(next)\n },\n { immediate: true },\n )\n\n // While on `system`, follow the OS if the user flips it at night. Only where\n // there is something to listen to; see `prefersDarkScheme`.\n if (typeof window !== 'undefined' && typeof window.matchMedia === 'function') {\n window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => {\n if (preference?.value === 'system') applyTheme('system')\n })\n }\n\n return preference\n}\n\n/**\n * Sets where the preference is stored.\n *\n * Safe in either order: called before the first `useTheme()` it simply changes\n * the key, and called after it re-reads under the new one, so the controller\n * never reads from one key while writing to another.\n *\n * @example\n * ```ts\n * setThemeStorageKey('hibi-theme') // once, at startup\n * ```\n */\nexport function setThemeStorageKey(key: string): void {\n storageKey = key\n if (preference) preference.value = readStoredTheme()\n}\n\n/** @returns The shared preference ref; assigning to it stores and applies it. */\nexport function useTheme(): Ref<ThemePreference> {\n return controller()\n}\n","import { onMounted, onUnmounted, readonly, ref } from 'vue'\n\n/**\n * Tracks whether the browser thinks it has a network connection.\n *\n * Note the limit: `navigator.onLine` only reports whether a network interface\n * is up, not whether requests actually succeed. Treat it as a hint for the UI,\n * never as a reason to skip error handling.\n *\n * Listeners are removed on unmount, so the composable is safe to call per view.\n *\n * @returns A readonly ref that flips with the browser's online/offline events.\n *\n * @example\n * ```ts\n * const isOnline = useOnline()\n * // <p v-if=\"!isOnline\">You're offline.</p>\n * ```\n */\nexport function useOnline() {\n const isOnline = ref(true)\n\n function update() {\n isOnline.value = navigator.onLine\n }\n\n onMounted(() => {\n update()\n window.addEventListener('online', update)\n window.addEventListener('offline', update)\n })\n\n onUnmounted(() => {\n window.removeEventListener('online', update)\n window.removeEventListener('offline', update)\n })\n\n return readonly(isOnline)\n}\n","import { readonly, ref } from 'vue'\n\n/**\n * Saying that something happened.\n *\n * The reason this exists is a measurement rather than a preference: the word\n * \"toast\" appeared **zero times** across all three consuming apps. Not because\n * they had decided against it — because there was no mechanism, so every save,\n * every delete and every export finished in silence and the only way to know\n * it had worked was that nothing had visibly broken.\n *\n * ── What is deliberately not here ──\n *\n * **No text.** The kit never knows a sentence. Callers pass the message; a\n * component that called a translator would force one on the app.\n *\n * **Not for form errors.** A field that was rejected says so beside itself,\n * where the reader's eye already is and where it stays until fixed. A toast\n * that disappears after four seconds is the wrong place for something the\n * reader has to act on. Use `BaseAlert` and `FormField` for those; use this\n * for what has already happened.\n *\n * **A singleton, on purpose.** Two hosts would mean two stacks racing for the\n * same corner. The store lives at module scope and `ToastHost` renders it.\n */\nexport type ToastTone = 'info' | 'success' | 'warning' | 'danger'\n\n/**\n * One button on a toast — nearly always \"Undo\".\n *\n * The toast is dismissed when it is pressed, so the handler only has to do\n * the undoing.\n */\nexport interface ToastAction {\n /** The button's text. Already translated. */\n readonly label: string\n readonly onClick: () => void\n}\n\nexport interface Toast {\n readonly id: number\n readonly message: string\n readonly tone: ToastTone\n /** Milliseconds on screen. `0` stays until dismissed. */\n readonly duration: number\n readonly action?: ToastAction | undefined\n}\n\nexport interface ToastOptions {\n /** Milliseconds on screen; `0` stays until dismissed. */\n duration?: number | undefined\n /**\n * A button on the toast. An undo is the kindest confirmation there is: act\n * first, and let the reader take it back, instead of asking \"are you sure\"\n * before every delete.\n */\n action?: ToastAction | undefined\n}\n\n/**\n * Four seconds: long enough to read a short sentence twice, short enough that\n * a second action does not queue behind it.\n */\nconst DEFAULT_DURATION = 4000\n\n/**\n * A failure is read more slowly than a confirmation, and more often twice.\n */\nconst DANGER_DURATION = 7000\n\n/**\n * A toast with a button stays longer: the reader has to read it, decide and\n * reach the button. Four seconds is enough to read \"Deleted\", not to undo it.\n */\nconst ACTION_DURATION = 8000\n\n/**\n * Three at once. A fourth pushes the oldest out rather than growing the stack\n * off the top of the screen — an action that produces ten toasts is a loop,\n * and a loop should not be able to cover the app it is running in.\n */\nconst MAX_VISIBLE = 3\n\nconst items = ref<Toast[]>([])\n\nlet nextId = 0\n\ninterface Countdown {\n handle: ReturnType<typeof setTimeout>\n remaining: number\n startedAt: number\n}\n\nconst countdowns = new Map<number, Countdown>()\n\nfunction clearCountdown(id: number): void {\n const countdown = countdowns.get(id)\n if (countdown === undefined) return\n\n clearTimeout(countdown.handle)\n countdowns.delete(id)\n}\n\n/** Removes a toast, whether it timed out or was dismissed. */\nfunction dismiss(id: number): void {\n clearCountdown(id)\n items.value = items.value.filter((item) => item.id !== id)\n}\n\n/** Removes everything on screen. For a route change, or a sign-out. */\nfunction dismissAll(): void {\n for (const id of countdowns.keys()) clearCountdown(id)\n items.value = []\n}\n\nfunction arm(id: number, remaining: number): void {\n // A timer is a browser thing. On a server there is nothing to time and\n // nothing to see, and arming one would keep the process alive past the last\n // page — which is how a prerender build hangs instead of finishing.\n if (typeof window === 'undefined' || remaining <= 0) return\n\n countdowns.set(id, {\n handle: setTimeout(() => dismiss(id), remaining),\n remaining,\n startedAt: Date.now(),\n })\n}\n\n/**\n * Stops the clock on a toast the reader is pointing at.\n *\n * Somebody who has moved the pointer onto it is reading it, and taking it away\n * mid-sentence is the one thing a notification must not do.\n */\nfunction pause(id: number): void {\n const countdown = countdowns.get(id)\n if (countdown === undefined) return\n\n clearTimeout(countdown.handle)\n countdowns.set(id, {\n ...countdown,\n remaining: Math.max(0, countdown.remaining - (Date.now() - countdown.startedAt)),\n })\n}\n\n/** Starts it again, from where it stopped rather than from the beginning. */\nfunction resume(id: number): void {\n const countdown = countdowns.get(id)\n if (countdown === undefined) return\n\n arm(id, countdown.remaining)\n}\n\nfunction push(tone: ToastTone, message: string, options: ToastOptions = {}): number {\n const id = ++nextId\n const duration =\n options.duration ??\n (options.action ? ACTION_DURATION : tone === 'danger' ? DANGER_DURATION : DEFAULT_DURATION)\n\n const next = [...items.value, { id, message, tone, duration, action: options.action }]\n\n while (next.length > MAX_VISIBLE) {\n const oldest = next.shift()\n if (oldest !== undefined) clearCountdown(oldest.id)\n }\n\n items.value = next\n arm(id, duration)\n\n return id\n}\n\n/**\n * The stack, and the four ways to add to it.\n *\n * @example\n * ```ts\n * const toast = useToast()\n *\n * toast.success(t('habit.saved'))\n * toast.danger(t('common.failed'), { duration: 0 }) // stays until dismissed\n *\n * const id = toast.info(t('export.preparing'), { duration: 0 })\n * toast.dismiss(id)\n *\n * toast.success(t('entry.deleted'), {\n * action: { label: t('common.undo'), onClick: () => restore(entry) },\n * })\n * ```\n */\nexport function useToast() {\n return {\n /** Every toast on screen, oldest first. `ToastHost` renders this. */\n toasts: readonly(items),\n info: (message: string, options?: ToastOptions) => push('info', message, options),\n success: (message: string, options?: ToastOptions) => push('success', message, options),\n warning: (message: string, options?: ToastOptions) => push('warning', message, options),\n danger: (message: string, options?: ToastOptions) => push('danger', message, options),\n dismiss,\n dismissAll,\n pause,\n resume,\n }\n}\n","<script setup lang=\"ts\">\nimport { computed } from 'vue'\n\n/**\n * A message the reader has to take in before carrying on.\n *\n * Roles rather than colours, like everything else here: `info` is neutral,\n * `success` confirms, `warning` is a condition to know about, `danger` is\n * something that went wrong or is about to. A component that took a hex would\n * be a component that ignores the theme, and the theme is the whole reason the\n * kit exists.\n *\n * `assertive` decides how a screen reader treats it: a failed save interrupts,\n * a note about a form field waits its turn. Getting this wrong is invisible on\n * screen and rude in a screen reader, which is why it is a prop and not a\n * guess.\n */\nconst { tone = 'info', assertive = false } = defineProps<{\n tone?: 'info' | 'success' | 'warning' | 'danger' | undefined\n /** Announce immediately, interrupting. For failures the reader must act on. */\n assertive?: boolean | undefined\n}>()\n\nconst TONES = {\n info: 'border-hair bg-muted/40 text-ink',\n success: 'border-positive/35 bg-positive/8 text-ink',\n warning: 'border-warning/40 bg-warning/8 text-ink',\n danger: 'border-negative/35 bg-negative/8 text-ink',\n} as const\n\nconst MARKS = {\n info: 'bg-ink-soft/15 text-ink-soft',\n success: 'bg-positive/15 text-positive',\n warning: 'bg-warning/15 text-warning',\n danger: 'bg-negative/15 text-negative',\n} as const\n\nconst skin = computed(() => TONES[tone])\nconst mark = computed(() => MARKS[tone])\n</script>\n\n<template>\n <div\n class=\"rounded-card flex items-start gap-3 border px-4 py-3.5 text-sm leading-relaxed\"\n :class=\"skin\"\n :role=\"assertive ? 'alert' : 'status'\"\n :aria-live=\"assertive ? 'assertive' : 'polite'\"\n >\n <span\n v-if=\"$slots.mark\"\n class=\"mt-px grid size-6 shrink-0 place-items-center rounded-full text-xs font-semibold\"\n :class=\"mark\"\n aria-hidden=\"true\"\n >\n <slot name=\"mark\" />\n </span>\n\n <div class=\"min-w-0 flex-1\">\n <p v-if=\"$slots.title\" class=\"text-ink font-semibold\">\n <slot name=\"title\" />\n </p>\n <div :class=\"$slots.title ? 'mt-1' : ''\"><slot /></div>\n </div>\n\n <slot name=\"action\" />\n </div>\n</template>\n","<script setup lang=\"ts\">\nimport { computed } from 'vue'\n\n/**\n * A message the reader has to take in before carrying on.\n *\n * Roles rather than colours, like everything else here: `info` is neutral,\n * `success` confirms, `warning` is a condition to know about, `danger` is\n * something that went wrong or is about to. A component that took a hex would\n * be a component that ignores the theme, and the theme is the whole reason the\n * kit exists.\n *\n * `assertive` decides how a screen reader treats it: a failed save interrupts,\n * a note about a form field waits its turn. Getting this wrong is invisible on\n * screen and rude in a screen reader, which is why it is a prop and not a\n * guess.\n */\nconst { tone = 'info', assertive = false } = defineProps<{\n tone?: 'info' | 'success' | 'warning' | 'danger' | undefined\n /** Announce immediately, interrupting. For failures the reader must act on. */\n assertive?: boolean | undefined\n}>()\n\nconst TONES = {\n info: 'border-hair bg-muted/40 text-ink',\n success: 'border-positive/35 bg-positive/8 text-ink',\n warning: 'border-warning/40 bg-warning/8 text-ink',\n danger: 'border-negative/35 bg-negative/8 text-ink',\n} as const\n\nconst MARKS = {\n info: 'bg-ink-soft/15 text-ink-soft',\n success: 'bg-positive/15 text-positive',\n warning: 'bg-warning/15 text-warning',\n danger: 'bg-negative/15 text-negative',\n} as const\n\nconst skin = computed(() => TONES[tone])\nconst mark = computed(() => MARKS[tone])\n</script>\n\n<template>\n <div\n class=\"rounded-card flex items-start gap-3 border px-4 py-3.5 text-sm leading-relaxed\"\n :class=\"skin\"\n :role=\"assertive ? 'alert' : 'status'\"\n :aria-live=\"assertive ? 'assertive' : 'polite'\"\n >\n <span\n v-if=\"$slots.mark\"\n class=\"mt-px grid size-6 shrink-0 place-items-center rounded-full text-xs font-semibold\"\n :class=\"mark\"\n aria-hidden=\"true\"\n >\n <slot name=\"mark\" />\n </span>\n\n <div class=\"min-w-0 flex-1\">\n <p v-if=\"$slots.title\" class=\"text-ink font-semibold\">\n <slot name=\"title\" />\n </p>\n <div :class=\"$slots.title ? 'mt-1' : ''\"><slot /></div>\n </div>\n\n <slot name=\"action\" />\n </div>\n</template>\n","<script setup lang=\"ts\">\nimport { computed, useId } from 'vue'\n\n/**\n * A label, a hint, an error, and the wiring between them.\n *\n * This was inside `BaseInput`, which is why the kit had one form control\n * instead of five. The hard part of a field is not the `<input>` — it is\n * generating an id, pointing the label at it, deciding whether the description\n * is the hint or the error, and telling assistive tech which one to read. That\n * is identical for a select, a textarea and an input, and every app that\n * needed one of the other two wrote the whole thing again.\n *\n * The control comes in through the slot and is handed what it needs to be\n * described. It is a slot rather than a prop so the field never has to know\n * what it is wrapping.\n *\n * @example\n * ```vue\n * <FormField :label=\"t('profile.name')\" :error=\"errors.name\">\n * <template #default=\"{ id, describedBy, invalid }\">\n * <input :id=\"id\" :aria-describedby=\"describedBy\" :aria-invalid=\"invalid\" />\n * </template>\n * </FormField>\n * ```\n */\nconst {\n label,\n error = '',\n hint = '',\n labelHidden = false,\n fieldId = undefined,\n size = 'md',\n} = defineProps<{\n label: string\n error?: string | undefined\n hint?: string | undefined\n /**\n * `sm` for a control that sits inside something else — a toolbar, a filter\n * row, a settings line — rather than in a form of its own.\n *\n * It exists because every hand-written select in all three apps was the\n * small one, and the kit only had the large one. A part is not reusable if\n * reaching for it costs a size somebody chose on purpose.\n */\n size?: 'sm' | 'md' | undefined\n /**\n * Hides the label visually but keeps it for assistive tech. For fields whose\n * surrounding row already names them — dropping the label entirely would\n * leave the control with no accessible name at all.\n */\n labelHidden?: boolean | undefined\n /**\n * The control's `id`, when the app needs to address it from outside —\n * an `ErrorSummary` linking to the field it is about, a label elsewhere\n * on the page. Generated when it is not given, which is almost always.\n */\n fieldId?: string | undefined\n}>()\n\nconst generated = useId()\nconst id = computed(() => fieldId ?? generated)\nconst errorId = computed(() => `${id.value}-error`)\nconst hintId = computed(() => `${id.value}-hint`)\n\n/* One description at a time, and the error wins. Announcing the hint as well\n buries the reason the field was rejected under advice the reader has already\n had. */\nconst describedBy = computed(() => {\n if (error) return errorId.value\n if (hint) return hintId.value\n return undefined\n})\n</script>\n\n<template>\n <div class=\"flex flex-col\" :class=\"size === 'sm' ? 'gap-1' : 'gap-1.5'\">\n <label\n :for=\"id\"\n class=\"font-medium\"\n :class=\"[\n labelHidden ? 'sr-only' : '',\n size === 'sm' ? 'text-ink-soft text-xs' : 'text-ink text-sm',\n ]\"\n >\n {{ label }}\n </label>\n\n <slot :id=\"id\" :described-by=\"describedBy\" :invalid=\"Boolean(error)\" :size=\"size\" />\n\n <p v-if=\"error\" :id=\"errorId\" class=\"text-negative text-xs\">{{ error }}</p>\n <p v-else-if=\"hint\" :id=\"hintId\" class=\"text-ink-soft text-xs\">{{ hint }}</p>\n </div>\n</template>\n","<script setup lang=\"ts\">\nimport { computed, useId } from 'vue'\n\n/**\n * A label, a hint, an error, and the wiring between them.\n *\n * This was inside `BaseInput`, which is why the kit had one form control\n * instead of five. The hard part of a field is not the `<input>` — it is\n * generating an id, pointing the label at it, deciding whether the description\n * is the hint or the error, and telling assistive tech which one to read. That\n * is identical for a select, a textarea and an input, and every app that\n * needed one of the other two wrote the whole thing again.\n *\n * The control comes in through the slot and is handed what it needs to be\n * described. It is a slot rather than a prop so the field never has to know\n * what it is wrapping.\n *\n * @example\n * ```vue\n * <FormField :label=\"t('profile.name')\" :error=\"errors.name\">\n * <template #default=\"{ id, describedBy, invalid }\">\n * <input :id=\"id\" :aria-describedby=\"describedBy\" :aria-invalid=\"invalid\" />\n * </template>\n * </FormField>\n * ```\n */\nconst {\n label,\n error = '',\n hint = '',\n labelHidden = false,\n fieldId = undefined,\n size = 'md',\n} = defineProps<{\n label: string\n error?: string | undefined\n hint?: string | undefined\n /**\n * `sm` for a control that sits inside something else — a toolbar, a filter\n * row, a settings line — rather than in a form of its own.\n *\n * It exists because every hand-written select in all three apps was the\n * small one, and the kit only had the large one. A part is not reusable if\n * reaching for it costs a size somebody chose on purpose.\n */\n size?: 'sm' | 'md' | undefined\n /**\n * Hides the label visually but keeps it for assistive tech. For fields whose\n * surrounding row already names them — dropping the label entirely would\n * leave the control with no accessible name at all.\n */\n labelHidden?: boolean | undefined\n /**\n * The control's `id`, when the app needs to address it from outside —\n * an `ErrorSummary` linking to the field it is about, a label elsewhere\n * on the page. Generated when it is not given, which is almost always.\n */\n fieldId?: string | undefined\n}>()\n\nconst generated = useId()\nconst id = computed(() => fieldId ?? generated)\nconst errorId = computed(() => `${id.value}-error`)\nconst hintId = computed(() => `${id.value}-hint`)\n\n/* One description at a time, and the error wins. Announcing the hint as well\n buries the reason the field was rejected under advice the reader has already\n had. */\nconst describedBy = computed(() => {\n if (error) return errorId.value\n if (hint) return hintId.value\n return undefined\n})\n</script>\n\n<template>\n <div class=\"flex flex-col\" :class=\"size === 'sm' ? 'gap-1' : 'gap-1.5'\">\n <label\n :for=\"id\"\n class=\"font-medium\"\n :class=\"[\n labelHidden ? 'sr-only' : '',\n size === 'sm' ? 'text-ink-soft text-xs' : 'text-ink text-sm',\n ]\"\n >\n {{ label }}\n </label>\n\n <slot :id=\"id\" :described-by=\"describedBy\" :invalid=\"Boolean(error)\" :size=\"size\" />\n\n <p v-if=\"error\" :id=\"errorId\" class=\"text-negative text-xs\">{{ error }}</p>\n <p v-else-if=\"hint\" :id=\"hintId\" class=\"text-ink-soft text-xs\">{{ hint }}</p>\n </div>\n</template>\n","import { computed, useAttrs } from 'vue'\n\n/**\n * Splits a field's fallthrough attributes in two.\n *\n * The three text fields bind `$attrs` to their inner `<input>`, which is what\n * makes `placeholder`, `autocomplete` and `inputmode` reach the control rather\n * than pile up on a wrapper that cannot use them. `class` and `style` are the\n * exception, and they were going along with the rest: Vue puts them on a\n * component's root element, so `class=\"mt-4\"` reads as \"space this field\" and\n * was landing on the box _inside_ it, under the label.\n */\nexport function useFieldAttrs() {\n const attrs = useAttrs()\n return {\n fieldClass: computed(() => attrs.class),\n fieldStyle: computed(() => attrs.style),\n controlAttrs: computed(() => {\n const { class: _class, style: _style, ...rest } = attrs\n return rest\n }),\n }\n}\n","<script setup lang=\"ts\" generic=\"T extends string | number | undefined = string\">\nimport { computed, useSlots } from 'vue'\nimport type { InputHTMLAttributes } from 'vue'\n\nimport FormField from './FormField.vue'\nimport { useFieldAttrs } from '../composables/use-field-attrs'\nimport { useBoundValue } from '../composables/use-bound-value'\n\ndefineOptions({ inheritAttrs: false })\n\nconst { fieldClass, fieldStyle, controlAttrs } = useFieldAttrs()\n\n/**\n * A text field with its label, hint and error already wired to it.\n *\n * Every attribute of a native input — `placeholder`, `autocomplete`,\n * `inputmode` — goes to the input itself, not to the wrapper around it.\n */\nconst {\n modelValue = undefined,\n label,\n error = '',\n hint = '',\n type = 'text',\n labelHidden = false,\n size = 'md',\n variant = 'default',\n fieldId = undefined,\n} = defineProps<\n {\n /** The value, with `v-model`. Its type is whatever you bind. */\n modelValue?: T | undefined\n label: string\n error?: string | undefined\n hint?: string | undefined\n /**\n * Hides the label visually but keeps it for assistive tech. For fields whose\n * surrounding row already names them — dropping the label entirely would\n * leave the input with no accessible name at all.\n */\n labelHidden?: boolean | undefined\n /**\n * Every type a text field can be, because the ones missing were the ones\n * apps needed: `date` and `search` were hand-written three times each and\n * `url` twice, in files that already imported this component.\n */\n type?:\n | 'text'\n | 'email'\n | 'password'\n | 'number'\n | 'search'\n | 'tel'\n | 'url'\n | 'date'\n | 'time'\n | 'datetime-local'\n | undefined\n /** `sm` for a field inside a row rather than in a form of its own. */\n size?: 'sm' | 'md' | undefined\n /**\n * `unstyled` keeps the wiring and drops the surface.\n *\n * The label, the generated id, `aria-describedby` and the error are what a\n * field is; the border and the height are what it looks like. A search box\n * inside a bordered row, a url field in an editor popover — those places\n * were hand-writing the whole thing to avoid the appearance, and losing the\n * wiring with it.\n *\n * The same reasoning as `BaseButton`'s `unstyled`, and the same test: a\n * primitive is finished when the app can take its behaviour without its\n * paint.\n */\n variant?: 'default' | 'unstyled' | undefined\n /* The input's own attributes — `placeholder`, `autocomplete`, `inputmode` —\n typed for strictTemplates and passed to the input through `$attrs`. Not\n `size` and `type`, which are this component's own. */\n /**\n * Ties the generated id to a name you choose, so `ErrorSummary` can link\n * to this field. Its links point at nothing without it, and the kit's\n * own `FormField` has taken one since 2.22.0 while the fields built on\n * it did not — which left building a form out of these and summarising\n * its errors impossible without hand-writing the control.\n */\n fieldId?: string | undefined\n } & /* @vue-ignore */ Omit<InputHTMLAttributes, 'size' | 'type'>\n>()\n\n/* The control keeps 16px at every size, and that is not a rounding of the\n scale — it is the rule. iOS zooms the viewport when a text field it is\n focusing has a font-size under 16px, and the page never zooms back. Both\n phone consumers had written `input { font-size: 16px }` into their base\n layer to stop exactly this, and a `text-sm` utility from here would have\n overridden it in every app at once.\n\n So `size` reaches the label and the spacing, through FormField, and leaves\n the typing target alone. `BaseSelect` is free to shrink: a select opens a\n native picker rather than a caret, and does not trigger the zoom. */\nconst CONTROL_CLASS = 'h-11 text-base'\n\n/* `unstyled` is \"the wiring without the paint\", so it has no surface to\n attach anything to; an addon there would be the paint coming back. */\nconst slots = useSlots()\n\nconst grouped = computed(() => variant !== 'unstyled' && Boolean(slots.prefix || slots.suffix))\n\n/**\n * A number field's value is a number.\n *\n * Typed to `string` alone, `type=\"number\"` forced the caller to keep a string\n * ref and convert on both sides of it — and a component you have to wrap in\n * order to use is one you write yourself instead, which is exactly what the\n * first numeric field tried to reach for it did.\n *\n * Generic rather than `string | number`, so the value is whatever the app\n * bound: a `ref('')` gets strings back and a `ref(0)` numbers, and a\n * `string | undefined` from a form library is accepted as it is.\n */\n/**\n * Something attached to the field: a currency mark, a unit, a button.\n *\n * Both of these were written by hand inside this kit before they were a\n * slot — `PasswordInput` positions a toggle over the field and `NumberInput`\n * two steppers — and an app that wanted \"€\" in front of an amount had to\n * rebuild the whole field to get it, losing the label wiring and\n * `aria-describedby` on the way. That is the gap test, with the kit itself\n * as the app.\n *\n * Given either slot, the border and the ground move to a wrapper and the\n * input goes transparent inside it, so the edge encloses the addon whatever\n * width it is. With neither, the field renders exactly as it did before the\n * slots existed.\n *\n * **What goes in them is yours, including whether it is read out.** A `€`\n * beside a field labelled \"Amount\" is decoration and belongs behind\n * `aria-hidden`; a unit that is the only place \"kilograms\" appears is not,\n * and belongs in the label instead. The kit cannot tell which it is.\n */\ndefineSlots<{\n prefix?: () => unknown\n suffix?: () => unknown\n}>()\n\nconst emit = defineEmits<{ 'update:modelValue': [value: T] }>()\n// The destructured prop is typed by T's constraint, not by T itself — a\n// limit of destructuring in a generic component — so it is narrowed here.\nconst model = useBoundValue<T>(\n () => modelValue as T | undefined,\n (value) => emit('update:modelValue', value),\n)\n</script>\n\n<template>\n <FormField\n :field-id=\"fieldId\"\n :class=\"fieldClass\"\n :style=\"fieldStyle\"\n :label=\"label\"\n :error=\"error\"\n :hint=\"hint\"\n :label-hidden=\"labelHidden\"\n :size=\"size\"\n >\n <template #default=\"{ id, describedBy, invalid }\">\n <!-- The grouped shape: the wrapper is the control, the input is bare\n inside it. `has-[:focus-visible]` rather than `focus-within` so a\n mouse press does not ring the whole group, which is how the rest\n of the kit behaves. -->\n <div\n v-if=\"grouped\"\n class=\"control rounded-card has-focus-visible:outline-primary flex items-stretch overflow-hidden has-focus-visible:outline-2 has-focus-visible:outline-offset-1\"\n :class=\"[CONTROL_CLASS, invalid ? 'border-negative' : '']\"\n >\n <span v-if=\"$slots.prefix\" class=\"text-ink-soft flex shrink-0 items-center ps-3\">\n <slot name=\"prefix\" />\n </span>\n\n <input\n :id=\"id\"\n v-model=\"model\"\n :type=\"type\"\n :aria-invalid=\"invalid\"\n :aria-describedby=\"describedBy\"\n v-bind=\"controlAttrs\"\n class=\"text-ink min-w-0 flex-1 bg-transparent px-3 text-base focus-visible:outline-none\"\n />\n\n <span v-if=\"$slots.suffix\" class=\"text-ink-soft flex shrink-0 items-center pe-3\">\n <slot name=\"suffix\" />\n </span>\n </div>\n\n <input\n v-else\n :id=\"id\"\n v-model=\"model\"\n :type=\"type\"\n :aria-invalid=\"invalid\"\n :aria-describedby=\"describedBy\"\n v-bind=\"controlAttrs\"\n :class=\"[\n variant === 'unstyled'\n ? ''\n : 'control text-ink rounded-card focus-visible:outline-primary px-3 focus-visible:outline-2 focus-visible:outline-offset-1',\n variant === 'unstyled' ? 'text-base' : CONTROL_CLASS,\n variant !== 'unstyled' && invalid ? 'border-negative' : '',\n ]\"\n />\n </template>\n </FormField>\n</template>\n","<script setup lang=\"ts\" generic=\"T extends string | number | undefined = string\">\nimport { computed, useSlots } from 'vue'\nimport type { InputHTMLAttributes } from 'vue'\n\nimport FormField from './FormField.vue'\nimport { useFieldAttrs } from '../composables/use-field-attrs'\nimport { useBoundValue } from '../composables/use-bound-value'\n\ndefineOptions({ inheritAttrs: false })\n\nconst { fieldClass, fieldStyle, controlAttrs } = useFieldAttrs()\n\n/**\n * A text field with its label, hint and error already wired to it.\n *\n * Every attribute of a native input — `placeholder`, `autocomplete`,\n * `inputmode` — goes to the input itself, not to the wrapper around it.\n */\nconst {\n modelValue = undefined,\n label,\n error = '',\n hint = '',\n type = 'text',\n labelHidden = false,\n size = 'md',\n variant = 'default',\n fieldId = undefined,\n} = defineProps<\n {\n /** The value, with `v-model`. Its type is whatever you bind. */\n modelValue?: T | undefined\n label: string\n error?: string | undefined\n hint?: string | undefined\n /**\n * Hides the label visually but keeps it for assistive tech. For fields whose\n * surrounding row already names them — dropping the label entirely would\n * leave the input with no accessible name at all.\n */\n labelHidden?: boolean | undefined\n /**\n * Every type a text field can be, because the ones missing were the ones\n * apps needed: `date` and `search` were hand-written three times each and\n * `url` twice, in files that already imported this component.\n */\n type?:\n | 'text'\n | 'email'\n | 'password'\n | 'number'\n | 'search'\n | 'tel'\n | 'url'\n | 'date'\n | 'time'\n | 'datetime-local'\n | undefined\n /** `sm` for a field inside a row rather than in a form of its own. */\n size?: 'sm' | 'md' | undefined\n /**\n * `unstyled` keeps the wiring and drops the surface.\n *\n * The label, the generated id, `aria-describedby` and the error are what a\n * field is; the border and the height are what it looks like. A search box\n * inside a bordered row, a url field in an editor popover — those places\n * were hand-writing the whole thing to avoid the appearance, and losing the\n * wiring with it.\n *\n * The same reasoning as `BaseButton`'s `unstyled`, and the same test: a\n * primitive is finished when the app can take its behaviour without its\n * paint.\n */\n variant?: 'default' | 'unstyled' | undefined\n /* The input's own attributes — `placeholder`, `autocomplete`, `inputmode` —\n typed for strictTemplates and passed to the input through `$attrs`. Not\n `size` and `type`, which are this component's own. */\n /**\n * Ties the generated id to a name you choose, so `ErrorSummary` can link\n * to this field. Its links point at nothing without it, and the kit's\n * own `FormField` has taken one since 2.22.0 while the fields built on\n * it did not — which left building a form out of these and summarising\n * its errors impossible without hand-writing the control.\n */\n fieldId?: string | undefined\n } & /* @vue-ignore */ Omit<InputHTMLAttributes, 'size' | 'type'>\n>()\n\n/* The control keeps 16px at every size, and that is not a rounding of the\n scale — it is the rule. iOS zooms the viewport when a text field it is\n focusing has a font-size under 16px, and the page never zooms back. Both\n phone consumers had written `input { font-size: 16px }` into their base\n layer to stop exactly this, and a `text-sm` utility from here would have\n overridden it in every app at once.\n\n So `size` reaches the label and the spacing, through FormField, and leaves\n the typing target alone. `BaseSelect` is free to shrink: a select opens a\n native picker rather than a caret, and does not trigger the zoom. */\nconst CONTROL_CLASS = 'h-11 text-base'\n\n/* `unstyled` is \"the wiring without the paint\", so it has no surface to\n attach anything to; an addon there would be the paint coming back. */\nconst slots = useSlots()\n\nconst grouped = computed(() => variant !== 'unstyled' && Boolean(slots.prefix || slots.suffix))\n\n/**\n * A number field's value is a number.\n *\n * Typed to `string` alone, `type=\"number\"` forced the caller to keep a string\n * ref and convert on both sides of it — and a component you have to wrap in\n * order to use is one you write yourself instead, which is exactly what the\n * first numeric field tried to reach for it did.\n *\n * Generic rather than `string | number`, so the value is whatever the app\n * bound: a `ref('')` gets strings back and a `ref(0)` numbers, and a\n * `string | undefined` from a form library is accepted as it is.\n */\n/**\n * Something attached to the field: a currency mark, a unit, a button.\n *\n * Both of these were written by hand inside this kit before they were a\n * slot — `PasswordInput` positions a toggle over the field and `NumberInput`\n * two steppers — and an app that wanted \"€\" in front of an amount had to\n * rebuild the whole field to get it, losing the label wiring and\n * `aria-describedby` on the way. That is the gap test, with the kit itself\n * as the app.\n *\n * Given either slot, the border and the ground move to a wrapper and the\n * input goes transparent inside it, so the edge encloses the addon whatever\n * width it is. With neither, the field renders exactly as it did before the\n * slots existed.\n *\n * **What goes in them is yours, including whether it is read out.** A `€`\n * beside a field labelled \"Amount\" is decoration and belongs behind\n * `aria-hidden`; a unit that is the only place \"kilograms\" appears is not,\n * and belongs in the label instead. The kit cannot tell which it is.\n */\ndefineSlots<{\n prefix?: () => unknown\n suffix?: () => unknown\n}>()\n\nconst emit = defineEmits<{ 'update:modelValue': [value: T] }>()\n// The destructured prop is typed by T's constraint, not by T itself — a\n// limit of destructuring in a generic component — so it is narrowed here.\nconst model = useBoundValue<T>(\n () => modelValue as T | undefined,\n (value) => emit('update:modelValue', value),\n)\n</script>\n\n<template>\n <FormField\n :field-id=\"fieldId\"\n :class=\"fieldClass\"\n :style=\"fieldStyle\"\n :label=\"label\"\n :error=\"error\"\n :hint=\"hint\"\n :label-hidden=\"labelHidden\"\n :size=\"size\"\n >\n <template #default=\"{ id, describedBy, invalid }\">\n <!-- The grouped shape: the wrapper is the control, the input is bare\n inside it. `has-[:focus-visible]` rather than `focus-within` so a\n mouse press does not ring the whole group, which is how the rest\n of the kit behaves. -->\n <div\n v-if=\"grouped\"\n class=\"control rounded-card has-focus-visible:outline-primary flex items-stretch overflow-hidden has-focus-visible:outline-2 has-focus-visible:outline-offset-1\"\n :class=\"[CONTROL_CLASS, invalid ? 'border-negative' : '']\"\n >\n <span v-if=\"$slots.prefix\" class=\"text-ink-soft flex shrink-0 items-center ps-3\">\n <slot name=\"prefix\" />\n </span>\n\n <input\n :id=\"id\"\n v-model=\"model\"\n :type=\"type\"\n :aria-invalid=\"invalid\"\n :aria-describedby=\"describedBy\"\n v-bind=\"controlAttrs\"\n class=\"text-ink min-w-0 flex-1 bg-transparent px-3 text-base focus-visible:outline-none\"\n />\n\n <span v-if=\"$slots.suffix\" class=\"text-ink-soft flex shrink-0 items-center pe-3\">\n <slot name=\"suffix\" />\n </span>\n </div>\n\n <input\n v-else\n :id=\"id\"\n v-model=\"model\"\n :type=\"type\"\n :aria-invalid=\"invalid\"\n :aria-describedby=\"describedBy\"\n v-bind=\"controlAttrs\"\n :class=\"[\n variant === 'unstyled'\n ? ''\n : 'control text-ink rounded-card focus-visible:outline-primary px-3 focus-visible:outline-2 focus-visible:outline-offset-1',\n variant === 'unstyled' ? 'text-base' : CONTROL_CLASS,\n variant !== 'unstyled' && invalid ? 'border-negative' : '',\n ]\"\n />\n </template>\n </FormField>\n</template>\n","<script setup lang=\"ts\">\n/**\n * Sign in with Google, in Google's own clothes.\n *\n * The mark is inlined rather than themed, and that is not an oversight: Google's\n * brand guidelines require their colours, and a reader scanning for the button\n * they already know is scanning for those exact four. It is the one component\n * here that carries a colour value, and the reason is written on the path.\n */\nconst { label, disabled = false } = defineProps<{\n label: string\n /**\n * Greyed out and unclickable while a sign-in is already in flight.\n *\n * A real prop rather than a fallthrough attribute: the button needs to look\n * disabled as well as be it, and a bare `disabled` landing on the element by\n * itself gets the second half only.\n */\n disabled?: boolean | undefined\n}>()\n\nconst emit = defineEmits<{ click: [] }>()\n</script>\n\n<template>\n <button\n type=\"button\"\n :disabled=\"disabled\"\n class=\"control text-ink rounded-card hover:bg-muted focus-ring flex h-11 w-full items-center justify-center gap-2 text-sm font-medium transition-colors duration-(--duration-fast) active:scale-(--press-scale) disabled:pointer-events-none disabled:opacity-50\"\n @click=\"emit('click')\"\n >\n <!-- Google asks for its own mark, so it is inlined rather than themed. -->\n <svg class=\"size-4\" viewBox=\"0 0 48 48\" aria-hidden=\"true\">\n <path\n fill=\"#EA4335\"\n d=\"M24 9.5c3.5 0 6.6 1.2 9 3.6l6.7-6.7C35.6 2.7 30.2.5 24 .5 14.6.5 6.5 5.9 2.6 13.7l7.8 6.1C12.3 13.7 17.7 9.5 24 9.5z\"\n />\n <path\n fill=\"#4285F4\"\n d=\"M46.5 24.5c0-1.6-.1-3.1-.4-4.5H24v9h12.7c-.6 3-2.3 5.6-4.9 7.3l7.6 5.9c4.4-4.1 7.1-10.2 7.1-17.7z\"\n />\n <path\n fill=\"#FBBC05\"\n d=\"M10.4 28.2a14.6 14.6 0 0 1 0-8.4l-7.8-6.1a24 24 0 0 0 0 20.6l7.8-6.1z\"\n />\n <path\n fill=\"#34A853\"\n d=\"M24 47.5c6.2 0 11.5-2 15.4-5.6l-7.6-5.9c-2.1 1.4-4.8 2.3-7.8 2.3-6.3 0-11.7-4.2-13.6-10l-7.8 6.1C6.5 42.1 14.6 47.5 24 47.5z\"\n />\n </svg>\n {{ label }}\n </button>\n</template>\n","<script setup lang=\"ts\">\n/**\n * Sign in with Google, in Google's own clothes.\n *\n * The mark is inlined rather than themed, and that is not an oversight: Google's\n * brand guidelines require their colours, and a reader scanning for the button\n * they already know is scanning for those exact four. It is the one component\n * here that carries a colour value, and the reason is written on the path.\n */\nconst { label, disabled = false } = defineProps<{\n label: string\n /**\n * Greyed out and unclickable while a sign-in is already in flight.\n *\n * A real prop rather than a fallthrough attribute: the button needs to look\n * disabled as well as be it, and a bare `disabled` landing on the element by\n * itself gets the second half only.\n */\n disabled?: boolean | undefined\n}>()\n\nconst emit = defineEmits<{ click: [] }>()\n</script>\n\n<template>\n <button\n type=\"button\"\n :disabled=\"disabled\"\n class=\"control text-ink rounded-card hover:bg-muted focus-ring flex h-11 w-full items-center justify-center gap-2 text-sm font-medium transition-colors duration-(--duration-fast) active:scale-(--press-scale) disabled:pointer-events-none disabled:opacity-50\"\n @click=\"emit('click')\"\n >\n <!-- Google asks for its own mark, so it is inlined rather than themed. -->\n <svg class=\"size-4\" viewBox=\"0 0 48 48\" aria-hidden=\"true\">\n <path\n fill=\"#EA4335\"\n d=\"M24 9.5c3.5 0 6.6 1.2 9 3.6l6.7-6.7C35.6 2.7 30.2.5 24 .5 14.6.5 6.5 5.9 2.6 13.7l7.8 6.1C12.3 13.7 17.7 9.5 24 9.5z\"\n />\n <path\n fill=\"#4285F4\"\n d=\"M46.5 24.5c0-1.6-.1-3.1-.4-4.5H24v9h12.7c-.6 3-2.3 5.6-4.9 7.3l7.6 5.9c4.4-4.1 7.1-10.2 7.1-17.7z\"\n />\n <path\n fill=\"#FBBC05\"\n d=\"M10.4 28.2a14.6 14.6 0 0 1 0-8.4l-7.8-6.1a24 24 0 0 0 0 20.6l7.8-6.1z\"\n />\n <path\n fill=\"#34A853\"\n d=\"M24 47.5c6.2 0 11.5-2 15.4-5.6l-7.6-5.9c-2.1 1.4-4.8 2.3-7.8 2.3-6.3 0-11.7-4.2-13.6-10l-7.8 6.1C6.5 42.1 14.6 47.5 24 47.5z\"\n />\n </svg>\n {{ label }}\n </button>\n</template>\n","import { nextTick, readonly, ref } from 'vue'\n\n/**\n * Saying something to a screen reader when nothing on screen has changed\n * enough to say it.\n *\n * The kit already has a live region in a dozen places — `ToastHost`,\n * `BaseSpinner`, `OfflineBanner`, `CopyButton` — but every one of them\n * belongs to a component that happens to be on screen. What has no home is\n * everything else: a filter that narrows a list to three results, a route\n * that changed under a client-side navigation, a draft that saved itself.\n * On screen all of those are obvious. To a reader they are silence.\n *\n * ── Three things this gets right that a hand-written region does not ──\n *\n * **The region exists before the text does.** A live region inserted into\n * the page together with its first message is usually not announced at\n * all: the reader has nothing to notice a change *to*. So `AnnounceHost`\n * renders both regions empty, from mount, and only the text inside them\n * ever changes. This is the single most common reason a hand-rolled\n * announcement does nothing, and it fails silently on every platform.\n *\n * **The same sentence twice is still said twice.** \"3 results\" after\n * \"3 results\" is not a mutation, so a reader says nothing — and the reader\n * has no idea the filter did anything. The text is cleared and set again\n * on the next tick, which is a change either way.\n *\n * **Polite by default.** `assertive` interrupts whatever is being read,\n * mid-word. That is right for \"connection lost\" and rude for \"saved\", and\n * a region that is always assertive trains the reader to resent it.\n *\n * ── What is deliberately not here ──\n *\n * **No text of its own.** Like everything else in the kit, the sentence is\n * the app's.\n *\n * **Not for anything on screen.** If there is a visible message, it should\n * be the live region — `BaseAlert`, `FormField`, a toast. This is only for\n * what a sighted reader learns from the screen itself.\n *\n * **A singleton, on purpose.** Two hosts would be two regions, and a\n * message would be read twice.\n *\n * @example\n * ```ts\n * const { announce } = useAnnounce()\n * watch(results, (rows) => announce(t('search.found', { count: rows.length })))\n * ```\n */\nexport interface AnnounceOptions {\n /**\n * Interrupt whatever is being read. For something the reader has to know\n * now — a connection lost, a session about to expire — and nothing else.\n */\n assertive?: boolean | undefined\n}\n\nconst polite = ref('')\nconst assertive = ref('')\n\n/** Long enough to be read, short enough that a reader landing in the region\n * later does not find a sentence about something that has since changed. */\nconst CLEAR_AFTER_MS = 7000\n\nlet timer: ReturnType<typeof setTimeout> | undefined\n\n/**\n * Says something once. Render one `AnnounceHost` in the app for it to reach a\n * reader at all.\n */\nexport function announce(message: string, { assertive: urgent = false }: AnnounceOptions = {}) {\n const target = urgent ? assertive : polite\n\n if (timer !== undefined) clearTimeout(timer)\n\n /* Cleared first, then set on the next tick. Setting the same string again\n is not a change, and a reader announces changes — so without this, the\n second \"3 results\" is silence. */\n polite.value = ''\n assertive.value = ''\n\n void nextTick(() => {\n target.value = message\n\n timer = setTimeout(() => {\n target.value = ''\n timer = undefined\n }, CLEAR_AFTER_MS)\n })\n}\n\nexport function useAnnounce() {\n return {\n announce,\n /** What the regions are saying. `AnnounceHost` renders these; apps read them in tests. */\n polite: readonly(polite),\n assertive: readonly(assertive),\n }\n}\n"],"mappings":"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"}