@eifi1/ui-kit 0.52.0 → 0.53.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 (193) hide show
  1. package/README.md +47 -54
  2. package/dist/billing/cancel-subscription.d.ts +5 -14
  3. package/dist/billing/cancel-subscription.js +1 -3
  4. package/dist/billing/cancel-subscription.js.map +1 -1
  5. package/dist/billing/plan-card.d.ts +5 -13
  6. package/dist/billing/plan-card.js +3 -11
  7. package/dist/billing/plan-card.js.map +1 -1
  8. package/dist/billing/plan-switch.d.ts +5 -14
  9. package/dist/billing/plan-switch.js +1 -3
  10. package/dist/billing/plan-switch.js.map +1 -1
  11. package/dist/chart.d.ts +3 -14
  12. package/dist/chart.js +64 -11
  13. package/dist/chart.js.map +1 -1
  14. package/dist/components/date-field.d.ts +1 -1
  15. package/dist/components/date-field.js.map +1 -1
  16. package/dist/components/date-picker.d.ts +1 -1
  17. package/dist/components/date-picker.js.map +1 -1
  18. package/dist/components/date-range-picker.d.ts +4 -17
  19. package/dist/components/date-range-picker.js +2 -2
  20. package/dist/components/date-range-picker.js.map +1 -1
  21. package/dist/components/nav-pills.d.ts +1 -1
  22. package/dist/components/nav-pills.js.map +1 -1
  23. package/dist/components/series-chart-axes.d.ts +1 -1
  24. package/dist/components/series-chart-axes.js.map +1 -1
  25. package/dist/components/series-chart.d.ts +64 -19
  26. package/dist/components/series-chart.js +2 -1
  27. package/dist/components/series-chart.js.map +1 -1
  28. package/dist/components/treemap-labels.d.ts +6 -1
  29. package/dist/components/treemap-labels.js +2 -2
  30. package/dist/components/treemap-labels.js.map +1 -1
  31. package/dist/components/treemap.d.ts +29 -4
  32. package/dist/components/treemap.js +22 -9
  33. package/dist/components/treemap.js.map +1 -1
  34. package/dist/data-table.d.ts +1 -6
  35. package/dist/data-table.js +1 -9
  36. package/dist/data-table.js.map +1 -1
  37. package/dist/demo/demo-countdown.d.ts +0 -5
  38. package/dist/demo/demo-countdown.js +0 -4
  39. package/dist/demo/demo-countdown.js.map +1 -1
  40. package/dist/eslint/index.js +1 -1
  41. package/dist/feedback/feedback-mutation.d.ts +11 -23
  42. package/dist/feedback/feedback-mutation.js +1 -9
  43. package/dist/feedback/feedback-mutation.js.map +1 -1
  44. package/dist/feedback/feedback-public-list.d.ts +1 -6
  45. package/dist/feedback/feedback-public-list.js +1 -2
  46. package/dist/feedback/feedback-public-list.js.map +1 -1
  47. package/dist/feedback/feedback-row-detail-labels.js +0 -8
  48. package/dist/feedback/feedback-row-detail-labels.js.map +1 -1
  49. package/dist/feedback/feedback-row-detail.d.ts +3 -50
  50. package/dist/feedback/feedback-row-detail.js +0 -8
  51. package/dist/feedback/feedback-row-detail.js.map +1 -1
  52. package/dist/feedback/feedback-status-undo.d.ts +3 -19
  53. package/dist/feedback/feedback-status-undo.js.map +1 -1
  54. package/dist/feedback/feedback-thread.d.ts +0 -9
  55. package/dist/feedback/feedback-thread.js.map +1 -1
  56. package/dist/feedback.d.ts +6 -40
  57. package/dist/feedback.js +88 -46
  58. package/dist/feedback.js.map +1 -1
  59. package/dist/i18n/defaults.js +0 -4
  60. package/dist/i18n/defaults.js.map +1 -1
  61. package/dist/i18n/german.js +2 -19
  62. package/dist/i18n/german.js.map +1 -1
  63. package/dist/i18n/kit-labels.d.ts +5 -14
  64. package/dist/i18n/kit-labels.js.map +1 -1
  65. package/dist/i18n/locales/en.js +2 -30
  66. package/dist/i18n/locales/en.js.map +3 -3
  67. package/dist/i18n/locales/es.js +2 -19
  68. package/dist/i18n/locales/es.js.map +1 -1
  69. package/dist/i18n/locales/fr.js +2 -19
  70. package/dist/i18n/locales/fr.js.map +1 -1
  71. package/dist/i18n/locales/hu.js +2 -19
  72. package/dist/i18n/locales/hu.js.map +1 -1
  73. package/dist/i18n/locales/it.js +2 -20
  74. package/dist/i18n/locales/it.js.map +1 -1
  75. package/dist/i18n/locales/zh.js +2 -19
  76. package/dist/i18n/locales/zh.js.map +1 -1
  77. package/dist/i18n/review.js +9 -4
  78. package/dist/i18n/review.js.map +1 -1
  79. package/dist/i18n/shared-labels.js +0 -1
  80. package/dist/i18n/shared-labels.js.map +1 -1
  81. package/dist/index.d.ts +15 -64
  82. package/dist/index.js +22 -102
  83. package/dist/index.js.map +1 -1
  84. package/dist/rhf/fields.d.ts +2 -2
  85. package/dist/rhf/fields.js.map +1 -1
  86. package/dist/search/search-index.d.ts +0 -6
  87. package/dist/search/search-index.js +0 -4
  88. package/dist/search/search-index.js.map +1 -1
  89. package/dist/search.d.ts +1 -8
  90. package/dist/search.js +2 -7
  91. package/dist/search.js.map +1 -1
  92. package/dist/shell/account-menu-language.d.ts +7 -21
  93. package/dist/shell/account-menu-language.js +1 -3
  94. package/dist/shell/account-menu-language.js.map +1 -1
  95. package/dist/shell/account-menu-text-size.d.ts +9 -23
  96. package/dist/shell/account-menu-text-size.js +2 -3
  97. package/dist/shell/account-menu-text-size.js.map +1 -1
  98. package/dist/shell/app-shell.d.ts +0 -10
  99. package/dist/shell/app-shell.js +1 -7
  100. package/dist/shell/app-shell.js.map +1 -1
  101. package/dist/shell/company-switcher.d.ts +1 -1
  102. package/dist/shell/company-switcher.js.map +1 -1
  103. package/dist/shell/dev-menu.d.ts +1 -1
  104. package/dist/shell/dev-menu.js.map +1 -1
  105. package/dist/shell/top-bar-parts-labels.d.ts +1 -1
  106. package/dist/shell/top-bar-parts-labels.js.map +1 -1
  107. package/dist/shell.d.ts +7 -29
  108. package/dist/shell.js +12 -42
  109. package/dist/shell.js.map +1 -1
  110. package/dist/sync.d.ts +2 -6
  111. package/dist/sync.js +2 -15
  112. package/dist/sync.js.map +1 -1
  113. package/dist/theme/chart-palette.d.ts +0 -5
  114. package/dist/theme/chart-palette.js +0 -2
  115. package/dist/theme/chart-palette.js.map +1 -1
  116. package/package.json +1 -1
  117. package/src/billing/cancel-subscription.tsx +5 -16
  118. package/src/billing/plan-card.tsx +7 -24
  119. package/src/billing/plan-switch.tsx +5 -16
  120. package/src/chart.ts +62 -13
  121. package/src/components/date-field.tsx +1 -1
  122. package/src/components/date-picker.tsx +0 -1
  123. package/src/components/date-range-picker.tsx +6 -22
  124. package/src/components/nav-pills.tsx +1 -1
  125. package/src/components/series-chart-axes.ts +6 -1
  126. package/src/components/series-chart.tsx +78 -21
  127. package/src/components/treemap-labels.ts +11 -3
  128. package/src/components/treemap.tsx +65 -12
  129. package/src/data-table.ts +1 -11
  130. package/src/demo/demo-countdown.ts +3 -8
  131. package/src/feedback/feedback-mutation.ts +12 -33
  132. package/src/feedback/feedback-public-list.tsx +1 -7
  133. package/src/feedback/feedback-row-detail-labels.ts +0 -8
  134. package/src/feedback/feedback-row-detail.tsx +8 -58
  135. package/src/feedback/feedback-status-undo.ts +4 -18
  136. package/src/feedback/feedback-thread.tsx +0 -10
  137. package/src/feedback.ts +89 -48
  138. package/src/i18n/defaults.ts +0 -4
  139. package/src/i18n/german.ts +2 -19
  140. package/src/i18n/kit-labels.tsx +5 -14
  141. package/src/i18n/locales/es.ts +2 -19
  142. package/src/i18n/locales/fr.ts +2 -19
  143. package/src/i18n/locales/hu.ts +2 -19
  144. package/src/i18n/locales/it.ts +2 -20
  145. package/src/i18n/locales/zh.ts +2 -19
  146. package/src/i18n/review.ts +9 -4
  147. package/src/i18n/shared-labels.ts +0 -1
  148. package/src/index.ts +30 -112
  149. package/src/rhf/fields.tsx +2 -2
  150. package/src/search/search-index.ts +0 -15
  151. package/src/search.ts +8 -8
  152. package/src/shell/account-menu-language.tsx +7 -27
  153. package/src/shell/account-menu-text-size.tsx +13 -32
  154. package/src/shell/app-shell.tsx +0 -13
  155. package/src/shell/company-switcher.tsx +1 -1
  156. package/src/shell/dev-menu.tsx +1 -1
  157. package/src/shell/top-bar-parts-labels.ts +1 -1
  158. package/src/shell.ts +17 -44
  159. package/src/sync.ts +2 -15
  160. package/src/theme/chart-palette.ts +0 -2
  161. package/tokens.css +4 -4
  162. package/dist/billing/dialog-close.d.ts +0 -29
  163. package/dist/billing/dialog-close.js +0 -8
  164. package/dist/billing/dialog-close.js.map +0 -1
  165. package/dist/components/field-sync-labels.d.ts +0 -3
  166. package/dist/components/field-sync-labels.js +0 -12
  167. package/dist/components/field-sync-labels.js.map +0 -1
  168. package/dist/components/field-sync.d.ts +0 -186
  169. package/dist/components/field-sync.js +0 -269
  170. package/dist/components/field-sync.js.map +0 -1
  171. package/dist/components/section-nav.d.ts +0 -44
  172. package/dist/components/section-nav.js +0 -33
  173. package/dist/components/section-nav.js.map +0 -1
  174. package/dist/components/sign-chip-labels.d.ts +0 -3
  175. package/dist/components/sign-chip-labels.js +0 -11
  176. package/dist/components/sign-chip-labels.js.map +0 -1
  177. package/dist/components/sign-chip.d.ts +0 -77
  178. package/dist/components/sign-chip.js +0 -47
  179. package/dist/components/sign-chip.js.map +0 -1
  180. package/dist/lib/isin.d.ts +0 -38
  181. package/dist/lib/isin.js +0 -36
  182. package/dist/lib/isin.js.map +0 -1
  183. package/dist/shell/role-switcher.d.ts +0 -28
  184. package/dist/shell/role-switcher.js +0 -31
  185. package/dist/shell/role-switcher.js.map +0 -1
  186. package/src/billing/dialog-close.ts +0 -34
  187. package/src/components/field-sync-labels.ts +0 -14
  188. package/src/components/field-sync.tsx +0 -504
  189. package/src/components/section-nav.tsx +0 -72
  190. package/src/components/sign-chip-labels.ts +0 -13
  191. package/src/components/sign-chip.tsx +0 -133
  192. package/src/lib/isin.ts +0 -72
  193. package/src/shell/role-switcher.tsx +0 -53
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/search/search-index.ts"],"sourcesContent":["import type { ReactNode } from \"react\";\nimport { foldForSearch } from \"../lib/text-match\";\n\n/**\n * The matcher behind {@link GlobalSearch} — pure, React-free, testable on its own.\n *\n * WHY IT EXISTS. `CommandPalette` is deliberately domain-free: it shows whatever a\n * `search(query)` provider returns. So every app wrote that provider, and every app wrote\n * the same one — `label.toLowerCase().includes(q)` over its pages and actions (kastlan's\n * command palette, keksdose's global search) plus a second copy with keywords for the\n * settings catalogue (keksdose's `matchSettingsEntries`). A substring test answers \"does\n * this string contain that one\", which is not what a search box is asked:\n *\n * - \"range date\" finds nothing, because the words are in the other order;\n * - \"Uberweisung\" misses \"Überweisung\", and \"Tooltp\" misses \"Tooltip\";\n * - \"del\" matches \"model\" as happily as \"delete\", and every hit ranks the same, so the\n * exact page name can sit under six unrelated rows.\n *\n * What it does instead, per entry:\n *\n * - NORMALISES both sides: lower case, accents stripped (NFD minus combining marks),\n * `ß` → `ss`, punctuation as a word break, and camelCase split so `DateRangePicker`\n * is also the words date, range, picker.\n * - Requires EVERY query word to match SOMEWHERE in the entry — title, keywords or\n * description, in any order.\n * - A word matches a word of the entry exactly, as its prefix, inside it (four letters or\n * more, for compounds: \"bereich\" in \"Datumsbereich\"), or — for words of five letters\n * or more — at edit distance one, transpositions included (\"tooltp\", \"calender\").\n * - RANKS in tiers: exact title > title prefix > all words in the title > some word only\n * in the keywords > some word only in the description. Within a tier, better word\n * matches (exact over prefix over infix over typo) score higher, and `weight` is added\n * last. The tiers are {@link SEARCH_TIER_POINTS} points apart, so a weight below that reorders within\n * a tier and never across one.\n * - Caps each group (`groupLimit`, default 8) after ranking, so one broad group cannot\n * push every other group off the list.\n */\n\n/** One searchable thing: a page, an action, a record, a settings row. */\nexport interface SearchEntry {\n /** Unique across the entries handed to one index. */\n id: string;\n /** What the row says, and what ranks highest. */\n title: string;\n /** Other words or phrases that should find it — synonyms, a translated name, tags.\n * Matched like the title, ranked below it. */\n keywords?: readonly string[];\n /** Longer text, matched last and ranked lowest. Not shown in the row. */\n description?: string;\n /** Heading the row is grouped under in the palette. */\n group?: string;\n /** Secondary text on the row's trailing side (the page it lives on, an amount). */\n hint?: string;\n /** Leading icon. */\n icon?: ReactNode;\n /** Where it leads — the app's own path, handed to `GlobalSearch`'s `navigate`. */\n href?: string;\n /** Run instead of navigating. Wins over `href` for a plain click and ↵; `href` still\n * makes the row a real link for a middle-/⌘-click. */\n onSelect?: () => void;\n /** Added to the score. Below {@link SEARCH_TIER_POINTS} it reorders within a tier. */\n weight?: number;\n /** Masks the row for session replay (see `CommandItem.redact`). */\n redact?: boolean;\n}\n\nexport type SearchMatchField = \"title\" | \"keywords\" | \"description\";\n\nexport interface SearchHit<E extends SearchEntry = SearchEntry> {\n entry: E;\n score: number;\n /** The weakest field any query word had to be found in. */\n field: SearchMatchField;\n}\n\nexport interface SearchIndexOptions {\n /** Hits kept per `group` after ranking. Default 8; `Infinity` for no cap. */\n groupLimit?: number;\n /** Hits kept in total. Default: no cap beyond `groupLimit`. */\n limit?: number;\n /** Shortest query word allowed an edit-distance-1 match. Default 5 — below that a\n * single typo is a different word (\"cat\" / \"car\"). */\n typoMinLength?: number;\n /** Shortest query word matched INSIDE an entry word. Default 4. */\n infixMinLength?: number;\n}\n\nexport interface SearchIndex<E extends SearchEntry = SearchEntry> {\n /** Ranked hits for `query`; an empty query returns none. */\n search: (query: string) => SearchHit<E>[];\n readonly entries: readonly E[];\n}\n\n/** Points between two ranking tiers. */\nexport const SEARCH_TIER_POINTS = 1000;\n\nconst TIERS = {\n exact: 5,\n prefix: 4,\n title: 3,\n keywords: 2,\n description: 1,\n} as const;\n\n/** How well one query word matched one entry word — higher is better. */\nconst WORD_SCORE = { exact: 40, prefix: 30, infix: 15, typo: 10 } as const;\n/** How much the field a word matched in is worth, multiplied with the above. */\nconst FIELD_FACTOR: Record<SearchMatchField, number> = { title: 3, keywords: 2, description: 1 };\n\n/**\n * Lower case, accents off, punctuation to spaces — the form both sides are compared in.\n * Exported so an app can normalise its own keys the same way. The fold is every kit\n * list's {@link foldForSearch} (DUP-5, refactor audit 2026-10); the index adds the\n * punctuation-to-space step its word split needs.\n */\nexport function normalizeSearchText(text: string): string {\n return foldForSearch(text)\n .replace(/[^\\p{L}\\p{N}]+/gu, \" \")\n .trim();\n}\n\n/** The words of a text, camelCase split too: \"DateRangePicker\" → daterangepicker, date,\n * range, picker. The joined word stays so \"daterange\" still prefixes it. */\nfunction words(text: string): string[] {\n const out = new Set<string>();\n // Split camelCase BEFORE lower-casing, or the boundaries are gone.\n const spaced = text.replace(/(\\p{Ll}|\\p{N})(\\p{Lu})/gu, \"$1 $2\").replace(/(\\p{Lu})(\\p{Lu}\\p{Ll})/gu, \"$1 $2\");\n for (const w of normalizeSearchText(text).split(\" \")) if (w) out.add(w);\n for (const w of normalizeSearchText(spaced).split(\" \")) if (w) out.add(w);\n return [...out];\n}\n\n/** Restricted Damerau–Levenshtein distance, early-exiting once it must exceed `max`. */\nfunction withinDistance(a: string, b: string, max: number): boolean {\n if (Math.abs(a.length - b.length) > max) return false;\n if (a === b) return true;\n const n = a.length;\n const m = b.length;\n let prev2: number[] = [];\n let prev = Array.from({ length: m + 1 }, (_, j) => j);\n for (let i = 1; i <= n; i++) {\n const cur = [i];\n let rowMin = i;\n for (let j = 1; j <= m; j++) {\n const cost = a[i - 1] === b[j - 1] ? 0 : 1;\n let v = Math.min(prev[j] + 1, cur[j - 1] + 1, prev[j - 1] + cost);\n if (i > 1 && j > 1 && a[i - 1] === b[j - 2] && a[i - 2] === b[j - 1]) {\n v = Math.min(v, prev2[j - 2] + 1);\n }\n cur.push(v);\n if (v < rowMin) rowMin = v;\n }\n if (rowMin > max) return false;\n prev2 = prev;\n prev = cur;\n }\n return prev[m] <= max;\n}\n\ntype WordKind = keyof typeof WORD_SCORE;\n\nfunction matchWord(\n q: string,\n word: string,\n opts: Required<Pick<SearchIndexOptions, \"typoMinLength\" | \"infixMinLength\">>,\n): WordKind | null {\n if (word === q) return \"exact\";\n if (word.startsWith(q)) return \"prefix\";\n if (q.length >= opts.infixMinLength && word.includes(q)) return \"infix\";\n if (q.length >= opts.typoMinLength) {\n // The whole word, or the word's start as long as the query (a typo in a prefix:\n // \"calender\" is one edit from \"calendar\", the start of \"calendars\").\n if (withinDistance(q, word, 1)) return \"typo\";\n if (word.length > q.length && withinDistance(q, word.slice(0, q.length), 1)) return \"typo\";\n }\n return null;\n}\n\ninterface Prepared<E> {\n entry: E;\n title: string;\n titleCompact: string;\n fields: Array<[SearchMatchField, string[]]>;\n}\n\n/**\n * Build an index over `entries`. Normalisation happens once here, so `search` is a scan\n * over prepared words — a few thousand entries per keystroke is nothing.\n */\nexport function createSearchIndex<E extends SearchEntry>(\n entries: readonly E[],\n options: SearchIndexOptions = {},\n): SearchIndex<E> {\n const { groupLimit = 8, limit = Infinity, typoMinLength = 5, infixMinLength = 4 } = options;\n const wordOpts = { typoMinLength, infixMinLength };\n const prepared: Prepared<E>[] = entries.map((entry) => {\n const title = normalizeSearchText(entry.title);\n return {\n entry,\n title,\n titleCompact: title.replace(/ /g, \"\"),\n fields: [\n [\"title\", words(entry.title)],\n [\"keywords\", (entry.keywords ?? []).flatMap(words)],\n [\"description\", entry.description ? words(entry.description) : []],\n ],\n };\n });\n\n const search = (query: string): SearchHit<E>[] => {\n const q = normalizeSearchText(query);\n if (!q) return [];\n const qWords = [...new Set(q.split(\" \"))];\n const qCompact = q.replace(/ /g, \"\");\n const hits: SearchHit<E>[] = [];\n\n for (const p of prepared) {\n let quality = 0;\n let weakest: SearchMatchField = \"title\";\n let ok = true;\n for (const qw of qWords) {\n let best = 0;\n let bestField: SearchMatchField | null = null;\n for (const [field, fieldWords] of p.fields) {\n for (const w of fieldWords) {\n const kind = matchWord(qw, w, wordOpts);\n if (!kind) continue;\n const s = WORD_SCORE[kind] * FIELD_FACTOR[field];\n if (s > best) {\n best = s;\n bestField = field;\n }\n }\n }\n if (!bestField) {\n ok = false;\n break;\n }\n quality += best;\n if (FIELD_FACTOR[bestField] < FIELD_FACTOR[weakest]) weakest = bestField;\n }\n if (!ok) continue;\n\n let tier: number = TIERS[weakest];\n if (p.title === q || p.titleCompact === qCompact) tier = TIERS.exact;\n else if (\n p.title.startsWith(q) ||\n p.titleCompact.startsWith(qCompact) ||\n // The whole title, mistyped once (\"Tooltp\") — as good as a prefix, not exact.\n (qCompact.length >= typoMinLength && withinDistance(qCompact, p.titleCompact, 1))\n ) {\n tier = TIERS.prefix;\n }\n // Within a tier: how well the words matched (≤ 120 each, averaged so a long query\n // cannot climb by word count), and how much of the title the query covers — so\n // \"Tooltip\" beats \"Popovers, menus & tooltips\" for the same one-word match.\n const coverage = Math.min(1, qCompact.length / Math.max(1, p.titleCompact.length)) * 40;\n const score =\n tier * SEARCH_TIER_POINTS +\n Math.min(quality / qWords.length + coverage, SEARCH_TIER_POINTS / 2 - 1) +\n (p.entry.weight ?? 0);\n hits.push({ entry: p.entry, score, field: weakest });\n }\n\n hits.sort((a, b) => b.score - a.score || a.entry.title.localeCompare(b.entry.title));\n return capByGroup(hits, groupLimit, limit);\n };\n\n return { search, entries };\n}\n\nfunction capByGroup<E extends SearchEntry>(hits: SearchHit<E>[], groupLimit: number, limit: number): SearchHit<E>[] {\n const perGroup = new Map<string, number>();\n const out: SearchHit<E>[] = [];\n for (const hit of hits) {\n if (out.length >= limit) break;\n const g = hit.entry.group ?? \"\";\n const n = perGroup.get(g) ?? 0;\n if (n >= groupLimit) continue;\n perGroup.set(g, n + 1);\n out.push(hit);\n }\n return out;\n}\n\n/**\n * One-shot form of {@link createSearchIndex}: the matching entries, best first. For a\n * list that changes on every call (a server response, a store selector); build an index\n * once when the entries are stable.\n */\nexport function matchEntries<E extends SearchEntry>(\n entries: readonly E[],\n query: string,\n options?: SearchIndexOptions,\n): E[] {\n return createSearchIndex(entries, options)\n .search(query)\n .map((hit) => hit.entry);\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/search/search-index.ts"],"sourcesContent":["import type { ReactNode } from \"react\";\nimport { foldForSearch } from \"../lib/text-match\";\n\n/**\n * The matcher behind {@link GlobalSearch} — pure, React-free, testable on its own.\n *\n * WHY IT EXISTS. `CommandPalette` is deliberately domain-free: it shows whatever a\n * `search(query)` provider returns. So every app wrote that provider, and every app wrote\n * the same one — `label.toLowerCase().includes(q)` over its pages and actions (kastlan's\n * command palette, keksdose's global search) plus a second copy with keywords for the\n * settings catalogue (keksdose's `matchSettingsEntries`). A substring test answers \"does\n * this string contain that one\", which is not what a search box is asked:\n *\n * - \"range date\" finds nothing, because the words are in the other order;\n * - \"Uberweisung\" misses \"Überweisung\", and \"Tooltp\" misses \"Tooltip\";\n * - \"del\" matches \"model\" as happily as \"delete\", and every hit ranks the same, so the\n * exact page name can sit under six unrelated rows.\n *\n * What it does instead, per entry:\n *\n * - NORMALISES both sides: lower case, accents stripped (NFD minus combining marks),\n * `ß` → `ss`, punctuation as a word break, and camelCase split so `DateRangePicker`\n * is also the words date, range, picker.\n * - Requires EVERY query word to match SOMEWHERE in the entry — title, keywords or\n * description, in any order.\n * - A word matches a word of the entry exactly, as its prefix, inside it (four letters or\n * more, for compounds: \"bereich\" in \"Datumsbereich\"), or — for words of five letters\n * or more — at edit distance one, transpositions included (\"tooltp\", \"calender\").\n * - RANKS in tiers: exact title > title prefix > all words in the title > some word only\n * in the keywords > some word only in the description. Within a tier, better word\n * matches (exact over prefix over infix over typo) score higher, and `weight` is added\n * last. The tiers are {@link SEARCH_TIER_POINTS} points apart, so a weight below that reorders within\n * a tier and never across one.\n * - Caps each group (`groupLimit`, default 8) after ranking, so one broad group cannot\n * push every other group off the list.\n */\n\n/** One searchable thing: a page, an action, a record, a settings row. */\nexport interface SearchEntry {\n /** Unique across the entries handed to one index. */\n id: string;\n /** What the row says, and what ranks highest. */\n title: string;\n /** Other words or phrases that should find it — synonyms, a translated name, tags.\n * Matched like the title, ranked below it. */\n keywords?: readonly string[];\n /** Longer text, matched last and ranked lowest. Not shown in the row. */\n description?: string;\n /** Heading the row is grouped under in the palette. */\n group?: string;\n /** Secondary text on the row's trailing side (the page it lives on, an amount). */\n hint?: string;\n /** Leading icon. */\n icon?: ReactNode;\n /** Where it leads — the app's own path, handed to `GlobalSearch`'s `navigate`. */\n href?: string;\n /** Run instead of navigating. Wins over `href` for a plain click and ↵; `href` still\n * makes the row a real link for a middle-/⌘-click. */\n onSelect?: () => void;\n /** Added to the score. Below {@link SEARCH_TIER_POINTS} it reorders within a tier. */\n weight?: number;\n /** Masks the row for session replay (see `CommandItem.redact`). */\n redact?: boolean;\n}\n\nexport type SearchMatchField = \"title\" | \"keywords\" | \"description\";\n\nexport interface SearchHit<E extends SearchEntry = SearchEntry> {\n entry: E;\n score: number;\n /** The weakest field any query word had to be found in. */\n field: SearchMatchField;\n}\n\nexport interface SearchIndexOptions {\n /** Hits kept per `group` after ranking. Default 8; `Infinity` for no cap. */\n groupLimit?: number;\n /** Hits kept in total. Default: no cap beyond `groupLimit`. */\n limit?: number;\n /** Shortest query word allowed an edit-distance-1 match. Default 5 — below that a\n * single typo is a different word (\"cat\" / \"car\"). */\n typoMinLength?: number;\n /** Shortest query word matched INSIDE an entry word. Default 4. */\n infixMinLength?: number;\n}\n\nexport interface SearchIndex<E extends SearchEntry = SearchEntry> {\n /** Ranked hits for `query`; an empty query returns none. */\n search: (query: string) => SearchHit<E>[];\n readonly entries: readonly E[];\n}\n\n/** Points between two ranking tiers. */\nexport const SEARCH_TIER_POINTS = 1000;\n\nconst TIERS = {\n exact: 5,\n prefix: 4,\n title: 3,\n keywords: 2,\n description: 1,\n} as const;\n\n/** How well one query word matched one entry word — higher is better. */\nconst WORD_SCORE = { exact: 40, prefix: 30, infix: 15, typo: 10 } as const;\n/** How much the field a word matched in is worth, multiplied with the above. */\nconst FIELD_FACTOR: Record<SearchMatchField, number> = { title: 3, keywords: 2, description: 1 };\n\n/**\n * Lower case, accents off, punctuation to spaces — the form both sides are compared in.\n * Exported so an app can normalise its own keys the same way. The fold is every kit\n * list's {@link foldForSearch} (DUP-5, refactor audit 2026-10); the index adds the\n * punctuation-to-space step its word split needs.\n */\nexport function normalizeSearchText(text: string): string {\n return foldForSearch(text)\n .replace(/[^\\p{L}\\p{N}]+/gu, \" \")\n .trim();\n}\n\n/** The words of a text, camelCase split too: \"DateRangePicker\" → daterangepicker, date,\n * range, picker. The joined word stays so \"daterange\" still prefixes it. */\nfunction words(text: string): string[] {\n const out = new Set<string>();\n // Split camelCase BEFORE lower-casing, or the boundaries are gone.\n const spaced = text.replace(/(\\p{Ll}|\\p{N})(\\p{Lu})/gu, \"$1 $2\").replace(/(\\p{Lu})(\\p{Lu}\\p{Ll})/gu, \"$1 $2\");\n for (const w of normalizeSearchText(text).split(\" \")) if (w) out.add(w);\n for (const w of normalizeSearchText(spaced).split(\" \")) if (w) out.add(w);\n return [...out];\n}\n\n/** Restricted Damerau–Levenshtein distance, early-exiting once it must exceed `max`. */\nfunction withinDistance(a: string, b: string, max: number): boolean {\n if (Math.abs(a.length - b.length) > max) return false;\n if (a === b) return true;\n const n = a.length;\n const m = b.length;\n let prev2: number[] = [];\n let prev = Array.from({ length: m + 1 }, (_, j) => j);\n for (let i = 1; i <= n; i++) {\n const cur = [i];\n let rowMin = i;\n for (let j = 1; j <= m; j++) {\n const cost = a[i - 1] === b[j - 1] ? 0 : 1;\n let v = Math.min(prev[j] + 1, cur[j - 1] + 1, prev[j - 1] + cost);\n if (i > 1 && j > 1 && a[i - 1] === b[j - 2] && a[i - 2] === b[j - 1]) {\n v = Math.min(v, prev2[j - 2] + 1);\n }\n cur.push(v);\n if (v < rowMin) rowMin = v;\n }\n if (rowMin > max) return false;\n prev2 = prev;\n prev = cur;\n }\n return prev[m] <= max;\n}\n\ntype WordKind = keyof typeof WORD_SCORE;\n\nfunction matchWord(\n q: string,\n word: string,\n opts: Required<Pick<SearchIndexOptions, \"typoMinLength\" | \"infixMinLength\">>,\n): WordKind | null {\n if (word === q) return \"exact\";\n if (word.startsWith(q)) return \"prefix\";\n if (q.length >= opts.infixMinLength && word.includes(q)) return \"infix\";\n if (q.length >= opts.typoMinLength) {\n // The whole word, or the word's start as long as the query (a typo in a prefix:\n // \"calender\" is one edit from \"calendar\", the start of \"calendars\").\n if (withinDistance(q, word, 1)) return \"typo\";\n if (word.length > q.length && withinDistance(q, word.slice(0, q.length), 1)) return \"typo\";\n }\n return null;\n}\n\ninterface Prepared<E> {\n entry: E;\n title: string;\n titleCompact: string;\n fields: Array<[SearchMatchField, string[]]>;\n}\n\n/**\n * Build an index over `entries`. Normalisation happens once here, so `search` is a scan\n * over prepared words — a few thousand entries per keystroke is nothing.\n */\nexport function createSearchIndex<E extends SearchEntry>(\n entries: readonly E[],\n options: SearchIndexOptions = {},\n): SearchIndex<E> {\n const { groupLimit = 8, limit = Infinity, typoMinLength = 5, infixMinLength = 4 } = options;\n const wordOpts = { typoMinLength, infixMinLength };\n const prepared: Prepared<E>[] = entries.map((entry) => {\n const title = normalizeSearchText(entry.title);\n return {\n entry,\n title,\n titleCompact: title.replace(/ /g, \"\"),\n fields: [\n [\"title\", words(entry.title)],\n [\"keywords\", (entry.keywords ?? []).flatMap(words)],\n [\"description\", entry.description ? words(entry.description) : []],\n ],\n };\n });\n\n const search = (query: string): SearchHit<E>[] => {\n const q = normalizeSearchText(query);\n if (!q) return [];\n const qWords = [...new Set(q.split(\" \"))];\n const qCompact = q.replace(/ /g, \"\");\n const hits: SearchHit<E>[] = [];\n\n for (const p of prepared) {\n let quality = 0;\n let weakest: SearchMatchField = \"title\";\n let ok = true;\n for (const qw of qWords) {\n let best = 0;\n let bestField: SearchMatchField | null = null;\n for (const [field, fieldWords] of p.fields) {\n for (const w of fieldWords) {\n const kind = matchWord(qw, w, wordOpts);\n if (!kind) continue;\n const s = WORD_SCORE[kind] * FIELD_FACTOR[field];\n if (s > best) {\n best = s;\n bestField = field;\n }\n }\n }\n if (!bestField) {\n ok = false;\n break;\n }\n quality += best;\n if (FIELD_FACTOR[bestField] < FIELD_FACTOR[weakest]) weakest = bestField;\n }\n if (!ok) continue;\n\n let tier: number = TIERS[weakest];\n if (p.title === q || p.titleCompact === qCompact) tier = TIERS.exact;\n else if (\n p.title.startsWith(q) ||\n p.titleCompact.startsWith(qCompact) ||\n // The whole title, mistyped once (\"Tooltp\") — as good as a prefix, not exact.\n (qCompact.length >= typoMinLength && withinDistance(qCompact, p.titleCompact, 1))\n ) {\n tier = TIERS.prefix;\n }\n // Within a tier: how well the words matched (≤ 120 each, averaged so a long query\n // cannot climb by word count), and how much of the title the query covers — so\n // \"Tooltip\" beats \"Popovers, menus & tooltips\" for the same one-word match.\n const coverage = Math.min(1, qCompact.length / Math.max(1, p.titleCompact.length)) * 40;\n const score =\n tier * SEARCH_TIER_POINTS +\n Math.min(quality / qWords.length + coverage, SEARCH_TIER_POINTS / 2 - 1) +\n (p.entry.weight ?? 0);\n hits.push({ entry: p.entry, score, field: weakest });\n }\n\n hits.sort((a, b) => b.score - a.score || a.entry.title.localeCompare(b.entry.title));\n return capByGroup(hits, groupLimit, limit);\n };\n\n return { search, entries };\n}\n\nfunction capByGroup<E extends SearchEntry>(hits: SearchHit<E>[], groupLimit: number, limit: number): SearchHit<E>[] {\n const perGroup = new Map<string, number>();\n const out: SearchHit<E>[] = [];\n for (const hit of hits) {\n if (out.length >= limit) break;\n const g = hit.entry.group ?? \"\";\n const n = perGroup.get(g) ?? 0;\n if (n >= groupLimit) continue;\n perGroup.set(g, n + 1);\n out.push(hit);\n }\n return out;\n}\n"],"mappings":"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","names":[]}
package/dist/search.d.ts CHANGED
@@ -1,10 +1,3 @@
1
1
  export * from "./search/command-palette.js";
2
- export * from "./search/search-index.js";
3
- export {
4
- /** @deprecated 0.52 — `createSearchIndex`, whose ranking tiers it spaces (API-4); removed in 0.53 */
5
- SEARCH_TIER_POINTS,
6
- /** @deprecated 0.52 — `createSearchIndex(entries).search(query)`, the same match (API-4); removed in 0.53 */
7
- matchEntries,
8
- /** @deprecated 0.52 — `foldForSearch`, the list fold it builds on (API-4); removed in 0.53 */
9
- normalizeSearchText, } from "./search/search-index.js";
2
+ export { createSearchIndex, type SearchEntry, type SearchMatchField, type SearchHit, type SearchIndexOptions, type SearchIndex, } from "./search/search-index.js";
10
3
  export * from "./search/global-search.js";
package/dist/search.js CHANGED
@@ -1,15 +1,10 @@
1
1
  "use client";
2
2
  export * from "./search/command-palette.js";
3
- export * from "./search/search-index.js";
4
3
  import {
5
- SEARCH_TIER_POINTS,
6
- matchEntries,
7
- normalizeSearchText
4
+ createSearchIndex
8
5
  } from "./search/search-index.js";
9
6
  export * from "./search/global-search.js";
10
7
  export {
11
- SEARCH_TIER_POINTS,
12
- matchEntries,
13
- normalizeSearchText
8
+ createSearchIndex
14
9
  };
15
10
  //# sourceMappingURL=search.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/search.ts"],"sourcesContent":["// `@eifi1/ui-kit/search` — the command palette / global search.\n// A re-slicing of the main barrel, not a new API.\nexport * from \"./search/command-palette\";\nexport * from \"./search/search-index\";\n// 0.52: the slice's share of the 55 names no app reads (refactor audit 2026-10-30, API-4).\nexport {\n /** @deprecated 0.52 — `createSearchIndex`, whose ranking tiers it spaces (API-4); removed in 0.53 */\n SEARCH_TIER_POINTS,\n /** @deprecated 0.52 — `createSearchIndex(entries).search(query)`, the same match (API-4); removed in 0.53 */\n matchEntries,\n /** @deprecated 0.52 — `foldForSearch`, the list fold it builds on (API-4); removed in 0.53 */\n normalizeSearchText,\n} from \"./search/search-index\";\nexport * from \"./search/global-search\";\n"],"mappings":";AAEA,cAAc;AACd,cAAc;AAEd;AAAA,EAEE;AAAA,EAEA;AAAA,EAEA;AAAA,OACK;AACP,cAAc;","names":[]}
1
+ {"version":3,"sources":["../src/search.ts"],"sourcesContent":["// `@eifi1/ui-kit/search` — the command palette / global search.\n// A re-slicing of the main barrel, not a new API.\nexport * from \"./search/command-palette\";\n// Named, not `*`: 0.53 took `SEARCH_TIER_POINTS` and `normalizeSearchText` off the surface\n// (deprecated in 0.52, refactor audit 2026-10-30, API-4); the index keeps them.\nexport {\n createSearchIndex,\n type SearchEntry,\n type SearchMatchField,\n type SearchHit,\n type SearchIndexOptions,\n type SearchIndex,\n} from \"./search/search-index\";\nexport * from \"./search/global-search\";\n"],"mappings":";AAEA,cAAc;AAGd;AAAA,EACE;AAAA,OAMK;AACP,cAAc;","names":[]}
@@ -18,11 +18,15 @@ export interface AccountMenuLanguageOption {
18
18
  */
19
19
  country?: string;
20
20
  }
21
- /** {@link AccountMenuLanguageProps} but the handler. */
22
- interface AccountMenuLanguageBaseProps {
21
+ export interface AccountMenuLanguageProps {
23
22
  languages: readonly AccountMenuLanguageOption[];
24
23
  /** The active language's code. */
25
24
  value: string | undefined;
25
+ /** A pick: the app sets the device's language and, signed in, the account's
26
+ * (settings §6.2). `value` + `onChange`, as every kit control pairs them
27
+ * (CONTRIBUTING §4; refactor audit 2026-10-30, API-5). Required: 0.53 removed the
28
+ * deprecated `onPick`. */
29
+ onChange: (code: string) => void;
26
30
  /** The account menu's `close` (from `TopBarActionMenu`'s `children`): a pick shuts it. */
27
31
  close?: () => void;
28
32
  /** The row's name. Default: `topBar.language` ("Language"). */
@@ -30,23 +34,6 @@ interface AccountMenuLanguageBaseProps {
30
34
  /** Open the sub-list at first. Default `false`. */
31
35
  defaultOpen?: boolean;
32
36
  }
33
- /**
34
- * `onChange`, or the deprecated `onPick`, is required: with both optional, a row an app
35
- * moved off `onPick` without adding `onChange` — or with it misspelt — compiled, and a
36
- * pick only closed the menu (0.52 code review, account-menu-language.tsx).
37
- */
38
- export type AccountMenuLanguageProps = AccountMenuLanguageBaseProps & ({
39
- /** A pick: the app sets the device's language and, signed in, the account's
40
- * (settings §6.2). `value` + `onChange`, as every kit control pairs them
41
- * (CONTRIBUTING §4; refactor audit 2026-10-30, API-5). */
42
- onChange: (code: string) => void;
43
- /** @deprecated 0.52 — `onChange` (the same call); removed in 0.53. */
44
- onPick?: (code: string) => void;
45
- } | {
46
- onChange?: undefined;
47
- /** @deprecated 0.52 — `onChange` (the same call); removed in 0.53. */
48
- onPick: (code: string) => void;
49
- });
50
37
  /**
51
38
  * The language choice INSIDE the account menu (docs/top-bar-harmonization.md §2.3),
52
39
  * keksdose's sub-list: a `Disclosure variant="menu"` row — the globe, "Language", the
@@ -58,5 +45,4 @@ export type AccountMenuLanguageProps = AccountMenuLanguageBaseProps & ({
58
45
  * app's context rows (§6.5). Signed out there is no account menu; `LanguageMenu` stays
59
46
  * the bar's button (§2.3).
60
47
  */
61
- export declare function AccountMenuLanguage({ languages, value, onChange, onPick, close, label, defaultOpen, }: AccountMenuLanguageProps): import("react").JSX.Element;
62
- export {};
48
+ export declare function AccountMenuLanguage({ languages, value, onChange, close, label, defaultOpen, }: AccountMenuLanguageProps): import("react").JSX.Element;
@@ -22,12 +22,10 @@ function AccountMenuLanguage({
22
22
  languages,
23
23
  value,
24
24
  onChange,
25
- onPick,
26
25
  close,
27
26
  label,
28
27
  defaultOpen = false
29
28
  }) {
30
- const change = onChange ?? onPick;
31
29
  const words = useKitLabels("topBar", DEFAULT_TOP_BAR_LABELS);
32
30
  const [open, setOpen] = useState(defaultOpen);
33
31
  const active = languages.find((l) => l.code === value);
@@ -56,7 +54,7 @@ function AccountMenuLanguage({
56
54
  {
57
55
  checked: lang.code === value,
58
56
  onClick: () => {
59
- change?.(lang.code);
57
+ onChange(lang.code);
60
58
  setOpen(false);
61
59
  close?.();
62
60
  },
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/shell/account-menu-language.tsx"],"sourcesContent":["import { useState } from \"react\";\nimport type { ReactNode } from \"react\";\nimport { Globe } from \"lucide-react\";\nimport { Disclosure } from \"../components/disclosure\";\nimport { MenuItem } from \"../components/menu-item\";\nimport { DEFAULT_TOP_BAR_LABELS, useKitLabels } from \"../i18n/kit-labels\";\nimport { languageCodeLabel } from \"./language-code\";\n\n/** One offered language in the account menu's sub-list. */\nexport interface AccountMenuLanguageOption {\n code: string;\n /** Its name, in its own language (\"Deutsch\", \"Français\"). */\n label: ReactNode;\n /**\n * A flag before the name, as the app draws it — keksdose's flag-icons span\n * (`<span aria-hidden className=\"fi fi-ch …\" />`). A node rather than a country code, so\n * the kit ships no flag set; decorative, so give it `aria-hidden`.\n */\n flag?: ReactNode;\n /**\n * Or the ISO 3166-1 alpha-2 country (lowercase) whose flag-icons flag to draw, as\n * `LanguageMenu`'s `LanguageOption` takes it — so one language list feeds both\n * (Kurvenschmiede's `LANGUAGES`). Needs the app's flag-icons CSS, as `LanguageMenu`\n * does. `flag` wins when both are given.\n */\n country?: string;\n}\n\n/** The option's flag: its own node, else the flag-icons flag of its country. */\nfunction flagOf(lang: AccountMenuLanguageOption): ReactNode {\n if (lang.flag !== undefined) return lang.flag;\n if (!lang.country) return undefined;\n return (\n <span\n aria-hidden\n data-country={lang.country}\n className={`fi fi-${lang.country} inline-block h-[0.9375rem] w-5 shrink-0 rounded-sm shadow-[0_0_0_1px_rgba(0,0,0,0.08)]`}\n />\n );\n}\n\n/** {@link AccountMenuLanguageProps} but the handler. */\ninterface AccountMenuLanguageBaseProps {\n languages: readonly AccountMenuLanguageOption[];\n /** The active language's code. */\n value: string | undefined;\n /** The account menu's `close` (from `TopBarActionMenu`'s `children`): a pick shuts it. */\n close?: () => void;\n /** The row's name. Default: `topBar.language` (\"Language\"). */\n label?: string;\n /** Open the sub-list at first. Default `false`. */\n defaultOpen?: boolean;\n}\n\n/**\n * `onChange`, or the deprecated `onPick`, is required: with both optional, a row an app\n * moved off `onPick` without adding `onChange` — or with it misspelt — compiled, and a\n * pick only closed the menu (0.52 code review, account-menu-language.tsx).\n */\nexport type AccountMenuLanguageProps = AccountMenuLanguageBaseProps &\n (\n | {\n /** A pick: the app sets the device's language and, signed in, the account's\n * (settings §6.2). `value` + `onChange`, as every kit control pairs them\n * (CONTRIBUTING §4; refactor audit 2026-10-30, API-5). */\n onChange: (code: string) => void;\n /** @deprecated 0.52 — `onChange` (the same call); removed in 0.53. */\n onPick?: (code: string) => void;\n }\n | {\n onChange?: undefined;\n /** @deprecated 0.52 — `onChange` (the same call); removed in 0.53. */\n onPick: (code: string) => void;\n }\n );\n\n/**\n * The language choice INSIDE the account menu (docs/top-bar-harmonization.md §2.3),\n * keksdose's sub-list: a `Disclosure variant=\"menu\"` row — the globe, \"Language\", the\n * active language's flag and code (\"DE\") and the chevron — that opens in place onto one checked\n * `MenuItem` per language. Inline rather than a nested popover: a popover inside a\n * popover fights the outside click and has nowhere to go on a phone (keksdose #140).\n *\n * It renders its own `<li>`: drop it into `TopBarActionMenu`'s `children` first after the\n * app's context rows (§6.5). Signed out there is no account menu; `LanguageMenu` stays\n * the bar's button (§2.3).\n */\nexport function AccountMenuLanguage({\n languages,\n value,\n onChange,\n onPick,\n close,\n label,\n defaultOpen = false,\n}: AccountMenuLanguageProps) {\n const change = onChange ?? onPick;\n const words = useKitLabels(\"topBar\", DEFAULT_TOP_BAR_LABELS);\n const [open, setOpen] = useState(defaultOpen);\n const active = languages.find((l) => l.code === value);\n return (\n <li>\n <Disclosure\n variant=\"menu\"\n open={open}\n onOpenChange={setOpen}\n title={\n <span className=\"flex items-center justify-between gap-3\">\n <span className=\"flex items-center gap-2\">\n <Globe aria-hidden className=\"size-4\" />\n {label ?? words.language}\n </span>\n {active && (\n // The flag alone could not tell de-CH, fr-CH and it-CH apart (one Swiss\n // flag each), so the code sits beside it (0.49 KIT-10); the row's name\n // carries the language's own name.\n <span className=\"flex items-center gap-1.5\">\n {flagOf(active)}\n <span aria-hidden className=\"text-xs font-semibold tabular-nums tracking-wide text-secondary\">\n {languageCodeLabel(active.code)}\n </span>\n <span className=\"sr-only\">{active.label}</span>\n </span>\n )}\n </span>\n }\n >\n <ul>\n {languages.map((lang) => (\n <li key={lang.code}>\n <MenuItem\n checked={lang.code === value}\n onClick={() => {\n change?.(lang.code);\n setOpen(false);\n close?.();\n }}\n leading={flagOf(lang)}\n // Under the row's own text, past the globe.\n className=\"ps-9\"\n >\n {lang.label}\n </MenuItem>\n </li>\n ))}\n </ul>\n </Disclosure>\n </li>\n );\n}\n"],"mappings":";AAiCI,cA2EQ,YA3ER;AAjCJ,SAAS,gBAAgB;AAEzB,SAAS,aAAa;AACtB,SAAS,kBAAkB;AAC3B,SAAS,gBAAgB;AACzB,SAAS,wBAAwB,oBAAoB;AACrD,SAAS,yBAAyB;AAuBlC,SAAS,OAAO,MAA4C;AAC1D,MAAI,KAAK,SAAS,OAAW,QAAO,KAAK;AACzC,MAAI,CAAC,KAAK,QAAS,QAAO;AAC1B,SACE;AAAA,IAAC;AAAA;AAAA,MACC,eAAW;AAAA,MACX,gBAAc,KAAK;AAAA,MACnB,WAAW,SAAS,KAAK,OAAO;AAAA;AAAA,EAClC;AAEJ;AAgDO,SAAS,oBAAoB;AAAA,EAClC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,cAAc;AAChB,GAA6B;AAC3B,QAAM,SAAS,YAAY;AAC3B,QAAM,QAAQ,aAAa,UAAU,sBAAsB;AAC3D,QAAM,CAAC,MAAM,OAAO,IAAI,SAAS,WAAW;AAC5C,QAAM,SAAS,UAAU,KAAK,CAAC,MAAM,EAAE,SAAS,KAAK;AACrD,SACE,oBAAC,QACC;AAAA,IAAC;AAAA;AAAA,MACC,SAAQ;AAAA,MACR;AAAA,MACA,cAAc;AAAA,MACd,OACE,qBAAC,UAAK,WAAU,2CACd;AAAA,6BAAC,UAAK,WAAU,2BACd;AAAA,8BAAC,SAAM,eAAW,MAAC,WAAU,UAAS;AAAA,UACrC,SAAS,MAAM;AAAA,WAClB;AAAA,QACC;AAAA;AAAA;AAAA,QAIC,qBAAC,UAAK,WAAU,6BACb;AAAA,iBAAO,MAAM;AAAA,UACd,oBAAC,UAAK,eAAW,MAAC,WAAU,mEACzB,4BAAkB,OAAO,IAAI,GAChC;AAAA,UACA,oBAAC,UAAK,WAAU,WAAW,iBAAO,OAAM;AAAA,WAC1C;AAAA,SAEJ;AAAA,MAGF,8BAAC,QACE,oBAAU,IAAI,CAAC,SACd,oBAAC,QACC;AAAA,QAAC;AAAA;AAAA,UACC,SAAS,KAAK,SAAS;AAAA,UACvB,SAAS,MAAM;AACb,qBAAS,KAAK,IAAI;AAClB,oBAAQ,KAAK;AACb,oBAAQ;AAAA,UACV;AAAA,UACA,SAAS,OAAO,IAAI;AAAA,UAEpB,WAAU;AAAA,UAET,eAAK;AAAA;AAAA,MACR,KAbO,KAAK,IAcd,CACD,GACH;AAAA;AAAA,EACF,GACF;AAEJ;","names":[]}
1
+ {"version":3,"sources":["../../src/shell/account-menu-language.tsx"],"sourcesContent":["import { useState } from \"react\";\nimport type { ReactNode } from \"react\";\nimport { Globe } from \"lucide-react\";\nimport { Disclosure } from \"../components/disclosure\";\nimport { MenuItem } from \"../components/menu-item\";\nimport { DEFAULT_TOP_BAR_LABELS, useKitLabels } from \"../i18n/kit-labels\";\nimport { languageCodeLabel } from \"./language-code\";\n\n/** One offered language in the account menu's sub-list. */\nexport interface AccountMenuLanguageOption {\n code: string;\n /** Its name, in its own language (\"Deutsch\", \"Français\"). */\n label: ReactNode;\n /**\n * A flag before the name, as the app draws it — keksdose's flag-icons span\n * (`<span aria-hidden className=\"fi fi-ch …\" />`). A node rather than a country code, so\n * the kit ships no flag set; decorative, so give it `aria-hidden`.\n */\n flag?: ReactNode;\n /**\n * Or the ISO 3166-1 alpha-2 country (lowercase) whose flag-icons flag to draw, as\n * `LanguageMenu`'s `LanguageOption` takes it — so one language list feeds both\n * (Kurvenschmiede's `LANGUAGES`). Needs the app's flag-icons CSS, as `LanguageMenu`\n * does. `flag` wins when both are given.\n */\n country?: string;\n}\n\n/** The option's flag: its own node, else the flag-icons flag of its country. */\nfunction flagOf(lang: AccountMenuLanguageOption): ReactNode {\n if (lang.flag !== undefined) return lang.flag;\n if (!lang.country) return undefined;\n return (\n <span\n aria-hidden\n data-country={lang.country}\n className={`fi fi-${lang.country} inline-block h-[0.9375rem] w-5 shrink-0 rounded-sm shadow-[0_0_0_1px_rgba(0,0,0,0.08)]`}\n />\n );\n}\n\nexport interface AccountMenuLanguageProps {\n languages: readonly AccountMenuLanguageOption[];\n /** The active language's code. */\n value: string | undefined;\n /** A pick: the app sets the device's language and, signed in, the account's\n * (settings §6.2). `value` + `onChange`, as every kit control pairs them\n * (CONTRIBUTING §4; refactor audit 2026-10-30, API-5). Required: 0.53 removed the\n * deprecated `onPick`. */\n onChange: (code: string) => void;\n /** The account menu's `close` (from `TopBarActionMenu`'s `children`): a pick shuts it. */\n close?: () => void;\n /** The row's name. Default: `topBar.language` (\"Language\"). */\n label?: string;\n /** Open the sub-list at first. Default `false`. */\n defaultOpen?: boolean;\n}\n\n/**\n * The language choice INSIDE the account menu (docs/top-bar-harmonization.md §2.3),\n * keksdose's sub-list: a `Disclosure variant=\"menu\"` row — the globe, \"Language\", the\n * active language's flag and code (\"DE\") and the chevron — that opens in place onto one checked\n * `MenuItem` per language. Inline rather than a nested popover: a popover inside a\n * popover fights the outside click and has nowhere to go on a phone (keksdose #140).\n *\n * It renders its own `<li>`: drop it into `TopBarActionMenu`'s `children` first after the\n * app's context rows (§6.5). Signed out there is no account menu; `LanguageMenu` stays\n * the bar's button (§2.3).\n */\nexport function AccountMenuLanguage({\n languages,\n value,\n onChange,\n close,\n label,\n defaultOpen = false,\n}: AccountMenuLanguageProps) {\n const words = useKitLabels(\"topBar\", DEFAULT_TOP_BAR_LABELS);\n const [open, setOpen] = useState(defaultOpen);\n const active = languages.find((l) => l.code === value);\n return (\n <li>\n <Disclosure\n variant=\"menu\"\n open={open}\n onOpenChange={setOpen}\n title={\n <span className=\"flex items-center justify-between gap-3\">\n <span className=\"flex items-center gap-2\">\n <Globe aria-hidden className=\"size-4\" />\n {label ?? words.language}\n </span>\n {active && (\n // The flag alone could not tell de-CH, fr-CH and it-CH apart (one Swiss\n // flag each), so the code sits beside it (0.49 KIT-10); the row's name\n // carries the language's own name.\n <span className=\"flex items-center gap-1.5\">\n {flagOf(active)}\n <span aria-hidden className=\"text-xs font-semibold tabular-nums tracking-wide text-secondary\">\n {languageCodeLabel(active.code)}\n </span>\n <span className=\"sr-only\">{active.label}</span>\n </span>\n )}\n </span>\n }\n >\n <ul>\n {languages.map((lang) => (\n <li key={lang.code}>\n <MenuItem\n checked={lang.code === value}\n onClick={() => {\n onChange(lang.code);\n setOpen(false);\n close?.();\n }}\n leading={flagOf(lang)}\n // Under the row's own text, past the globe.\n className=\"ps-9\"\n >\n {lang.label}\n </MenuItem>\n </li>\n ))}\n </ul>\n </Disclosure>\n </li>\n );\n}\n"],"mappings":";AAiCI,cAuDQ,YAvDR;AAjCJ,SAAS,gBAAgB;AAEzB,SAAS,aAAa;AACtB,SAAS,kBAAkB;AAC3B,SAAS,gBAAgB;AACzB,SAAS,wBAAwB,oBAAoB;AACrD,SAAS,yBAAyB;AAuBlC,SAAS,OAAO,MAA4C;AAC1D,MAAI,KAAK,SAAS,OAAW,QAAO,KAAK;AACzC,MAAI,CAAC,KAAK,QAAS,QAAO;AAC1B,SACE;AAAA,IAAC;AAAA;AAAA,MACC,eAAW;AAAA,MACX,gBAAc,KAAK;AAAA,MACnB,WAAW,SAAS,KAAK,OAAO;AAAA;AAAA,EAClC;AAEJ;AA8BO,SAAS,oBAAoB;AAAA,EAClC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,cAAc;AAChB,GAA6B;AAC3B,QAAM,QAAQ,aAAa,UAAU,sBAAsB;AAC3D,QAAM,CAAC,MAAM,OAAO,IAAI,SAAS,WAAW;AAC5C,QAAM,SAAS,UAAU,KAAK,CAAC,MAAM,EAAE,SAAS,KAAK;AACrD,SACE,oBAAC,QACC;AAAA,IAAC;AAAA;AAAA,MACC,SAAQ;AAAA,MACR;AAAA,MACA,cAAc;AAAA,MACd,OACE,qBAAC,UAAK,WAAU,2CACd;AAAA,6BAAC,UAAK,WAAU,2BACd;AAAA,8BAAC,SAAM,eAAW,MAAC,WAAU,UAAS;AAAA,UACrC,SAAS,MAAM;AAAA,WAClB;AAAA,QACC;AAAA;AAAA;AAAA,QAIC,qBAAC,UAAK,WAAU,6BACb;AAAA,iBAAO,MAAM;AAAA,UACd,oBAAC,UAAK,eAAW,MAAC,WAAU,mEACzB,4BAAkB,OAAO,IAAI,GAChC;AAAA,UACA,oBAAC,UAAK,WAAU,WAAW,iBAAO,OAAM;AAAA,WAC1C;AAAA,SAEJ;AAAA,MAGF,8BAAC,QACE,oBAAU,IAAI,CAAC,SACd,oBAAC,QACC;AAAA,QAAC;AAAA;AAAA,UACC,SAAS,KAAK,SAAS;AAAA,UACvB,SAAS,MAAM;AACb,qBAAS,KAAK,IAAI;AAClB,oBAAQ,KAAK;AACb,oBAAQ;AAAA,UACV;AAAA,UACA,SAAS,OAAO,IAAI;AAAA,UAEpB,WAAU;AAAA,UAET,eAAK;AAAA;AAAA,MACR,KAbO,KAAK,IAcd,CACD,GACH;AAAA;AAAA,EACF,GACF;AAEJ;","names":[]}
@@ -1,38 +1,25 @@
1
1
  import { type AppearanceLabels } from "../components/appearance-labels.js";
2
2
  import type { LabelOverride } from "../i18n/kit-labels.js";
3
3
  import { type TextSize } from "../theme/text-size.js";
4
- /** {@link AccountMenuTextSizeProps} but the handler. */
5
- interface AccountMenuTextSizeBaseProps {
4
+ export interface AccountMenuTextSizeProps {
6
5
  /** The size in force. Default: the one on `<html>` ({@link useTextSize}), which is the
7
6
  * size whoever applied it put there. */
8
7
  value?: TextSize;
9
- /** The row's name. Default: `appearance.textSize` ("Text size"). */
10
- label?: string;
11
- /** The `appearance` words (`textSize`, `textSizes`, `textSmaller`, `textLarger`). */
12
- labels?: LabelOverride<AppearanceLabels>;
13
- }
14
- /**
15
- * `onChange`, or the deprecated `onPick`, is required: with both optional, a row an app
16
- * moved off `onPick` without adding `onChange` — or with it misspelt — compiled, and A− /
17
- * A+ stayed enabled and changed nothing (0.52 code review, account-menu-language.tsx).
18
- */
19
- export type AccountMenuTextSizeProps = AccountMenuTextSizeBaseProps & ({
20
8
  /**
21
9
  * A step: the next size down or up. The app applies it and keeps it as its
22
10
  * Settings ▸ Appearance does — `useAccountAppearance`'s `pickTextSize` (this
23
11
  * device, and the account when signed in), with `appearance.saveFailed` as the
24
12
  * toast when the account refused. The kit does not apply it itself, so the two
25
13
  * places can never disagree. `value` + `onChange`, as every kit control pairs them
26
- * (CONTRIBUTING §4; refactor audit 2026-10-30, API-5).
14
+ * (CONTRIBUTING §4; refactor audit 2026-10-30, API-5). Required: 0.53 removed the
15
+ * deprecated `onPick`.
27
16
  */
28
17
  onChange: (size: TextSize) => void;
29
- /** @deprecated 0.52 — `onChange` (the same call); removed in 0.53. */
30
- onPick?: (size: TextSize) => void;
31
- } | {
32
- onChange?: undefined;
33
- /** @deprecated 0.52 — `onChange` (the same call); removed in 0.53. */
34
- onPick: (size: TextSize) => void;
35
- });
18
+ /** The row's name. Default: `appearance.textSize` ("Text size"). */
19
+ label?: string;
20
+ /** The `appearance` words (`textSize`, `textSizes`, `textSmaller`, `textLarger`). */
21
+ labels?: LabelOverride<AppearanceLabels>;
22
+ }
36
23
  /**
37
24
  * The text size one tap away, INSIDE the account menu (0.46, keksdose live #426, its
38
25
  * `TextSizeRow`): the row's icon and name, the size in force, and A− / A+, which step
@@ -46,5 +33,4 @@ export type AccountMenuTextSizeProps = AccountMenuTextSizeBaseProps & ({
46
33
  * behind it and can step again. The size's name is a polite live region, so a step is
47
34
  * announced.
48
35
  */
49
- export declare function AccountMenuTextSize({ value, onChange, onPick, label, labels: labelsProp }: AccountMenuTextSizeProps): import("react").JSX.Element;
50
- export {};
36
+ export declare function AccountMenuTextSize({ value, onChange, label, labels: labelsProp }: AccountMenuTextSizeProps): import("react").JSX.Element;
@@ -4,15 +4,14 @@ import { AArrowDown, AArrowUp, ALargeSmall } from "lucide-react";
4
4
  import { IconButton } from "../components/icon-button.js";
5
5
  import { useAppearanceLabels } from "../components/appearance-labels.js";
6
6
  import { TEXT_SIZES, useTextSize } from "../theme/text-size.js";
7
- function AccountMenuTextSize({ value, onChange, onPick, label, labels: labelsProp }) {
8
- const change = onChange ?? onPick;
7
+ function AccountMenuTextSize({ value, onChange, label, labels: labelsProp }) {
9
8
  const labels = useAppearanceLabels(labelsProp);
10
9
  const onHtml = useTextSize().size;
11
10
  const size = value ?? onHtml;
12
11
  const index = TEXT_SIZES.indexOf(size);
13
12
  const step = (by) => {
14
13
  const next = TEXT_SIZES[index + by];
15
- if (next) change?.(next);
14
+ if (next) onChange(next);
16
15
  };
17
16
  return /* @__PURE__ */ jsxs("li", { "data-account-menu-text-size": "", className: "flex items-center gap-2 px-3 py-1.5 text-sm", children: [
18
17
  /* @__PURE__ */ jsx(ALargeSmall, { "aria-hidden": true, className: "size-4 shrink-0 text-muted" }),
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/shell/account-menu-text-size.tsx"],"sourcesContent":["import { AArrowDown, AArrowUp, ALargeSmall } from \"lucide-react\";\nimport { IconButton } from \"../components/icon-button\";\nimport { useAppearanceLabels, type AppearanceLabels } from \"../components/appearance-labels\";\nimport type { LabelOverride } from \"../i18n/kit-labels\";\nimport { TEXT_SIZES, useTextSize, type TextSize } from \"../theme/text-size\";\n\n/** {@link AccountMenuTextSizeProps} but the handler. */\ninterface AccountMenuTextSizeBaseProps {\n /** The size in force. Default: the one on `<html>` ({@link useTextSize}), which is the\n * size whoever applied it put there. */\n value?: TextSize;\n /** The row's name. Default: `appearance.textSize` (\"Text size\"). */\n label?: string;\n /** The `appearance` words (`textSize`, `textSizes`, `textSmaller`, `textLarger`). */\n labels?: LabelOverride<AppearanceLabels>;\n}\n\n/**\n * `onChange`, or the deprecated `onPick`, is required: with both optional, a row an app\n * moved off `onPick` without adding `onChange` — or with it misspelt — compiled, and A− /\n * A+ stayed enabled and changed nothing (0.52 code review, account-menu-language.tsx).\n */\nexport type AccountMenuTextSizeProps = AccountMenuTextSizeBaseProps &\n (\n | {\n /**\n * A step: the next size down or up. The app applies it and keeps it as its\n * Settings ▸ Appearance does — `useAccountAppearance`'s `pickTextSize` (this\n * device, and the account when signed in), with `appearance.saveFailed` as the\n * toast when the account refused. The kit does not apply it itself, so the two\n * places can never disagree. `value` + `onChange`, as every kit control pairs them\n * (CONTRIBUTING §4; refactor audit 2026-10-30, API-5).\n */\n onChange: (size: TextSize) => void;\n /** @deprecated 0.52 — `onChange` (the same call); removed in 0.53. */\n onPick?: (size: TextSize) => void;\n }\n | {\n onChange?: undefined;\n /** @deprecated 0.52 — `onChange` (the same call); removed in 0.53. */\n onPick: (size: TextSize) => void;\n }\n );\n\n/**\n * The text size one tap away, INSIDE the account menu (0.46, keksdose live #426, its\n * `TextSizeRow`): the row's icon and name, the size in force, and A− / A+, which step\n * through {@link TEXT_SIZES} (Normal → Large → Extra large). A− is disabled at the\n * smallest, A+ at the largest. The browser's pinch zoom stays as it is — this is the\n * quick path to the setting, not a replacement for zoom.\n *\n * It renders its own `<li>`: drop it into `TopBarActionMenu`'s `children` beside\n * `AccountMenuLanguage`. The menu promotes the two buttons to `menuitem`s, so ↑/↓ reach\n * them like every other row, and it stays open on a step: the person sees the page grow\n * behind it and can step again. The size's name is a polite live region, so a step is\n * announced.\n */\nexport function AccountMenuTextSize({ value, onChange, onPick, label, labels: labelsProp }: AccountMenuTextSizeProps) {\n const change = onChange ?? onPick;\n const labels = useAppearanceLabels(labelsProp);\n const onHtml = useTextSize().size;\n const size = value ?? onHtml;\n const index = TEXT_SIZES.indexOf(size);\n const step = (by: -1 | 1) => {\n const next = TEXT_SIZES[index + by];\n if (next) change?.(next);\n };\n return (\n <li data-account-menu-text-size=\"\" className=\"flex items-center gap-2 px-3 py-1.5 text-sm\">\n <ALargeSmall aria-hidden className=\"size-4 shrink-0 text-muted\" />\n <span className=\"min-w-0 flex-1 truncate\">\n {label ?? labels.textSize}\n <span className=\"ms-2 text-xs text-muted\" aria-live=\"polite\">\n {labels.textSizes[size]}\n </span>\n </span>\n <IconButton\n size=\"sm\"\n label={labels.textSmaller}\n labelVisible={false}\n tooltip={false}\n disabled={index <= 0}\n onClick={() => step(-1)}\n >\n <AArrowDown />\n </IconButton>\n <IconButton\n size=\"sm\"\n label={labels.textLarger}\n labelVisible={false}\n tooltip={false}\n disabled={index >= TEXT_SIZES.length - 1}\n onClick={() => step(1)}\n >\n <AArrowUp />\n </IconButton>\n </li>\n );\n}\n"],"mappings":";AAqEM,cACA,YADA;AArEN,SAAS,YAAY,UAAU,mBAAmB;AAClD,SAAS,kBAAkB;AAC3B,SAAS,2BAAkD;AAE3D,SAAS,YAAY,mBAAkC;AAqDhD,SAAS,oBAAoB,EAAE,OAAO,UAAU,QAAQ,OAAO,QAAQ,WAAW,GAA6B;AACpH,QAAM,SAAS,YAAY;AAC3B,QAAM,SAAS,oBAAoB,UAAU;AAC7C,QAAM,SAAS,YAAY,EAAE;AAC7B,QAAM,OAAO,SAAS;AACtB,QAAM,QAAQ,WAAW,QAAQ,IAAI;AACrC,QAAM,OAAO,CAAC,OAAe;AAC3B,UAAM,OAAO,WAAW,QAAQ,EAAE;AAClC,QAAI,KAAM,UAAS,IAAI;AAAA,EACzB;AACA,SACE,qBAAC,QAAG,+BAA4B,IAAG,WAAU,+CAC3C;AAAA,wBAAC,eAAY,eAAW,MAAC,WAAU,8BAA6B;AAAA,IAChE,qBAAC,UAAK,WAAU,2BACb;AAAA,eAAS,OAAO;AAAA,MACjB,oBAAC,UAAK,WAAU,2BAA0B,aAAU,UACjD,iBAAO,UAAU,IAAI,GACxB;AAAA,OACF;AAAA,IACA;AAAA,MAAC;AAAA;AAAA,QACC,MAAK;AAAA,QACL,OAAO,OAAO;AAAA,QACd,cAAc;AAAA,QACd,SAAS;AAAA,QACT,UAAU,SAAS;AAAA,QACnB,SAAS,MAAM,KAAK,EAAE;AAAA,QAEtB,8BAAC,cAAW;AAAA;AAAA,IACd;AAAA,IACA;AAAA,MAAC;AAAA;AAAA,QACC,MAAK;AAAA,QACL,OAAO,OAAO;AAAA,QACd,cAAc;AAAA,QACd,SAAS;AAAA,QACT,UAAU,SAAS,WAAW,SAAS;AAAA,QACvC,SAAS,MAAM,KAAK,CAAC;AAAA,QAErB,8BAAC,YAAS;AAAA;AAAA,IACZ;AAAA,KACF;AAEJ;","names":[]}
1
+ {"version":3,"sources":["../../src/shell/account-menu-text-size.tsx"],"sourcesContent":["import { AArrowDown, AArrowUp, ALargeSmall } from \"lucide-react\";\nimport { IconButton } from \"../components/icon-button\";\nimport { useAppearanceLabels, type AppearanceLabels } from \"../components/appearance-labels\";\nimport type { LabelOverride } from \"../i18n/kit-labels\";\nimport { TEXT_SIZES, useTextSize, type TextSize } from \"../theme/text-size\";\n\nexport interface AccountMenuTextSizeProps {\n /** The size in force. Default: the one on `<html>` ({@link useTextSize}), which is the\n * size whoever applied it put there. */\n value?: TextSize;\n /**\n * A step: the next size down or up. The app applies it and keeps it as its\n * Settings ▸ Appearance does — `useAccountAppearance`'s `pickTextSize` (this\n * device, and the account when signed in), with `appearance.saveFailed` as the\n * toast when the account refused. The kit does not apply it itself, so the two\n * places can never disagree. `value` + `onChange`, as every kit control pairs them\n * (CONTRIBUTING §4; refactor audit 2026-10-30, API-5). Required: 0.53 removed the\n * deprecated `onPick`.\n */\n onChange: (size: TextSize) => void;\n /** The row's name. Default: `appearance.textSize` (\"Text size\"). */\n label?: string;\n /** The `appearance` words (`textSize`, `textSizes`, `textSmaller`, `textLarger`). */\n labels?: LabelOverride<AppearanceLabels>;\n}\n\n/**\n * The text size one tap away, INSIDE the account menu (0.46, keksdose live #426, its\n * `TextSizeRow`): the row's icon and name, the size in force, and A− / A+, which step\n * through {@link TEXT_SIZES} (Normal → Large → Extra large). A− is disabled at the\n * smallest, A+ at the largest. The browser's pinch zoom stays as it is — this is the\n * quick path to the setting, not a replacement for zoom.\n *\n * It renders its own `<li>`: drop it into `TopBarActionMenu`'s `children` beside\n * `AccountMenuLanguage`. The menu promotes the two buttons to `menuitem`s, so ↑/↓ reach\n * them like every other row, and it stays open on a step: the person sees the page grow\n * behind it and can step again. The size's name is a polite live region, so a step is\n * announced.\n */\nexport function AccountMenuTextSize({ value, onChange, label, labels: labelsProp }: AccountMenuTextSizeProps) {\n const labels = useAppearanceLabels(labelsProp);\n const onHtml = useTextSize().size;\n const size = value ?? onHtml;\n const index = TEXT_SIZES.indexOf(size);\n const step = (by: -1 | 1) => {\n const next = TEXT_SIZES[index + by];\n if (next) onChange(next);\n };\n return (\n <li data-account-menu-text-size=\"\" className=\"flex items-center gap-2 px-3 py-1.5 text-sm\">\n <ALargeSmall aria-hidden className=\"size-4 shrink-0 text-muted\" />\n <span className=\"min-w-0 flex-1 truncate\">\n {label ?? labels.textSize}\n <span className=\"ms-2 text-xs text-muted\" aria-live=\"polite\">\n {labels.textSizes[size]}\n </span>\n </span>\n <IconButton\n size=\"sm\"\n label={labels.textSmaller}\n labelVisible={false}\n tooltip={false}\n disabled={index <= 0}\n onClick={() => step(-1)}\n >\n <AArrowDown />\n </IconButton>\n <IconButton\n size=\"sm\"\n label={labels.textLarger}\n labelVisible={false}\n tooltip={false}\n disabled={index >= TEXT_SIZES.length - 1}\n onClick={() => step(1)}\n >\n <AArrowUp />\n </IconButton>\n </li>\n );\n}\n"],"mappings":";AAkDM,cACA,YADA;AAlDN,SAAS,YAAY,UAAU,mBAAmB;AAClD,SAAS,kBAAkB;AAC3B,SAAS,2BAAkD;AAE3D,SAAS,YAAY,mBAAkC;AAmChD,SAAS,oBAAoB,EAAE,OAAO,UAAU,OAAO,QAAQ,WAAW,GAA6B;AAC5G,QAAM,SAAS,oBAAoB,UAAU;AAC7C,QAAM,SAAS,YAAY,EAAE;AAC7B,QAAM,OAAO,SAAS;AACtB,QAAM,QAAQ,WAAW,QAAQ,IAAI;AACrC,QAAM,OAAO,CAAC,OAAe;AAC3B,UAAM,OAAO,WAAW,QAAQ,EAAE;AAClC,QAAI,KAAM,UAAS,IAAI;AAAA,EACzB;AACA,SACE,qBAAC,QAAG,+BAA4B,IAAG,WAAU,+CAC3C;AAAA,wBAAC,eAAY,eAAW,MAAC,WAAU,8BAA6B;AAAA,IAChE,qBAAC,UAAK,WAAU,2BACb;AAAA,eAAS,OAAO;AAAA,MACjB,oBAAC,UAAK,WAAU,2BAA0B,aAAU,UACjD,iBAAO,UAAU,IAAI,GACxB;AAAA,OACF;AAAA,IACA;AAAA,MAAC;AAAA;AAAA,QACC,MAAK;AAAA,QACL,OAAO,OAAO;AAAA,QACd,cAAc;AAAA,QACd,SAAS;AAAA,QACT,UAAU,SAAS;AAAA,QACnB,SAAS,MAAM,KAAK,EAAE;AAAA,QAEtB,8BAAC,cAAW;AAAA;AAAA,IACd;AAAA,IACA;AAAA,MAAC;AAAA;AAAA,QACC,MAAK;AAAA,QACL,OAAO,OAAO;AAAA,QACd,cAAc;AAAA,QACd,SAAS;AAAA,QACT,UAAU,SAAS,WAAW,SAAS;AAAA,QACvC,SAAS,MAAM,KAAK,CAAC;AAAA,QAErB,8BAAC,YAAS;AAAA;AAAA,IACZ;AAAA,KACF;AAEJ;","names":[]}
@@ -144,16 +144,6 @@ export declare function pickMobileBar<T>(entries: readonly T[], maxItems: number
144
144
  shown: T[];
145
145
  hidden: T[];
146
146
  };
147
- /**
148
- * Split the bar's entries into the cells shown and the ones under "More", in the app's
149
- * order (§10.7: the app puts its four most used first, or orders by role). One hidden
150
- * entry is no saving — the More cell would take its place — so the bar then shows them
151
- * all: the cell count is the same either way, and a real entry beats a detour.
152
- */
153
- export declare function splitMobileBar<T>(entries: readonly T[], max: number): {
154
- shown: T[];
155
- hidden: T[];
156
- };
157
147
  /**
158
148
  * Exported and `<div>`-shaped. This is the outermost element of every consuming app, so
159
149
  * there is nothing above it to hang an id, a landmark label or a `data-tour` anchor on —
@@ -49,11 +49,6 @@ function pickMobileBar(entries, maxItems, priority) {
49
49
  hidden: entries.filter((_, i) => !chosen.has(i))
50
50
  };
51
51
  }
52
- function splitMobileBar(entries, max) {
53
- const limit = Math.max(1, Math.floor(max));
54
- if (entries.length <= limit + 1) return { shown: [...entries], hidden: [] };
55
- return { shown: entries.slice(0, limit), hidden: entries.slice(limit) };
56
- }
57
52
  function mobileCells(maxItems, size) {
58
53
  if (maxItems === void 0) return DEFAULT_MOBILE_MAX_ITEMS[size];
59
54
  return typeof maxItems === "number" ? maxItems : maxItems[size] ?? DEFAULT_MOBILE_MAX_ITEMS[size];
@@ -852,7 +847,6 @@ export {
852
847
  AppShell,
853
848
  DEFAULT_APP_SHELL_MORE_LABELS,
854
849
  DEFAULT_MOBILE_MAX_ITEMS,
855
- pickMobileBar,
856
- splitMobileBar
850
+ pickMobileBar
857
851
  };
858
852
  //# sourceMappingURL=app-shell.js.map