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.
- package/README.md +4 -4
- package/dist/app/TourShell.vue.d.ts +2 -0
- package/dist/app/index.d.ts +96 -6
- package/dist/app.js +2 -2
- package/dist/app.js.map +1 -1
- package/dist/components/BaseDatePicker.vue.d.ts +8 -0
- package/dist/components/BaseInput.vue.d.ts +8 -0
- package/dist/components/BaseSelect.vue.d.ts +8 -0
- package/dist/components/BaseTextarea.vue.d.ts +8 -0
- package/dist/components/ColorPicker.vue.d.ts +8 -0
- package/dist/components/NumberInput.vue.d.ts +8 -0
- package/dist/components/SectionHeading.vue.d.ts +6 -0
- package/dist/components/StatCard.vue.d.ts +5 -0
- package/dist/components/TagsInput.vue.d.ts +8 -0
- package/dist/components/TimePicker.vue.d.ts +8 -0
- package/dist/index.d.ts +913 -56
- package/dist/index.js +35 -14
- package/dist/index.js.map +1 -1
- package/dist/motion/index.d.ts +47 -3
- package/dist/pwa/index.d.ts +42 -2
- package/dist/styles.css +56 -56
- package/dist/{use-announce-QN44Whqc.js → use-announce-D8SKsiqn.js} +5 -2
- package/dist/use-announce-D8SKsiqn.js.map +1 -0
- package/dist/web/index.d.ts +308 -7
- package/package.json +2 -1
- package/dist/use-announce-QN44Whqc.js.map +0 -1
package/dist/motion/index.d.ts
CHANGED
|
@@ -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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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';
|
package/dist/pwa/index.d.ts
CHANGED
|
@@ -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
|
-
/**
|
|
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
|
-
/**
|
|
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-
|
|
213
|
-
.tour-leave-active[data-v-
|
|
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-
|
|
217
|
-
.tour-leave-to[data-v-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
780
|
+
.rk-number.is-disabled[data-v-d8a8fb7e] {
|
|
781
781
|
opacity: 0.5;
|
|
782
782
|
}
|
|
783
|
-
.rk-number-field[data-v-
|
|
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-
|
|
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-
|
|
803
|
+
.rk-number-step[data-v-d8a8fb7e]:hover:not(:disabled) {
|
|
804
804
|
background: var(--color-muted);
|
|
805
805
|
}
|
|
806
|
-
.rk-number-step[data-v-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
1806
|
+
.rk-color-native[data-v-721fcae8]:disabled {
|
|
1807
1807
|
cursor: not-allowed;
|
|
1808
1808
|
}
|
|
1809
|
-
.rk-color-hex[data-v-
|
|
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-
|
|
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-
|
|
1824
|
+
.rk-color-hex[data-v-721fcae8]:disabled {
|
|
1825
1825
|
opacity: 0.5;
|
|
1826
1826
|
}
|
|
1827
|
-
.rk-color-swatches[data-v-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
1856
|
+
.rk-color-swatch[data-v-721fcae8] {
|
|
1857
1857
|
transition: none;
|
|
1858
1858
|
}
|
|
1859
|
-
.rk-color-swatch[data-v-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
2141
|
+
.rk-time[data-v-2ae98769] {
|
|
2142
2142
|
display: block;
|
|
2143
2143
|
}
|
|
2144
|
-
.rk-time-field[data-v-
|
|
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-
|
|
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-
|
|
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-
|
|
2163
|
+
.rk-time-field[data-v-2ae98769]:disabled {
|
|
2164
2164
|
opacity: 0.5;
|
|
2165
2165
|
}
|
|
2166
|
-
.rk-time-columns[data-v-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
2271
|
+
.rk-date-field[data-v-2c91e3bb]:disabled {
|
|
2272
2272
|
opacity: 0.5;
|
|
2273
2273
|
}
|
|
2274
|
-
.rk-date-clear[data-v-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
2300
|
+
.rk-date-panel[data-v-2c91e3bb] .rk-cal {
|
|
2301
2301
|
width: 17rem;
|
|
2302
2302
|
}
|
|
2303
|
-
.rk-date-presets[data-v-
|
|
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-
|
|
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-
|
|
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-
|
|
2324
|
+
.rk-date-panel[data-v-2c91e3bb] {
|
|
2325
2325
|
flex-direction: column;
|
|
2326
2326
|
}
|
|
2327
|
-
.rk-date-presets[data-v-
|
|
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-
|
|
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-
|
|
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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"}
|