@eifi1/ui-kit 0.21.0 → 0.22.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.
Files changed (278) hide show
  1. package/dist/chart.d.ts +10 -8
  2. package/dist/components/account-chips.d.ts +10 -3
  3. package/dist/components/account-chips.js +5 -2
  4. package/dist/components/account-chips.js.map +1 -1
  5. package/dist/components/account-settings.d.ts +33 -2
  6. package/dist/components/account-settings.js +43 -21
  7. package/dist/components/account-settings.js.map +1 -1
  8. package/dist/components/amount-input.d.ts +27 -8
  9. package/dist/components/amount-input.js +15 -28
  10. package/dist/components/amount-input.js.map +1 -1
  11. package/dist/components/autocomplete.d.ts +8 -0
  12. package/dist/components/autocomplete.js +9 -4
  13. package/dist/components/autocomplete.js.map +1 -1
  14. package/dist/components/button-group.d.ts +10 -8
  15. package/dist/components/calculator.d.ts +10 -8
  16. package/dist/components/checkbox-group.d.ts +104 -0
  17. package/dist/components/checkbox-group.js +111 -0
  18. package/dist/components/checkbox-group.js.map +1 -0
  19. package/dist/components/checkbox.d.ts +19 -0
  20. package/dist/components/checkbox.js +30 -7
  21. package/dist/components/checkbox.js.map +1 -1
  22. package/dist/components/chip.d.ts +18 -0
  23. package/dist/components/chip.js +47 -10
  24. package/dist/components/chip.js.map +1 -1
  25. package/dist/components/combobox.d.ts +13 -2
  26. package/dist/components/combobox.js +17 -8
  27. package/dist/components/combobox.js.map +1 -1
  28. package/dist/components/confirm-dialog.d.ts +66 -161
  29. package/dist/components/copy-button.d.ts +10 -8
  30. package/dist/components/country-select.d.ts +124 -0
  31. package/dist/components/country-select.js +237 -0
  32. package/dist/components/country-select.js.map +1 -0
  33. package/dist/components/currency-select.d.ts +17 -1
  34. package/dist/components/currency-select.js +56 -23
  35. package/dist/components/currency-select.js.map +1 -1
  36. package/dist/components/danger-confirm.d.ts +66 -166
  37. package/dist/components/danger-confirm.js +133 -49
  38. package/dist/components/danger-confirm.js.map +1 -1
  39. package/dist/components/data-table-cells.d.ts +1 -2
  40. package/dist/components/data-table-filter-popover.d.ts +1 -2
  41. package/dist/components/data-table-filters.d.ts +1 -2
  42. package/dist/components/data-table-labels.d.ts +4 -97
  43. package/dist/components/data-table-labels.js +9 -1
  44. package/dist/components/data-table-labels.js.map +1 -1
  45. package/dist/components/data-table-pagination.d.ts +3 -1
  46. package/dist/components/data-table.d.ts +1 -2
  47. package/dist/components/data-table.js +98 -3
  48. package/dist/components/data-table.js.map +1 -1
  49. package/dist/components/date-picker.d.ts +25 -5
  50. package/dist/components/date-picker.js +110 -59
  51. package/dist/components/date-picker.js.map +1 -1
  52. package/dist/components/entity-combobox.d.ts +8 -1
  53. package/dist/components/entity-combobox.js +9 -4
  54. package/dist/components/entity-combobox.js.map +1 -1
  55. package/dist/components/facing-pair.d.ts +10 -8
  56. package/dist/components/field-anatomy.d.ts +95 -0
  57. package/dist/components/field-anatomy.js +84 -0
  58. package/dist/components/field-anatomy.js.map +1 -0
  59. package/dist/components/field-parts.d.ts +114 -0
  60. package/dist/components/field-parts.js +72 -0
  61. package/dist/components/field-parts.js.map +1 -0
  62. package/dist/components/file-button.d.ts +10 -8
  63. package/dist/components/file-button.js +13 -3
  64. package/dist/components/file-button.js.map +1 -1
  65. package/dist/components/file-dropzone.d.ts +32 -9
  66. package/dist/components/file-dropzone.js +57 -34
  67. package/dist/components/file-dropzone.js.map +1 -1
  68. package/dist/components/form-actions.d.ts +10 -8
  69. package/dist/components/form-actions.js +61 -3
  70. package/dist/components/form-actions.js.map +1 -1
  71. package/dist/components/iban-input.d.ts +66 -0
  72. package/dist/components/iban-input.js +125 -0
  73. package/dist/components/iban-input.js.map +1 -0
  74. package/dist/components/icon-picker.d.ts +12 -2
  75. package/dist/components/icon-picker.js +9 -2
  76. package/dist/components/icon-picker.js.map +1 -1
  77. package/dist/components/inline-edit-field.d.ts +167 -0
  78. package/dist/components/inline-edit-field.js +293 -0
  79. package/dist/components/inline-edit-field.js.map +1 -0
  80. package/dist/components/language-select.d.ts +127 -0
  81. package/dist/components/language-select.js +18 -0
  82. package/dist/components/language-select.js.map +1 -0
  83. package/dist/components/line-items.d.ts +25 -3
  84. package/dist/components/line-items.js +55 -46
  85. package/dist/components/line-items.js.map +1 -1
  86. package/dist/components/list.d.ts +25 -0
  87. package/dist/components/list.js +16 -5
  88. package/dist/components/list.js.map +1 -1
  89. package/dist/components/money-field.d.ts +20 -8
  90. package/dist/components/money-field.js.map +1 -1
  91. package/dist/components/month-picker.d.ts +52 -4
  92. package/dist/components/month-picker.js +290 -86
  93. package/dist/components/month-picker.js.map +1 -1
  94. package/dist/components/multi-select.d.ts +8 -1
  95. package/dist/components/multi-select.js +9 -4
  96. package/dist/components/multi-select.js.map +1 -1
  97. package/dist/components/number-field.d.ts +25 -13
  98. package/dist/components/number-field.js +6 -20
  99. package/dist/components/number-field.js.map +1 -1
  100. package/dist/components/number-input.d.ts +70 -13
  101. package/dist/components/number-input.js +90 -38
  102. package/dist/components/number-input.js.map +1 -1
  103. package/dist/components/numpad-sheet.d.ts +10 -8
  104. package/dist/components/one-time-code-input.d.ts +100 -0
  105. package/dist/components/one-time-code-input.js +137 -0
  106. package/dist/components/one-time-code-input.js.map +1 -0
  107. package/dist/components/phone-input.d.ts +66 -0
  108. package/dist/components/phone-input.js +171 -0
  109. package/dist/components/phone-input.js.map +1 -0
  110. package/dist/components/series-chart.d.ts +10 -8
  111. package/dist/components/settings-fields.d.ts +10 -8
  112. package/dist/components/share-card.d.ts +8 -6
  113. package/dist/components/sign-chip.d.ts +66 -0
  114. package/dist/components/sign-chip.js +39 -0
  115. package/dist/components/sign-chip.js.map +1 -0
  116. package/dist/components/swatch-picker.d.ts +27 -2
  117. package/dist/components/swatch-picker.js +73 -31
  118. package/dist/components/swatch-picker.js.map +1 -1
  119. package/dist/components/switch.d.ts +19 -0
  120. package/dist/components/switch.js +24 -6
  121. package/dist/components/switch.js.map +1 -1
  122. package/dist/components/text-link.d.ts +10 -8
  123. package/dist/components/tile-radio.d.ts +68 -13
  124. package/dist/components/tile-radio.js +3 -3
  125. package/dist/components/tile-radio.js.map +1 -1
  126. package/dist/components/time-input.d.ts +15 -9
  127. package/dist/components/time-input.js.map +1 -1
  128. package/dist/components/toggle-group.d.ts +47 -1
  129. package/dist/components/toggle-group.js +110 -69
  130. package/dist/components/toggle-group.js.map +1 -1
  131. package/dist/components/ui.d.ts +10 -8
  132. package/dist/components/ui.js +280 -78
  133. package/dist/components/ui.js.map +1 -1
  134. package/dist/components/use-table-state.d.ts +1 -2
  135. package/dist/{data-table-3mi2R3so.d.ts → data-table-labels-B7OdnM0S.d.ts} +140 -3
  136. package/dist/data-table.d.ts +1 -2
  137. package/dist/data-table.js +2 -1
  138. package/dist/data-table.js.map +1 -1
  139. package/dist/feedback/feedback-attachment.d.ts +1 -1
  140. package/dist/feedback/feedback-attachment.js +210 -3
  141. package/dist/feedback/feedback-attachment.js.map +1 -1
  142. package/dist/feedback/feedback-dialog.d.ts +1 -1
  143. package/dist/feedback/feedback-dialog.js.map +1 -1
  144. package/dist/feedback/feedback-inbox.d.ts +1 -1
  145. package/dist/feedback/feedback-thread.d.ts +37 -3
  146. package/dist/feedback/feedback-thread.js +38 -9
  147. package/dist/feedback/feedback-thread.js.map +1 -1
  148. package/dist/{feedback-attachment-WFiXJ8We.d.ts → feedback-attachment-NhUm7Zga.d.ts} +70 -5
  149. package/dist/feedback.d.ts +2 -2
  150. package/dist/hooks/use-file-drop.d.ts +10 -8
  151. package/dist/i18n/defaults.d.ts +10 -8
  152. package/dist/i18n/defaults.js +17 -2
  153. package/dist/i18n/defaults.js.map +1 -1
  154. package/dist/i18n/german.d.ts +10 -8
  155. package/dist/i18n/german.js +50 -5
  156. package/dist/i18n/german.js.map +1 -1
  157. package/dist/i18n/kit-labels.d.ts +10 -8
  158. package/dist/i18n/kit-labels.js +8 -2
  159. package/dist/i18n/kit-labels.js.map +1 -1
  160. package/dist/i18n/languages.d.ts +10 -8
  161. package/dist/i18n/locales/de-CH.d.ts +10 -8
  162. package/dist/i18n/locales/en.d.ts +10 -8
  163. package/dist/i18n/locales/en.js +9 -0
  164. package/dist/i18n/locales/en.js.map +1 -1
  165. package/dist/i18n/locales/es.d.ts +10 -8
  166. package/dist/i18n/locales/es.js +48 -5
  167. package/dist/i18n/locales/es.js.map +1 -1
  168. package/dist/i18n/locales/fr.d.ts +10 -8
  169. package/dist/i18n/locales/fr.js +48 -5
  170. package/dist/i18n/locales/fr.js.map +1 -1
  171. package/dist/i18n/locales/hu.d.ts +10 -8
  172. package/dist/i18n/locales/hu.js +49 -5
  173. package/dist/i18n/locales/hu.js.map +1 -1
  174. package/dist/i18n/locales/it.d.ts +10 -8
  175. package/dist/i18n/locales/it.js +49 -5
  176. package/dist/i18n/locales/it.js.map +1 -1
  177. package/dist/i18n/locales/zh.d.ts +10 -8
  178. package/dist/i18n/locales/zh.js +48 -5
  179. package/dist/i18n/locales/zh.js.map +1 -1
  180. package/dist/i18n/review.d.ts +10 -8
  181. package/dist/i18n/review.js +8 -1
  182. package/dist/i18n/review.js.map +1 -1
  183. package/dist/index.d.ts +17 -10
  184. package/dist/index.js +57 -2
  185. package/dist/index.js.map +1 -1
  186. package/dist/{kit-labels-D1J2_jyD.d.ts → kit-labels-R8mIAc9N.d.ts} +811 -8
  187. package/dist/lib/countries.d.ts +43 -0
  188. package/dist/lib/countries.js +39 -0
  189. package/dist/lib/countries.js.map +1 -0
  190. package/dist/lib/decimal-marks.d.ts +46 -0
  191. package/dist/lib/decimal-marks.js +40 -0
  192. package/dist/lib/decimal-marks.js.map +1 -0
  193. package/dist/lib/iban.d.ts +98 -0
  194. package/dist/lib/iban.js +154 -0
  195. package/dist/lib/iban.js.map +1 -0
  196. package/dist/lib/isin.d.ts +30 -0
  197. package/dist/lib/isin.js +36 -0
  198. package/dist/lib/isin.js.map +1 -0
  199. package/dist/lib/phone.d.ts +69 -0
  200. package/dist/lib/phone.js +121 -0
  201. package/dist/lib/phone.js.map +1 -0
  202. package/dist/rhf/fields.d.ts +148 -11
  203. package/dist/rhf/fields.js +332 -3
  204. package/dist/rhf/fields.js.map +1 -1
  205. package/dist/rhf/form.d.ts +10 -8
  206. package/dist/rhf.d.ts +13 -9
  207. package/dist/rhf.js.map +1 -1
  208. package/dist/shell/app-shell.d.ts +10 -8
  209. package/dist/shell/top-bar-brand.d.ts +10 -8
  210. package/dist/shell.d.ts +10 -8
  211. package/dist/tour/tour.js +20 -24
  212. package/dist/tour/tour.js.map +1 -1
  213. package/dist/wizard/stepper-nav.d.ts +10 -8
  214. package/dist/wizard.d.ts +10 -8
  215. package/package.json +1 -1
  216. package/src/components/account-chips.tsx +17 -3
  217. package/src/components/account-settings.tsx +92 -26
  218. package/src/components/amount-input.tsx +49 -56
  219. package/src/components/autocomplete.tsx +133 -110
  220. package/src/components/checkbox-group.tsx +229 -0
  221. package/src/components/checkbox.tsx +72 -8
  222. package/src/components/chip.tsx +80 -10
  223. package/src/components/combobox.tsx +292 -253
  224. package/src/components/country-select.tsx +443 -0
  225. package/src/components/currency-select.tsx +109 -33
  226. package/src/components/danger-confirm.tsx +345 -77
  227. package/src/components/data-table-labels.ts +16 -1
  228. package/src/components/data-table.tsx +175 -6
  229. package/src/components/date-picker.tsx +173 -62
  230. package/src/components/entity-combobox.tsx +107 -87
  231. package/src/components/field-anatomy.tsx +190 -0
  232. package/src/components/field-parts.tsx +184 -0
  233. package/src/components/file-button.tsx +36 -3
  234. package/src/components/file-dropzone.tsx +106 -38
  235. package/src/components/form-actions.tsx +115 -2
  236. package/src/components/iban-input.tsx +231 -0
  237. package/src/components/icon-picker.tsx +26 -4
  238. package/src/components/inline-edit-field.tsx +533 -0
  239. package/src/components/language-select.tsx +78 -0
  240. package/src/components/line-items.tsx +35 -2
  241. package/src/components/list.tsx +55 -6
  242. package/src/components/money-field.tsx +10 -0
  243. package/src/components/month-picker.tsx +399 -63
  244. package/src/components/multi-select.tsx +80 -58
  245. package/src/components/number-field.tsx +28 -46
  246. package/src/components/number-input.tsx +210 -43
  247. package/src/components/one-time-code-input.tsx +288 -0
  248. package/src/components/phone-input.tsx +299 -0
  249. package/src/components/sign-chip.tsx +103 -0
  250. package/src/components/swatch-picker.tsx +118 -30
  251. package/src/components/switch.tsx +58 -7
  252. package/src/components/tile-radio.tsx +71 -16
  253. package/src/components/time-input.tsx +8 -1
  254. package/src/components/toggle-group.tsx +206 -73
  255. package/src/components/ui.tsx +472 -105
  256. package/src/data-table.ts +2 -1
  257. package/src/feedback/feedback-attachment.tsx +313 -9
  258. package/src/feedback/feedback-dialog.tsx +2 -0
  259. package/src/feedback/feedback-thread.tsx +66 -5
  260. package/src/i18n/defaults.ts +16 -1
  261. package/src/i18n/german.ts +47 -0
  262. package/src/i18n/kit-labels.tsx +78 -2
  263. package/src/i18n/locales/en.ts +10 -0
  264. package/src/i18n/locales/es.ts +46 -0
  265. package/src/i18n/locales/fr.ts +45 -0
  266. package/src/i18n/locales/hu.ts +46 -0
  267. package/src/i18n/locales/it.ts +47 -0
  268. package/src/i18n/locales/zh.ts +44 -0
  269. package/src/i18n/review.ts +7 -0
  270. package/src/index.ts +53 -3
  271. package/src/lib/countries.ts +91 -0
  272. package/src/lib/decimal-marks.ts +99 -0
  273. package/src/lib/iban.ts +171 -0
  274. package/src/lib/isin.ts +62 -0
  275. package/src/lib/phone.ts +211 -0
  276. package/src/rhf/fields.tsx +606 -9
  277. package/src/rhf.ts +2 -1
  278. package/src/tour/tour.tsx +66 -28
@@ -0,0 +1,211 @@
1
+ /**
2
+ * Phone numbers — E.164 in, grouped out — for kastlan's contacts, without a
3
+ * libphonenumber dependency.
4
+ *
5
+ * kastlan stored whatever was typed ("079 …", "+41 (0)79 …", "0041 79 …") and could
6
+ * neither dial nor de-duplicate it. What it needs is one canonical form to store —
7
+ * E.164, "+41210000001" — and a readable one to show. libphonenumber gets both right
8
+ * for every country, at ~150 kB of metadata in every app's bundle; the kit's apps are
9
+ * Swiss and their neighbours, so this is **light per-country rules** for the six that
10
+ * matter and a generic shape check for the rest:
11
+ *
12
+ * - **CH, LI, DE, AT, FR, IT**: the national trunk prefix is dropped (a leading 0 —
13
+ * but NOT for Italy, whose fixed-line numbers keep their 0 after +39, and not for
14
+ * Liechtenstein, which has none), and the remaining national number has to be a
15
+ * plausible length for the country.
16
+ * - **Anything else** typed with its own "+" (or "00"): the digits, 8 to 15 in all,
17
+ * are taken as an E.164 number as they stand.
18
+ *
19
+ * **The limitation:** a plausible length is not a valid number. There is no check of
20
+ * area codes or number ranges, so a well-formed number that does not exist passes, and
21
+ * a German or Austrian number (whose area codes run from one to five digits) is
22
+ * grouped only where the split is known (mobile prefixes, the largest cities) and
23
+ * otherwise shown in one block. A number these rules cannot read is not an error —
24
+ * {@link parsePhone} returns `null` and the field keeps the text as typed.
25
+ */
26
+
27
+ /** The countries with their own rules. */
28
+ export type PhoneCountryCode = "CH" | "LI" | "DE" | "AT" | "FR" | "IT";
29
+
30
+ /** A country with rules, or `"other"`: a number typed with its own "+". */
31
+ export type PhoneCountry = PhoneCountryCode | "other";
32
+
33
+ interface CountryRule {
34
+ /** The country calling code, without the "+". */
35
+ dial: string;
36
+ /** A national number starts with a trunk 0 that is dropped after the country code. */
37
+ trunk: boolean;
38
+ /** The national significant number itself may start with 0 (Italy's fixed lines). */
39
+ zeroStart?: boolean;
40
+ /** The national significant number's length, inclusive. */
41
+ min: number;
42
+ max: number;
43
+ /** Groups for display, given the national significant number. */
44
+ group: (nsn: string) => string[];
45
+ }
46
+
47
+ /** `nsn` cut into the given sizes; whatever is left is one last group. */
48
+ function cut(nsn: string, sizes: number[]): string[] {
49
+ const out: string[] = [];
50
+ let at = 0;
51
+ for (const size of sizes) {
52
+ if (at >= nsn.length) break;
53
+ out.push(nsn.slice(at, at + size));
54
+ at += size;
55
+ }
56
+ if (at < nsn.length) out.push(nsn.slice(at));
57
+ return out;
58
+ }
59
+
60
+ const RULES: Record<PhoneCountryCode, CountryRule> = {
61
+ // 9 digits, 2-3-2-2 ("21 000 00 01"); the 08xx/09xx service numbers 3-3-3.
62
+ CH: {
63
+ dial: "41",
64
+ trunk: true,
65
+ min: 9,
66
+ max: 9,
67
+ group: (n) => (/^[89]/.test(n) ? cut(n, [3, 3, 3]) : cut(n, [2, 3, 2, 2])),
68
+ },
69
+ // 7 digits, 3-2-2 ("100 00 01"), and 9 for some mobile ranges (3-3-3). No trunk prefix.
70
+ LI: {
71
+ dial: "423",
72
+ trunk: false,
73
+ min: 7,
74
+ max: 9,
75
+ group: (n) => (n.length === 7 ? cut(n, [3, 2, 2]) : n.length === 9 ? cut(n, [3, 3, 3]) : [n]),
76
+ },
77
+ // Area codes of two to five digits: only the mobile prefixes and the four
78
+ // two-digit cities are split; anything else is one block rather than a wrong split.
79
+ DE: {
80
+ dial: "49",
81
+ trunk: true,
82
+ min: 6,
83
+ max: 13,
84
+ group: (n) =>
85
+ /^15/.test(n)
86
+ ? cut(n, [4])
87
+ : /^1[67]/.test(n)
88
+ ? cut(n, [3])
89
+ : /^(30|40|69|89)/.test(n)
90
+ ? cut(n, [2])
91
+ : [n],
92
+ },
93
+ // Vienna is "1"; mobile numbers start 6xx; other area codes vary, so one block.
94
+ AT: {
95
+ dial: "43",
96
+ trunk: true,
97
+ min: 6,
98
+ max: 13,
99
+ group: (n) => (/^6/.test(n) ? cut(n, [3]) : /^1/.test(n) ? cut(n, [1]) : [n]),
100
+ },
101
+ // 9 digits, 1-2-2-2-2 ("1 99 00 00 01").
102
+ FR: { dial: "33", trunk: true, min: 9, max: 9, group: (n) => cut(n, [1, 2, 2, 2, 2]) },
103
+ // The 0 of a fixed line is part of the number ("+39 06 …"); mobiles start with 3.
104
+ IT: {
105
+ dial: "39",
106
+ trunk: false,
107
+ zeroStart: true,
108
+ min: 6,
109
+ max: 11,
110
+ group: (n) => (/^3/.test(n) ? cut(n, [3, 3]) : /^0[26]/.test(n) ? cut(n, [2, 4]) : [n]),
111
+ },
112
+ };
113
+
114
+ /** The country calling codes with rules, "+" left off: `{ CH: "41", … }`. */
115
+ export const PHONE_DIAL_CODES: Readonly<Record<PhoneCountryCode, string>> = Object.freeze({
116
+ CH: RULES.CH.dial,
117
+ LI: RULES.LI.dial,
118
+ DE: RULES.DE.dial,
119
+ AT: RULES.AT.dial,
120
+ FR: RULES.FR.dial,
121
+ IT: RULES.IT.dial,
122
+ });
123
+
124
+ /** The countries with rules, in the order a picker offers them by default. */
125
+ export const PHONE_COUNTRIES: readonly PhoneCountryCode[] = Object.freeze(["CH", "LI", "DE", "AT", "FR", "IT"]);
126
+
127
+ /** Longest dial code first, so "+423" is Liechtenstein before anything shorter. */
128
+ const BY_DIAL = [...PHONE_COUNTRIES].sort((a, b) => RULES[b].dial.length - RULES[a].dial.length);
129
+
130
+ /** What {@link parsePhone} reads out of a number. */
131
+ export interface ParsedPhone {
132
+ /** The number in E.164: "+", the country code, the national number. */
133
+ e164: string;
134
+ /** The country whose rules read it, or `null` for a generic "+…" number. */
135
+ country: PhoneCountryCode | null;
136
+ /** The national significant number — the digits after the country code. For a
137
+ * generic number, every digit after the "+" (the country code is not split off). */
138
+ national: string;
139
+ }
140
+
141
+ /** A national significant number that passes `rule`, or `null`. */
142
+ function national(digits: string, rule: CountryRule): string | null {
143
+ const nsn = rule.trunk && digits.startsWith("0") ? digits.slice(1) : digits;
144
+ // A national number starts with 0 only where 0 is part of it (Italy's fixed lines).
145
+ if (nsn.startsWith("0") && !rule.zeroStart) return null;
146
+ if (nsn.length < rule.min || nsn.length > rule.max) return null;
147
+ return nsn;
148
+ }
149
+
150
+ /**
151
+ * Read a typed number. With a leading "+" or "00" the number carries its own country
152
+ * code — one of the six with rules, or any other (8–15 digits in all, the E.164
153
+ * limit). Without one it is a national number of `country` (default `"CH"`), its
154
+ * trunk 0 optional; with `country="other"` a number without "+" cannot be read.
155
+ *
156
+ * Spaces, hyphens, dots, slashes and parentheses are ignored ("(030) 1234567"), and
157
+ * so is the "(0)" of "+41 (0)21 …". Anything else — a letter, an extension ("ext. 12"), a note —
158
+ * and the result is `null`: the text is not a number these rules can read, and a field
159
+ * keeps it as it is rather than guess.
160
+ */
161
+ export function parsePhone(input: string, country: PhoneCountry = "CH"): ParsedPhone | null {
162
+ const text = input.trim();
163
+ if (text === "" || !/^\+?[\d\s().\-/]+$/.test(text)) return null;
164
+ let digits = text.replace(/\D/g, "");
165
+ let international = text.startsWith("+");
166
+ if (!international && digits.startsWith("00")) {
167
+ international = true;
168
+ digits = digits.slice(2);
169
+ }
170
+ if (international) {
171
+ // "+41 (0)21 …": the bracketed trunk prefix written for callers inside the country.
172
+ digits = text.replace(/\(\s*0\s*\)/g, "").replace(/\D/g, "").replace(/^00/, "");
173
+ if (digits.startsWith("0")) return null;
174
+ const known = BY_DIAL.find((c) => digits.startsWith(RULES[c].dial));
175
+ if (known) {
176
+ const rule = RULES[known];
177
+ const nsn = national(digits.slice(rule.dial.length), rule);
178
+ return nsn === null ? null : { e164: `+${rule.dial}${nsn}`, country: known, national: nsn };
179
+ }
180
+ if (digits.length < 8 || digits.length > 15) return null;
181
+ return { e164: `+${digits}`, country: null, national: digits };
182
+ }
183
+ if (country === "other") return null;
184
+ const rule = RULES[country];
185
+ const nsn = national(digits, rule);
186
+ return nsn === null ? null : { e164: `+${rule.dial}${nsn}`, country, national: nsn };
187
+ }
188
+
189
+ /** The national number grouped by its country's rules ("21 000 00 01"). */
190
+ export function formatNationalPhone(nsn: string, country: PhoneCountryCode): string {
191
+ return RULES[country].group(nsn).join(" ");
192
+ }
193
+
194
+ /**
195
+ * A number for display: an E.164 number (or anything {@link parsePhone} reads with a
196
+ * "+") grouped by its country's rules — "+41 21 000 00 01" — and a generic one as
197
+ * "+" and its digits. Anything it cannot read comes back exactly as it went in, so a
198
+ * list of contacts can format every row without first sorting out the free text.
199
+ */
200
+ export function formatPhone(value: string): string {
201
+ const parsed = value.trim().startsWith("+") ? parsePhone(value, "other") : null;
202
+ if (!parsed) return value;
203
+ if (!parsed.country) return parsed.e164;
204
+ return `+${RULES[parsed.country].dial} ${formatNationalPhone(parsed.national, parsed.country)}`;
205
+ }
206
+
207
+ /** Whether `value` is in E.164 shape: "+" and 8–15 digits, nothing else. Shape only —
208
+ * what {@link PhoneInput} emits for every number it could read. */
209
+ export function isE164(value: string): boolean {
210
+ return /^\+[1-9]\d{7,14}$/.test(value);
211
+ }