@flickfyi/flkr 0.0.141 → 0.0.146

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 (494) hide show
  1. package/node_modules/@flickfyi/attestation/.turbo/turbo-build.log +7 -7
  2. package/node_modules/@flickfyi/biscuit/.turbo/turbo-build.log +7 -7
  3. package/node_modules/@flickfyi/core/.oxlintrc.json +2 -1
  4. package/node_modules/@flickfyi/core/.turbo/turbo-build.log +502 -95
  5. package/node_modules/@flickfyi/core/README.md +58 -4
  6. package/node_modules/@flickfyi/core/dist/index.cjs +1 -1
  7. package/node_modules/@flickfyi/core/dist/index.js +35 -24
  8. package/node_modules/@flickfyi/core/dist/kowalski/calculations.d.ts +17 -0
  9. package/node_modules/@flickfyi/core/dist/kowalski/calculator.d.ts +13 -0
  10. package/node_modules/@flickfyi/core/dist/kowalski/catalog.cjs +1 -0
  11. package/node_modules/@flickfyi/core/dist/kowalski/catalog.d.cts +962 -0
  12. package/node_modules/@flickfyi/core/dist/kowalski/catalog.d.ts +962 -0
  13. package/node_modules/@flickfyi/core/dist/kowalski/catalog.js +639 -0
  14. package/node_modules/@flickfyi/core/dist/kowalski/clocks.d.ts +4 -0
  15. package/node_modules/@flickfyi/core/dist/kowalski/date-time.d.ts +22 -0
  16. package/node_modules/@flickfyi/core/dist/kowalski/details.d.ts +7 -0
  17. package/node_modules/@flickfyi/core/dist/kowalski/everyday.d.ts +8 -0
  18. package/node_modules/@flickfyi/core/dist/kowalski/format.d.ts +4 -0
  19. package/node_modules/@flickfyi/core/dist/kowalski/frame.d.ts +161 -0
  20. package/node_modules/@flickfyi/core/dist/kowalski/index.cjs +21 -0
  21. package/node_modules/@flickfyi/core/dist/kowalski/index.d.cts +8 -0
  22. package/node_modules/@flickfyi/core/dist/kowalski/index.d.ts +8 -0
  23. package/node_modules/@flickfyi/core/dist/kowalski/index.js +7496 -0
  24. package/node_modules/@flickfyi/core/dist/kowalski/productivity.d.ts +5 -0
  25. package/node_modules/@flickfyi/core/dist/kowalski/recipe.d.ts +8 -0
  26. package/node_modules/@flickfyi/core/dist/kowalski/tools.d.ts +7 -0
  27. package/node_modules/@flickfyi/core/dist/kowalski/use-clock.d.ts +6 -0
  28. package/node_modules/@flickfyi/core/dist/kowalski/use-configured-feature.d.ts +19 -0
  29. package/node_modules/@flickfyi/core/dist/kowalski/use-feature-store.d.ts +15 -0
  30. package/node_modules/@flickfyi/core/dist/kowalski.css +506 -0
  31. package/node_modules/@flickfyi/core/dist/molecules/field/field.d.ts +8 -0
  32. package/node_modules/@flickfyi/core/dist/molecules/index.cjs +1 -1
  33. package/node_modules/@flickfyi/core/dist/molecules/index.d.cts +3 -0
  34. package/node_modules/@flickfyi/core/dist/molecules/index.d.ts +3 -0
  35. package/node_modules/@flickfyi/core/dist/molecules/index.js +88 -27
  36. package/node_modules/@flickfyi/core/dist/molecules/segmented/segmented.d.ts +10 -0
  37. package/node_modules/@flickfyi/core/dist/molecules/stepper/stepper.d.ts +10 -0
  38. package/node_modules/@flickfyi/core/dist/quantum/use-store.d.ts +7 -1
  39. package/node_modules/@flickfyi/core/dist/themes.css +2 -0
  40. package/node_modules/@flickfyi/core/examples/kowalski/bill-split/bill-split.stories.tsx +15 -0
  41. package/node_modules/@flickfyi/core/examples/kowalski/bill-split/bill-split.tsx +17 -0
  42. package/node_modules/@flickfyi/core/examples/kowalski/bookmark/bookmark.stories.tsx +15 -0
  43. package/node_modules/@flickfyi/core/examples/kowalski/bookmark/bookmark.tsx +23 -0
  44. package/node_modules/@flickfyi/core/examples/kowalski/budget/budget.stories.tsx +15 -0
  45. package/node_modules/@flickfyi/core/examples/kowalski/budget/budget.tsx +26 -0
  46. package/node_modules/@flickfyi/core/examples/kowalski/calculator/calculator.stories.tsx +15 -0
  47. package/node_modules/@flickfyi/core/examples/kowalski/calculator/calculator.tsx +24 -0
  48. package/node_modules/@flickfyi/core/examples/kowalski/checklist/checklist.stories.tsx +15 -0
  49. package/node_modules/@flickfyi/core/examples/kowalski/checklist/checklist.tsx +26 -0
  50. package/node_modules/@flickfyi/core/examples/kowalski/color-palette/color-palette.stories.tsx +15 -0
  51. package/node_modules/@flickfyi/core/examples/kowalski/color-palette/color-palette.tsx +22 -0
  52. package/node_modules/@flickfyi/core/examples/kowalski/contact/contact.stories.tsx +15 -0
  53. package/node_modules/@flickfyi/core/examples/kowalski/contact/contact.tsx +23 -0
  54. package/node_modules/@flickfyi/core/examples/kowalski/countdown/countdown.stories.tsx +15 -0
  55. package/node_modules/@flickfyi/core/examples/kowalski/countdown/countdown.tsx +23 -0
  56. package/node_modules/@flickfyi/core/examples/kowalski/event/event.stories.tsx +15 -0
  57. package/node_modules/@flickfyi/core/examples/kowalski/event/event.tsx +24 -0
  58. package/node_modules/@flickfyi/core/examples/kowalski/expense/expense.stories.tsx +15 -0
  59. package/node_modules/@flickfyi/core/examples/kowalski/expense/expense.tsx +23 -0
  60. package/node_modules/@flickfyi/core/examples/kowalski/goal/goal.stories.tsx +15 -0
  61. package/node_modules/@flickfyi/core/examples/kowalski/goal/goal.tsx +24 -0
  62. package/node_modules/@flickfyi/core/examples/kowalski/habit/habit.stories.tsx +15 -0
  63. package/node_modules/@flickfyi/core/examples/kowalski/habit/habit.tsx +23 -0
  64. package/node_modules/@flickfyi/core/examples/kowalski/ledger/ledger.stories.tsx +15 -0
  65. package/node_modules/@flickfyi/core/examples/kowalski/ledger/ledger.tsx +25 -0
  66. package/node_modules/@flickfyi/core/examples/kowalski/note/note.stories.tsx +15 -0
  67. package/node_modules/@flickfyi/core/examples/kowalski/note/note.tsx +22 -0
  68. package/node_modules/@flickfyi/core/examples/kowalski/poll/poll.stories.tsx +15 -0
  69. package/node_modules/@flickfyi/core/examples/kowalski/poll/poll.tsx +26 -0
  70. package/node_modules/@flickfyi/core/examples/kowalski/random-picker/random-picker.stories.tsx +15 -0
  71. package/node_modules/@flickfyi/core/examples/kowalski/random-picker/random-picker.tsx +23 -0
  72. package/node_modules/@flickfyi/core/examples/kowalski/rating/rating.stories.tsx +15 -0
  73. package/node_modules/@flickfyi/core/examples/kowalski/rating/rating.tsx +19 -0
  74. package/node_modules/@flickfyi/core/examples/kowalski/recipe/recipe.stories.tsx +15 -0
  75. package/node_modules/@flickfyi/core/examples/kowalski/recipe/recipe.tsx +120 -0
  76. package/node_modules/@flickfyi/core/examples/kowalski/reminder/reminder.stories.tsx +15 -0
  77. package/node_modules/@flickfyi/core/examples/kowalski/reminder/reminder.tsx +23 -0
  78. package/node_modules/@flickfyi/core/examples/kowalski/rotation/rotation.stories.tsx +15 -0
  79. package/node_modules/@flickfyi/core/examples/kowalski/rotation/rotation.tsx +27 -0
  80. package/node_modules/@flickfyi/core/examples/kowalski/scoreboard/scoreboard.stories.tsx +15 -0
  81. package/node_modules/@flickfyi/core/examples/kowalski/scoreboard/scoreboard.tsx +26 -0
  82. package/node_modules/@flickfyi/core/examples/kowalski/shopping-list/shopping-list.stories.tsx +15 -0
  83. package/node_modules/@flickfyi/core/examples/kowalski/shopping-list/shopping-list.tsx +27 -0
  84. package/node_modules/@flickfyi/core/examples/kowalski/task-board/task-board.stories.tsx +15 -0
  85. package/node_modules/@flickfyi/core/examples/kowalski/task-board/task-board.tsx +27 -0
  86. package/node_modules/@flickfyi/core/examples/kowalski/theme.ts +3 -0
  87. package/node_modules/@flickfyi/core/examples/kowalski/time-zone/time-zone.stories.tsx +15 -0
  88. package/node_modules/@flickfyi/core/examples/kowalski/time-zone/time-zone.tsx +22 -0
  89. package/node_modules/@flickfyi/core/examples/kowalski/timer/timer.stories.tsx +15 -0
  90. package/node_modules/@flickfyi/core/examples/kowalski/timer/timer.tsx +23 -0
  91. package/node_modules/@flickfyi/core/examples/kowalski/trip/trip.stories.tsx +15 -0
  92. package/node_modules/@flickfyi/core/examples/kowalski/trip/trip.tsx +26 -0
  93. package/node_modules/@flickfyi/core/examples/kowalski/unit-converter/unit-converter.stories.tsx +15 -0
  94. package/node_modules/@flickfyi/core/examples/kowalski/unit-converter/unit-converter.tsx +24 -0
  95. package/node_modules/@flickfyi/core/examples/parrott/calculator/calculator.stories.tsx +3 -1
  96. package/node_modules/@flickfyi/core/examples/parrott/checklist/checklist.stories.tsx +3 -1
  97. package/node_modules/@flickfyi/core/examples/parrott/countdown/countdown.stories.tsx +3 -1
  98. package/node_modules/@flickfyi/core/examples/parrott/counter/counter.stories.tsx +1 -0
  99. package/node_modules/@flickfyi/core/examples/parrott/fortune/fortune.stories.tsx +3 -1
  100. package/node_modules/@flickfyi/core/examples/parrott/grid/grid.stories.tsx +3 -1
  101. package/node_modules/@flickfyi/core/examples/parrott/kale-smoothie/kale-smoothie.stories.tsx +1 -0
  102. package/node_modules/@flickfyi/core/examples/parrott/list/list.stories.tsx +3 -1
  103. package/node_modules/@flickfyi/core/examples/parrott/log/log.stories.tsx +3 -1
  104. package/node_modules/@flickfyi/core/examples/parrott/timed-task/timed-task.stories.tsx +3 -1
  105. package/node_modules/@flickfyi/core/examples/parrott/timer/timer.stories.tsx +3 -1
  106. package/node_modules/@flickfyi/core/fixtures/native/host/project.yml +3 -0
  107. package/node_modules/@flickfyi/core/fixtures/native/src/catalog-features.tsx +88 -0
  108. package/node_modules/@flickfyi/core/package.json +35 -5
  109. package/node_modules/@flickfyi/core/package.tsgo.json +19 -1
  110. package/node_modules/@flickfyi/core/scripts/vite-globals.d.ts +2 -0
  111. package/node_modules/@flickfyi/core/src/generated-types.d.ts +1 -0
  112. package/node_modules/@flickfyi/core/src/kowalski/README.md +9 -0
  113. package/node_modules/@flickfyi/core/src/kowalski/calculations.ts +106 -0
  114. package/node_modules/@flickfyi/core/src/kowalski/calculator.ts +243 -0
  115. package/node_modules/@flickfyi/core/src/kowalski/catalog.ts +787 -0
  116. package/node_modules/@flickfyi/core/src/kowalski/clocks.tsx +464 -0
  117. package/node_modules/@flickfyi/core/src/kowalski/date-time.tsx +469 -0
  118. package/node_modules/@flickfyi/core/src/kowalski/details.tsx +1387 -0
  119. package/node_modules/@flickfyi/core/src/kowalski/everyday.tsx +652 -0
  120. package/node_modules/@flickfyi/core/src/kowalski/format.ts +48 -0
  121. package/node_modules/@flickfyi/core/src/kowalski/frame.tsx +351 -0
  122. package/node_modules/@flickfyi/core/src/kowalski/index.ts +23 -0
  123. package/node_modules/@flickfyi/core/src/kowalski/productivity.tsx +575 -0
  124. package/node_modules/@flickfyi/core/src/kowalski/recipe.tsx +555 -0
  125. package/node_modules/@flickfyi/core/src/kowalski/stories/composition.stories.tsx +47 -0
  126. package/node_modules/@flickfyi/core/src/kowalski/stories/composition.tsx +100 -0
  127. package/node_modules/@flickfyi/core/src/kowalski/tools.tsx +1213 -0
  128. package/node_modules/@flickfyi/core/src/kowalski/use-clock.ts +19 -0
  129. package/node_modules/@flickfyi/core/src/kowalski/use-configured-feature.ts +85 -0
  130. package/node_modules/@flickfyi/core/src/kowalski/use-feature-store.ts +119 -0
  131. package/node_modules/@flickfyi/core/src/molecules/field/field.tsx +34 -0
  132. package/node_modules/@flickfyi/core/src/molecules/index.ts +3 -0
  133. package/node_modules/@flickfyi/core/src/molecules/segmented/segmented.tsx +48 -0
  134. package/node_modules/@flickfyi/core/src/molecules/stepper/stepper.tsx +54 -0
  135. package/node_modules/@flickfyi/core/src/quantum/use-store.ts +41 -16
  136. package/node_modules/@flickfyi/core/tests/fly.test.ts +34 -7
  137. package/node_modules/@flickfyi/core/tests/kowalski-calculations.test.ts +60 -0
  138. package/node_modules/@flickfyi/core/tests/kowalski-calculator.test.ts +142 -0
  139. package/node_modules/@flickfyi/core/tests/kowalski-contracts.test.ts +223 -0
  140. package/node_modules/@flickfyi/core/tests/kowalski-date-time.test.tsx +462 -0
  141. package/node_modules/@flickfyi/core/tests/kowalski-features.test.tsx +2158 -0
  142. package/node_modules/@flickfyi/core/tests/kowalski-store.test.tsx +514 -0
  143. package/node_modules/@flickfyi/core/tests/preview-build.test.ts +1 -1
  144. package/node_modules/@flickfyi/core/tests/quantum-use-store.test.ts +53 -1
  145. package/node_modules/@flickfyi/core/tests/verify-tsgo-assets.test.ts +1 -1
  146. package/node_modules/@flickfyi/core/tsconfig.vite-dts.json +2 -1
  147. package/node_modules/@flickfyi/core/tsgo.bun.lock +111 -9
  148. package/node_modules/@flickfyi/core/tsgo.package.json +21 -3
  149. package/node_modules/@flickfyi/core/vite.config.ts +9 -5
  150. package/node_modules/@flickfyi/core/vitest.config.ts +1 -1
  151. package/node_modules/@flickfyi/http-message-sig/.turbo/turbo-build.log +7 -7
  152. package/node_modules/@flickfyi/identity/.turbo/turbo-build.log +8 -9
  153. package/node_modules/@flickfyi/lens/.turbo/turbo-build.log +7 -7
  154. package/node_modules/@flickfyi/photon/.turbo/turbo-build.log +10 -4
  155. package/node_modules/@flickfyi/photon/Sources/Photon/Components/Button.swift +29 -2
  156. package/node_modules/@flickfyi/photon/Sources/PhotonVersion/Resources/photon-version.json +1 -1
  157. package/node_modules/@flickfyi/photon/Tests/PhotonTests/Components/ButtonHostTests.swift +26 -0
  158. package/node_modules/@flickfyi/photon/Tests/PhotonTests/Fixtures/StyleX/catalog-features.styles.json +1 -0
  159. package/node_modules/@flickfyi/photon/Tests/PhotonTests/Fixtures/StyleX/catalog-native.styles.json +1 -1
  160. package/node_modules/@flickfyi/photon/Tests/PhotonTests/Fixtures/StyleX/catalog-scale.styles.json +1 -1
  161. package/node_modules/@flickfyi/photon/Tests/PhotonTests/Fixtures/StyleX/catalog-theme.styles.json +1 -1
  162. package/node_modules/@flickfyi/photon/Tests/PhotonTests/Fixtures/StyleX/catalog-typography.styles.json +1 -1
  163. package/node_modules/@flickfyi/photon/Tests/PhotonTests/Fixtures/StyleX/stylex-calculator.styles.json +1 -1
  164. package/node_modules/@flickfyi/photon/dist/globals.d.ts +5 -0
  165. package/node_modules/@flickfyi/photon/dist/version.cjs +1 -1
  166. package/node_modules/@flickfyi/photon/dist/version.js +1 -1
  167. package/node_modules/@flickfyi/photon/generated/react-swiftui.d.ts +1 -0
  168. package/node_modules/@flickfyi/photon/package.json +1 -1
  169. package/node_modules/@flickfyi/photon/src/globals.d.ts +5 -0
  170. package/node_modules/@flickfyi/rwsdk-extensions/.turbo/turbo-build.log +7 -7
  171. package/node_modules/@formatjs/fast-memoize/LICENSE.md +9 -0
  172. package/node_modules/@formatjs/fast-memoize/README.md +3 -0
  173. package/node_modules/@formatjs/fast-memoize/index.d.ts +35 -0
  174. package/node_modules/@formatjs/fast-memoize/index.js +69 -0
  175. package/node_modules/@formatjs/fast-memoize/index.js.map +1 -0
  176. package/node_modules/@formatjs/fast-memoize/package.json +25 -0
  177. package/node_modules/@formatjs/icu-messageformat-parser/LICENSE.md +9 -0
  178. package/node_modules/@formatjs/icu-messageformat-parser/README.md +38 -0
  179. package/node_modules/@formatjs/icu-messageformat-parser/index.d.ts +393 -0
  180. package/node_modules/@formatjs/icu-messageformat-parser/index.js +2187 -0
  181. package/node_modules/@formatjs/icu-messageformat-parser/index.js.map +1 -0
  182. package/node_modules/@formatjs/icu-messageformat-parser/manipulator.d.ts +175 -0
  183. package/node_modules/@formatjs/icu-messageformat-parser/manipulator.js +209 -0
  184. package/node_modules/@formatjs/icu-messageformat-parser/manipulator.js.map +1 -0
  185. package/node_modules/@formatjs/icu-messageformat-parser/no-parser.d.ts +183 -0
  186. package/node_modules/@formatjs/icu-messageformat-parser/no-parser.js +162 -0
  187. package/node_modules/@formatjs/icu-messageformat-parser/no-parser.js.map +1 -0
  188. package/node_modules/@formatjs/icu-messageformat-parser/package.json +21 -0
  189. package/node_modules/@formatjs/icu-messageformat-parser/printer.d.ts +154 -0
  190. package/node_modules/@formatjs/icu-messageformat-parser/printer.js +224 -0
  191. package/node_modules/@formatjs/icu-messageformat-parser/printer.js.map +1 -0
  192. package/node_modules/@formatjs/icu-skeleton-parser/LICENSE.md +9 -0
  193. package/node_modules/@formatjs/icu-skeleton-parser/README.md +1 -0
  194. package/node_modules/@formatjs/icu-skeleton-parser/index.d.ts +67 -0
  195. package/node_modules/@formatjs/icu-skeleton-parser/index.js +380 -0
  196. package/node_modules/@formatjs/icu-skeleton-parser/index.js.map +1 -0
  197. package/node_modules/@formatjs/icu-skeleton-parser/package.json +15 -0
  198. package/node_modules/@formatjs/intl/LICENSE.md +9 -0
  199. package/node_modules/@formatjs/intl/README.md +3 -0
  200. package/node_modules/@formatjs/intl/index.d.ts +499 -0
  201. package/node_modules/@formatjs/intl/index.js +595 -0
  202. package/node_modules/@formatjs/intl/index.js.map +1 -0
  203. package/node_modules/@formatjs/intl/package.json +37 -0
  204. package/node_modules/decimal.js/LICENCE.md +23 -0
  205. package/node_modules/decimal.js/README.md +246 -0
  206. package/node_modules/decimal.js/decimal.d.ts +301 -0
  207. package/node_modules/decimal.js/decimal.js +4951 -0
  208. package/node_modules/decimal.js/decimal.mjs +4914 -0
  209. package/node_modules/decimal.js/package.json +55 -0
  210. package/node_modules/format-quantity/LICENSE +21 -0
  211. package/node_modules/format-quantity/README.md +181 -0
  212. package/node_modules/format-quantity/dist/cjs/format-quantity.cjs.development.d.ts +131 -0
  213. package/node_modules/format-quantity/dist/cjs/format-quantity.cjs.development.js +214 -0
  214. package/node_modules/format-quantity/dist/cjs/format-quantity.cjs.development.js.map +1 -0
  215. package/node_modules/format-quantity/dist/cjs/format-quantity.cjs.production.d.ts +131 -0
  216. package/node_modules/format-quantity/dist/cjs/format-quantity.cjs.production.js +2 -0
  217. package/node_modules/format-quantity/dist/cjs/format-quantity.cjs.production.js.map +1 -0
  218. package/node_modules/format-quantity/dist/cjs/index.d.ts +1 -0
  219. package/node_modules/format-quantity/dist/cjs/index.js +6 -0
  220. package/node_modules/format-quantity/dist/format-quantity.d.mts +131 -0
  221. package/node_modules/format-quantity/dist/format-quantity.legacy-esm.d.ts +131 -0
  222. package/node_modules/format-quantity/dist/format-quantity.legacy-esm.js +268 -0
  223. package/node_modules/format-quantity/dist/format-quantity.legacy-esm.js.map +1 -0
  224. package/node_modules/format-quantity/dist/format-quantity.mjs +208 -0
  225. package/node_modules/format-quantity/dist/format-quantity.mjs.map +1 -0
  226. package/node_modules/format-quantity/dist/format-quantity.production.d.mts +131 -0
  227. package/node_modules/format-quantity/dist/format-quantity.production.mjs +2 -0
  228. package/node_modules/format-quantity/dist/format-quantity.production.mjs.map +1 -0
  229. package/node_modules/format-quantity/dist/format-quantity.umd.min.js +3 -0
  230. package/node_modules/format-quantity/dist/format-quantity.umd.min.js.map +1 -0
  231. package/node_modules/format-quantity/package.json +81 -0
  232. package/node_modules/intl-messageformat/LICENSE.md +33 -0
  233. package/node_modules/intl-messageformat/README.md +3 -0
  234. package/node_modules/intl-messageformat/index.d.ts +177 -0
  235. package/node_modules/intl-messageformat/index.js +327 -0
  236. package/node_modules/intl-messageformat/index.js.map +1 -0
  237. package/node_modules/intl-messageformat/intl-messageformat.iife.js +2819 -0
  238. package/node_modules/intl-messageformat/package.json +40 -0
  239. package/node_modules/numeric-quantity/LICENSE +21 -0
  240. package/node_modules/numeric-quantity/README.md +306 -0
  241. package/node_modules/numeric-quantity/dist/cjs/index.d.ts +1 -0
  242. package/node_modules/numeric-quantity/dist/cjs/index.js +6 -0
  243. package/node_modules/numeric-quantity/dist/cjs/numeric-quantity.cjs.development.d.ts +293 -0
  244. package/node_modules/numeric-quantity/dist/cjs/numeric-quantity.cjs.development.js +579 -0
  245. package/node_modules/numeric-quantity/dist/cjs/numeric-quantity.cjs.development.js.map +1 -0
  246. package/node_modules/numeric-quantity/dist/cjs/numeric-quantity.cjs.production.d.ts +293 -0
  247. package/node_modules/numeric-quantity/dist/cjs/numeric-quantity.cjs.production.js +2 -0
  248. package/node_modules/numeric-quantity/dist/cjs/numeric-quantity.cjs.production.js.map +1 -0
  249. package/node_modules/numeric-quantity/dist/numeric-quantity.d.mts +293 -0
  250. package/node_modules/numeric-quantity/dist/numeric-quantity.iife.umd.min.js +2 -0
  251. package/node_modules/numeric-quantity/dist/numeric-quantity.iife.umd.min.js.map +1 -0
  252. package/node_modules/numeric-quantity/dist/numeric-quantity.legacy-esm.d.ts +293 -0
  253. package/node_modules/numeric-quantity/dist/numeric-quantity.legacy-esm.js +622 -0
  254. package/node_modules/numeric-quantity/dist/numeric-quantity.legacy-esm.js.map +1 -0
  255. package/node_modules/numeric-quantity/dist/numeric-quantity.mjs +564 -0
  256. package/node_modules/numeric-quantity/dist/numeric-quantity.mjs.map +1 -0
  257. package/node_modules/numeric-quantity/dist/numeric-quantity.production.d.mts +293 -0
  258. package/node_modules/numeric-quantity/dist/numeric-quantity.production.mjs +2 -0
  259. package/node_modules/numeric-quantity/dist/numeric-quantity.production.mjs.map +1 -0
  260. package/node_modules/numeric-quantity/package.json +72 -0
  261. package/node_modules/validator/LICENSE +20 -0
  262. package/node_modules/validator/README.md +269 -0
  263. package/node_modules/validator/es/index.js +218 -0
  264. package/node_modules/validator/es/lib/alpha.js +151 -0
  265. package/node_modules/validator/es/lib/blacklist.js +5 -0
  266. package/node_modules/validator/es/lib/contains.js +15 -0
  267. package/node_modules/validator/es/lib/equals.js +5 -0
  268. package/node_modules/validator/es/lib/escape.js +5 -0
  269. package/node_modules/validator/es/lib/isAbaRouting.js +15 -0
  270. package/node_modules/validator/es/lib/isAfter.js +10 -0
  271. package/node_modules/validator/es/lib/isAlpha.js +23 -0
  272. package/node_modules/validator/es/lib/isAlphanumeric.js +23 -0
  273. package/node_modules/validator/es/lib/isAscii.js +10 -0
  274. package/node_modules/validator/es/lib/isBIC.js +16 -0
  275. package/node_modules/validator/es/lib/isBase32.js +15 -0
  276. package/node_modules/validator/es/lib/isBase58.js +8 -0
  277. package/node_modules/validator/es/lib/isBase64.js +23 -0
  278. package/node_modules/validator/es/lib/isBefore.js +10 -0
  279. package/node_modules/validator/es/lib/isBoolean.js +15 -0
  280. package/node_modules/validator/es/lib/isBtcAddress.js +7 -0
  281. package/node_modules/validator/es/lib/isByteLength.js +19 -0
  282. package/node_modules/validator/es/lib/isCreditCard.js +43 -0
  283. package/node_modules/validator/es/lib/isCurrency.js +74 -0
  284. package/node_modules/validator/es/lib/isDataURI.js +31 -0
  285. package/node_modules/validator/es/lib/isDate.js +93 -0
  286. package/node_modules/validator/es/lib/isDecimal.js +22 -0
  287. package/node_modules/validator/es/lib/isDivisibleBy.js +6 -0
  288. package/node_modules/validator/es/lib/isEAN.js +70 -0
  289. package/node_modules/validator/es/lib/isEmail.js +165 -0
  290. package/node_modules/validator/es/lib/isEmpty.js +10 -0
  291. package/node_modules/validator/es/lib/isEthereumAddress.js +6 -0
  292. package/node_modules/validator/es/lib/isFQDN.js +67 -0
  293. package/node_modules/validator/es/lib/isFloat.js +14 -0
  294. package/node_modules/validator/es/lib/isFullWidth.js +6 -0
  295. package/node_modules/validator/es/lib/isHSL.js +13 -0
  296. package/node_modules/validator/es/lib/isHalfWidth.js +6 -0
  297. package/node_modules/validator/es/lib/isHash.js +21 -0
  298. package/node_modules/validator/es/lib/isHexColor.js +13 -0
  299. package/node_modules/validator/es/lib/isHexadecimal.js +6 -0
  300. package/node_modules/validator/es/lib/isIBAN.js +171 -0
  301. package/node_modules/validator/es/lib/isIMEI.js +40 -0
  302. package/node_modules/validator/es/lib/isIP.js +58 -0
  303. package/node_modules/validator/es/lib/isIPRange.js +41 -0
  304. package/node_modules/validator/es/lib/isISBN.js +47 -0
  305. package/node_modules/validator/es/lib/isISIN.js +55 -0
  306. package/node_modules/validator/es/lib/isISO15924.js +9 -0
  307. package/node_modules/validator/es/lib/isISO31661Alpha2.js +21 -0
  308. package/node_modules/validator/es/lib/isISO31661Alpha3.js +20 -0
  309. package/node_modules/validator/es/lib/isISO31661Numeric.js +8 -0
  310. package/node_modules/validator/es/lib/isISO4217.js +9 -0
  311. package/node_modules/validator/es/lib/isISO6346.js +28 -0
  312. package/node_modules/validator/es/lib/isISO6391.js +6 -0
  313. package/node_modules/validator/es/lib/isISO8601.js +42 -0
  314. package/node_modules/validator/es/lib/isISRC.js +8 -0
  315. package/node_modules/validator/es/lib/isISSN.js +19 -0
  316. package/node_modules/validator/es/lib/isIdentityCard.js +407 -0
  317. package/node_modules/validator/es/lib/isIn.js +23 -0
  318. package/node_modules/validator/es/lib/isInt.js +19 -0
  319. package/node_modules/validator/es/lib/isJSON.js +26 -0
  320. package/node_modules/validator/es/lib/isJWT.js +15 -0
  321. package/node_modules/validator/es/lib/isLatLong.js +21 -0
  322. package/node_modules/validator/es/lib/isLength.js +27 -0
  323. package/node_modules/validator/es/lib/isLicensePlate.js +58 -0
  324. package/node_modules/validator/es/lib/isLocale.js +107 -0
  325. package/node_modules/validator/es/lib/isLowercase.js +5 -0
  326. package/node_modules/validator/es/lib/isLuhnNumber.js +25 -0
  327. package/node_modules/validator/es/lib/isMACAddress.js +36 -0
  328. package/node_modules/validator/es/lib/isMD5.js +6 -0
  329. package/node_modules/validator/es/lib/isMagnetURI.js +9 -0
  330. package/node_modules/validator/es/lib/isMailtoURI.js +75 -0
  331. package/node_modules/validator/es/lib/isMimeType.js +40 -0
  332. package/node_modules/validator/es/lib/isMobilePhone.js +212 -0
  333. package/node_modules/validator/es/lib/isMongoId.js +6 -0
  334. package/node_modules/validator/es/lib/isMultibyte.js +10 -0
  335. package/node_modules/validator/es/lib/isNumeric.js +10 -0
  336. package/node_modules/validator/es/lib/isOctal.js +6 -0
  337. package/node_modules/validator/es/lib/isPassportNumber.js +145 -0
  338. package/node_modules/validator/es/lib/isPort.js +8 -0
  339. package/node_modules/validator/es/lib/isPostalCode.js +100 -0
  340. package/node_modules/validator/es/lib/isRFC3339.js +21 -0
  341. package/node_modules/validator/es/lib/isRgbColor.js +34 -0
  342. package/node_modules/validator/es/lib/isSemVer.js +14 -0
  343. package/node_modules/validator/es/lib/isSlug.js +6 -0
  344. package/node_modules/validator/es/lib/isStrongPassword.js +90 -0
  345. package/node_modules/validator/es/lib/isSurrogatePair.js +6 -0
  346. package/node_modules/validator/es/lib/isTaxID.js +1407 -0
  347. package/node_modules/validator/es/lib/isTime.js +22 -0
  348. package/node_modules/validator/es/lib/isULID.js +5 -0
  349. package/node_modules/validator/es/lib/isURL.js +241 -0
  350. package/node_modules/validator/es/lib/isUUID.js +23 -0
  351. package/node_modules/validator/es/lib/isUppercase.js +5 -0
  352. package/node_modules/validator/es/lib/isVAT.js +264 -0
  353. package/node_modules/validator/es/lib/isVariableWidth.js +7 -0
  354. package/node_modules/validator/es/lib/isWhitelisted.js +10 -0
  355. package/node_modules/validator/es/lib/ltrim.js +7 -0
  356. package/node_modules/validator/es/lib/matches.js +8 -0
  357. package/node_modules/validator/es/lib/normalizeEmail.js +131 -0
  358. package/node_modules/validator/es/lib/rtrim.js +15 -0
  359. package/node_modules/validator/es/lib/stripLow.js +7 -0
  360. package/node_modules/validator/es/lib/toBoolean.js +8 -0
  361. package/node_modules/validator/es/lib/toDate.js +6 -0
  362. package/node_modules/validator/es/lib/toFloat.js +5 -0
  363. package/node_modules/validator/es/lib/toInt.js +5 -0
  364. package/node_modules/validator/es/lib/trim.js +5 -0
  365. package/node_modules/validator/es/lib/unescape.js +8 -0
  366. package/node_modules/validator/es/lib/util/algorithms.js +79 -0
  367. package/node_modules/validator/es/lib/util/assertString.js +4 -0
  368. package/node_modules/validator/es/lib/util/checkHost.js +12 -0
  369. package/node_modules/validator/es/lib/util/includesArray.js +6 -0
  370. package/node_modules/validator/es/lib/util/includesString.js +4 -0
  371. package/node_modules/validator/es/lib/util/merge.js +14 -0
  372. package/node_modules/validator/es/lib/util/multilineRegex.js +12 -0
  373. package/node_modules/validator/es/lib/util/nullUndefinedCheck.js +3 -0
  374. package/node_modules/validator/es/lib/util/toString.js +13 -0
  375. package/node_modules/validator/es/lib/util/typeOf.js +10 -0
  376. package/node_modules/validator/es/lib/whitelist.js +5 -0
  377. package/node_modules/validator/index.js +229 -0
  378. package/node_modules/validator/lib/alpha.js +157 -0
  379. package/node_modules/validator/lib/blacklist.js +14 -0
  380. package/node_modules/validator/lib/contains.js +24 -0
  381. package/node_modules/validator/lib/equals.js +14 -0
  382. package/node_modules/validator/lib/escape.js +14 -0
  383. package/node_modules/validator/lib/isAbaRouting.js +23 -0
  384. package/node_modules/validator/lib/isAfter.js +19 -0
  385. package/node_modules/validator/lib/isAlpha.js +31 -0
  386. package/node_modules/validator/lib/isAlphanumeric.js +31 -0
  387. package/node_modules/validator/lib/isAscii.js +18 -0
  388. package/node_modules/validator/lib/isBIC.js +24 -0
  389. package/node_modules/validator/lib/isBase32.js +24 -0
  390. package/node_modules/validator/lib/isBase58.js +16 -0
  391. package/node_modules/validator/lib/isBase64.js +32 -0
  392. package/node_modules/validator/lib/isBefore.js +19 -0
  393. package/node_modules/validator/lib/isBoolean.js +24 -0
  394. package/node_modules/validator/lib/isBtcAddress.js +16 -0
  395. package/node_modules/validator/lib/isByteLength.js +27 -0
  396. package/node_modules/validator/lib/isCreditCard.js +52 -0
  397. package/node_modules/validator/lib/isCurrency.js +83 -0
  398. package/node_modules/validator/lib/isDataURI.js +40 -0
  399. package/node_modules/validator/lib/isDate.js +102 -0
  400. package/node_modules/validator/lib/isDecimal.js +31 -0
  401. package/node_modules/validator/lib/isDivisibleBy.js +15 -0
  402. package/node_modules/validator/lib/isEAN.js +78 -0
  403. package/node_modules/validator/lib/isEmail.js +174 -0
  404. package/node_modules/validator/lib/isEmpty.js +19 -0
  405. package/node_modules/validator/lib/isEthereumAddress.js +15 -0
  406. package/node_modules/validator/lib/isFQDN.js +76 -0
  407. package/node_modules/validator/lib/isFloat.js +22 -0
  408. package/node_modules/validator/lib/isFullWidth.js +14 -0
  409. package/node_modules/validator/lib/isHSL.js +22 -0
  410. package/node_modules/validator/lib/isHalfWidth.js +14 -0
  411. package/node_modules/validator/lib/isHash.js +30 -0
  412. package/node_modules/validator/lib/isHexColor.js +22 -0
  413. package/node_modules/validator/lib/isHexadecimal.js +15 -0
  414. package/node_modules/validator/lib/isIBAN.js +178 -0
  415. package/node_modules/validator/lib/isIMEI.js +49 -0
  416. package/node_modules/validator/lib/isIP.js +67 -0
  417. package/node_modules/validator/lib/isIPRange.js +50 -0
  418. package/node_modules/validator/lib/isISBN.js +56 -0
  419. package/node_modules/validator/lib/isISIN.js +64 -0
  420. package/node_modules/validator/lib/isISO15924.js +16 -0
  421. package/node_modules/validator/lib/isISO31661Alpha2.js +28 -0
  422. package/node_modules/validator/lib/isISO31661Alpha3.js +28 -0
  423. package/node_modules/validator/lib/isISO31661Numeric.js +16 -0
  424. package/node_modules/validator/lib/isISO4217.js +16 -0
  425. package/node_modules/validator/lib/isISO6346.js +35 -0
  426. package/node_modules/validator/lib/isISO6391.js +15 -0
  427. package/node_modules/validator/lib/isISO8601.js +50 -0
  428. package/node_modules/validator/lib/isISRC.js +16 -0
  429. package/node_modules/validator/lib/isISSN.js +28 -0
  430. package/node_modules/validator/lib/isIdentityCard.js +416 -0
  431. package/node_modules/validator/lib/isIn.js +32 -0
  432. package/node_modules/validator/lib/isInt.js +28 -0
  433. package/node_modules/validator/lib/isJSON.js +35 -0
  434. package/node_modules/validator/lib/isJWT.js +24 -0
  435. package/node_modules/validator/lib/isLatLong.js +30 -0
  436. package/node_modules/validator/lib/isLength.js +35 -0
  437. package/node_modules/validator/lib/isLicensePlate.js +67 -0
  438. package/node_modules/validator/lib/isLocale.js +115 -0
  439. package/node_modules/validator/lib/isLowercase.js +14 -0
  440. package/node_modules/validator/lib/isLuhnNumber.js +34 -0
  441. package/node_modules/validator/lib/isMACAddress.js +45 -0
  442. package/node_modules/validator/lib/isMD5.js +15 -0
  443. package/node_modules/validator/lib/isMagnetURI.js +18 -0
  444. package/node_modules/validator/lib/isMailtoURI.js +84 -0
  445. package/node_modules/validator/lib/isMimeType.js +48 -0
  446. package/node_modules/validator/lib/isMobilePhone.js +219 -0
  447. package/node_modules/validator/lib/isMongoId.js +15 -0
  448. package/node_modules/validator/lib/isMultibyte.js +18 -0
  449. package/node_modules/validator/lib/isNumeric.js +19 -0
  450. package/node_modules/validator/lib/isOctal.js +15 -0
  451. package/node_modules/validator/lib/isPassportNumber.js +152 -0
  452. package/node_modules/validator/lib/isPort.js +17 -0
  453. package/node_modules/validator/lib/isPostalCode.js +107 -0
  454. package/node_modules/validator/lib/isRFC3339.js +29 -0
  455. package/node_modules/validator/lib/isRgbColor.js +42 -0
  456. package/node_modules/validator/lib/isSemVer.js +22 -0
  457. package/node_modules/validator/lib/isSlug.js +15 -0
  458. package/node_modules/validator/lib/isStrongPassword.js +99 -0
  459. package/node_modules/validator/lib/isSurrogatePair.js +15 -0
  460. package/node_modules/validator/lib/isTaxID.js +1417 -0
  461. package/node_modules/validator/lib/isTime.js +31 -0
  462. package/node_modules/validator/lib/isULID.js +14 -0
  463. package/node_modules/validator/lib/isURL.js +249 -0
  464. package/node_modules/validator/lib/isUUID.js +32 -0
  465. package/node_modules/validator/lib/isUppercase.js +14 -0
  466. package/node_modules/validator/lib/isVAT.js +274 -0
  467. package/node_modules/validator/lib/isVariableWidth.js +16 -0
  468. package/node_modules/validator/lib/isWhitelisted.js +19 -0
  469. package/node_modules/validator/lib/ltrim.js +16 -0
  470. package/node_modules/validator/lib/matches.js +17 -0
  471. package/node_modules/validator/lib/normalizeEmail.js +140 -0
  472. package/node_modules/validator/lib/rtrim.js +24 -0
  473. package/node_modules/validator/lib/stripLow.js +16 -0
  474. package/node_modules/validator/lib/toBoolean.js +17 -0
  475. package/node_modules/validator/lib/toDate.js +15 -0
  476. package/node_modules/validator/lib/toFloat.js +14 -0
  477. package/node_modules/validator/lib/toInt.js +14 -0
  478. package/node_modules/validator/lib/trim.js +14 -0
  479. package/node_modules/validator/lib/unescape.js +17 -0
  480. package/node_modules/validator/lib/util/algorithms.js +88 -0
  481. package/node_modules/validator/lib/util/assertString.js +12 -0
  482. package/node_modules/validator/lib/util/checkHost.js +20 -0
  483. package/node_modules/validator/lib/util/includesArray.js +14 -0
  484. package/node_modules/validator/lib/util/includesString.js +12 -0
  485. package/node_modules/validator/lib/util/merge.js +22 -0
  486. package/node_modules/validator/lib/util/multilineRegex.js +20 -0
  487. package/node_modules/validator/lib/util/nullUndefinedCheck.js +11 -0
  488. package/node_modules/validator/lib/util/toString.js +21 -0
  489. package/node_modules/validator/lib/util/typeOf.js +18 -0
  490. package/node_modules/validator/lib/whitelist.js +14 -0
  491. package/node_modules/validator/package.json +76 -0
  492. package/node_modules/validator/validator.js +5808 -0
  493. package/node_modules/validator/validator.min.js +23 -0
  494. package/package.json +3 -3
@@ -0,0 +1,1213 @@
1
+ import { Flex, Icon, Text } from '@flickfyi/core'
2
+ import { Button, Input, Picker } from '@flickfyi/core/atoms'
3
+ import { Field, List, Segmented, Stat, Stepper } from '@flickfyi/core/molecules'
4
+ import * as stylex from '@stylexjs/stylex'
5
+ import { isEqual, uniqBy } from 'lodash'
6
+ import { useEffect, useMemo, useState } from 'react'
7
+
8
+ import { buttonVars } from '~/atoms/button/button.stylex'
9
+ import { radius, size, space, text } from '~/themes/scale.stylex'
10
+ import { appearance, button, roles } from '~/themes/shared'
11
+ import { colors, font } from '~/themes/tokens.stylex'
12
+
13
+ import { billShares, convertUnit, formatCurrency, pickResult } from './calculations'
14
+ import {
15
+ calculatorDisplay,
16
+ calculatorEquation,
17
+ calculatorKeyLabel,
18
+ calculatorKeys,
19
+ initialCalculator,
20
+ pressCalculatorKey,
21
+ } from './calculator'
22
+ import {
23
+ kowalskiCatalog,
24
+ type KowalskiInput,
25
+ type PickerDomain,
26
+ unitDefinitions,
27
+ } from './catalog'
28
+ import { format, parseNumberInput } from './format'
29
+ import {
30
+ FeatureAction,
31
+ FeatureFacts,
32
+ FeatureField,
33
+ FeatureFrame,
34
+ FeatureNumber,
35
+ FeatureWell,
36
+ layout,
37
+ } from './frame'
38
+ import { useConfiguredFeature } from './use-configured-feature'
39
+
40
+ export function BillSplit(input: KowalskiInput<'billSplit'>) {
41
+ const props = kowalskiCatalog.billSplit.props.parse(input)
42
+ const schema = kowalskiCatalog.billSplit.storeSchema
43
+ const store = useConfiguredFeature({
44
+ name: props.id ?? 'KowalskiBillSplit',
45
+ revision: props.revision,
46
+ schema,
47
+ scope: props.scope,
48
+ seed: schema.parse({
49
+ people: props.people,
50
+ tipPercent: props.tipPercent,
51
+ total: props.total,
52
+ }),
53
+ })
54
+ const result = billShares(
55
+ store.value.total,
56
+ store.value.people,
57
+ store.value.tipPercent,
58
+ props.currency
59
+ )
60
+ const money = format.formatters.getNumberFormat(format.locale, {
61
+ currency: props.currency,
62
+ style: 'currency',
63
+ })
64
+ const billTotal = formatCurrency(store.value.total, props.currency)
65
+ const perPerson = money.format(result.perPerson)
66
+ const subtotal = billShares(store.value.total, 1, 0, props.currency).total
67
+ const tipPresets = [0, 15, 20, 25]
68
+ const [customTipOpen, setCustomTipOpen] = useState(false)
69
+ const [peopleDraft, setPeopleDraft] = useState(
70
+ format.formatNumber(store.value.people, { format: 'integer' })
71
+ )
72
+ const [preservePeopleDraft, setPreservePeopleDraft] = useState<{ text: string }>()
73
+ useEffect(() => {
74
+ if (preservePeopleDraft === undefined) {
75
+ setPeopleDraft(current =>
76
+ parseNumberInput(current) === store.value.people
77
+ ? current
78
+ : format.formatNumber(store.value.people, { format: 'integer' })
79
+ )
80
+ }
81
+ }, [preservePeopleDraft, store.value.people])
82
+ const customTipVisible = customTipOpen || !tipPresets.includes(store.value.tipPercent)
83
+ const peopleValid = schema.shape.people.safeParse(parseNumberInput(peopleDraft)).success
84
+ return (
85
+ <FeatureFrame
86
+ onRetry={store.retry}
87
+ notice={store.error}
88
+ component="billSplit"
89
+ title={props.title}
90
+ >
91
+ <FeatureField
92
+ component="billSplit"
93
+ label="Bill total"
94
+ value={billTotal}
95
+ ready={store.ready}
96
+ saving={store.saving}
97
+ parseValue={value => parseNumberInput(value, props.currency)}
98
+ validate={value =>
99
+ schema.shape.total.safeParse(parseNumberInput(value, props.currency)).success
100
+ }
101
+ onChange={(value, onSaved) => {
102
+ store.set({ total: parseNumberInput(value, props.currency) }, onSaved)
103
+ }}
104
+ />
105
+ <Field label="Tip">
106
+ <Segmented
107
+ disabled={!store.ready}
108
+ options={[
109
+ ...tipPresets.map(tip => ({
110
+ label: format.formatNumber(tip / 100, { style: 'percent' }),
111
+ value: tip,
112
+ })),
113
+ { label: 'Custom', value: 'custom' as const },
114
+ ]}
115
+ value={customTipVisible ? 'custom' : store.value.tipPercent}
116
+ onChange={tip => {
117
+ setCustomTipOpen(tip === 'custom')
118
+ if (typeof tip === 'number') {
119
+ store.set({ tipPercent: tip })
120
+ }
121
+ }}
122
+ />
123
+ </Field>
124
+ {customTipVisible && (
125
+ <FeatureNumber
126
+ component="billSplit"
127
+ label="Tip percent"
128
+ min={0}
129
+ max={100}
130
+ ready={store.ready}
131
+ saving={store.saving}
132
+ value={store.value.tipPercent}
133
+ onChange={(tipPercent, onSaved) => {
134
+ store.set({ tipPercent }, onSaved)
135
+ }}
136
+ />
137
+ )}
138
+ <Field error={!peopleValid && 'Choose 1–1,000 people.'} label="People">
139
+ <Stepper
140
+ decrementDisabled={!store.ready || store.value.people <= 1}
141
+ decrementLabel="Fewer people"
142
+ incrementDisabled={!store.ready || store.value.people >= 1000}
143
+ incrementLabel="More people"
144
+ onDecrement={() => {
145
+ setPreservePeopleDraft(undefined)
146
+ store.set(current => ({ people: Math.max(1, current.people - 1) }))
147
+ }}
148
+ onIncrement={() => {
149
+ setPreservePeopleDraft(undefined)
150
+ store.set(current => ({ people: Math.min(1000, current.people + 1) }))
151
+ }}
152
+ >
153
+ {store.ready ? (
154
+ <Input
155
+ placeholder="People"
156
+ value={peopleDraft}
157
+ onChange={text => {
158
+ setPeopleDraft(text)
159
+ const edit = { text }
160
+ setPreservePeopleDraft(edit)
161
+ if (schema.shape.people.safeParse(parseNumberInput(text)).success) {
162
+ store.set({ people: parseNumberInput(text) }, () =>
163
+ setPreservePeopleDraft(current =>
164
+ current === edit ? undefined : current
165
+ )
166
+ )
167
+ }
168
+ }}
169
+ style={[layout.input, layout.stepperInput]}
170
+ />
171
+ ) : (
172
+ <Text {...stylex.props(roles.text, layout.stepperInput)}>
173
+ {format.formatNumber(store.value.people, { format: 'integer' })}
174
+ </Text>
175
+ )}
176
+ </Stepper>
177
+ {store.ready &&
178
+ peopleValid &&
179
+ preservePeopleDraft !== undefined &&
180
+ !store.saving && (
181
+ <FeatureAction
182
+ component="billSplit"
183
+ onClick={() =>
184
+ store.set({ people: parseNumberInput(peopleDraft) }, () =>
185
+ setPreservePeopleDraft(current =>
186
+ current === preservePeopleDraft ? undefined : current
187
+ )
188
+ )
189
+ }
190
+ >
191
+ Retry people
192
+ </FeatureAction>
193
+ )}
194
+ </Field>
195
+ <FeatureWell>
196
+ <Stat>
197
+ <Stat.Value style={billStyles.amount(perPerson.length)}>{perPerson}</Stat.Value>
198
+ <Stat.Caption>Per person</Stat.Caption>
199
+ </Stat>
200
+ {result.remainder > 0 && (
201
+ <Text {...stylex.props(roles.muted, billStyles.centered)}>
202
+ {`One person pays ${money.format(result.perPerson + result.remainder)} — ${money.format(result.remainder)} extra to cover rounding.`}
203
+ </Text>
204
+ )}
205
+ {store.value.tipPercent > 0 && (
206
+ <Flex {...stylex.props(billStyles.row)}>
207
+ <Text
208
+ {...stylex.props(roles.muted)}
209
+ >{`Tip (${format.formatNumber(store.value.tipPercent, { format: 'decimal', style: 'unit', unit: 'percent', unitDisplay: 'narrow' })})`}</Text>
210
+ <Text {...stylex.props(roles.text)}>
211
+ {money.format(result.total - subtotal)}
212
+ </Text>
213
+ </Flex>
214
+ )}
215
+ <Flex {...stylex.props(billStyles.row)}>
216
+ <Text {...stylex.props(roles.strong)}>Total with tip</Text>
217
+ <Text {...stylex.props(roles.strong)}>{money.format(result.total)}</Text>
218
+ </Flex>
219
+ </FeatureWell>
220
+ </FeatureFrame>
221
+ )
222
+ }
223
+
224
+ export function Expense(input: KowalskiInput<'expense'>) {
225
+ const props = kowalskiCatalog.expense.props.parse(input)
226
+ const schema = kowalskiCatalog.expense.storeSchema
227
+ const store = useConfiguredFeature({
228
+ name: props.id ?? 'KowalskiExpense',
229
+ revision: props.revision,
230
+ schema,
231
+ scope: props.scope,
232
+ seed: schema.parse({
233
+ amount: props.amount,
234
+ category: { value: props.category },
235
+ title: { value: props.title },
236
+ }),
237
+ })
238
+ const [draft, setDraft] = useState({ amount: '', category: '', title: '' })
239
+ const [baseline, setBaseline] = useState<typeof draft | null>(null)
240
+ const editing = baseline !== null
241
+ const amount = parseNumberInput(draft.amount, props.currency)
242
+ const validAmount = schema.shape.amount.safeParse(amount).success
243
+ const validTitle =
244
+ draft.title.trim().length > 0 &&
245
+ kowalskiCatalog.expense.props.shape.title.safeParse(draft.title).success
246
+ const validCategory = kowalskiCatalog.expense.props.shape.category.safeParse(
247
+ draft.category
248
+ ).success
249
+ const changed =
250
+ baseline !== null &&
251
+ (amount !== parseNumberInput(baseline.amount, props.currency) ||
252
+ draft.category !== baseline.category ||
253
+ draft.title !== baseline.title)
254
+ const formattedAmount = formatCurrency(store.value.amount, props.currency)
255
+ return (
256
+ <FeatureFrame
257
+ onRetry={store.retry}
258
+ notice={store.error}
259
+ component="expense"
260
+ title={
261
+ editing
262
+ ? 'Edit expense'
263
+ : store.value.title.value.trim()
264
+ ? store.value.title.value
265
+ : 'Untitled expense'
266
+ }
267
+ >
268
+ {editing ? (
269
+ <>
270
+ <Field
271
+ error={!validAmount && 'Enter an amount from 0 to 1,000,000,000,000.'}
272
+ label={props.currency === 'USD' ? 'Amount' : `Amount (${props.currency})`}
273
+ >
274
+ {store.saving ? (
275
+ <Text {...stylex.props(roles.text)}>{draft.amount}</Text>
276
+ ) : (
277
+ <Input
278
+ placeholder="Amount"
279
+ value={draft.amount}
280
+ onChange={value => setDraft(current => ({ ...current, amount: value }))}
281
+ style={[layout.input]}
282
+ />
283
+ )}
284
+ </Field>
285
+ {(
286
+ [
287
+ {
288
+ key: 'title',
289
+ label: 'Description',
290
+ valid: validTitle,
291
+ validation: 'Add a description using 160 characters or fewer.',
292
+ },
293
+ {
294
+ key: 'category',
295
+ label: 'Category',
296
+ valid: validCategory,
297
+ validation: 'Use 80 characters or fewer.',
298
+ },
299
+ ] as const
300
+ ).map(field => (
301
+ <Field
302
+ error={!field.valid && field.validation}
303
+ key={field.key}
304
+ label={field.label}
305
+ >
306
+ {store.saving ? (
307
+ <Text {...stylex.props(roles.text)}>
308
+ {draft[field.key] || 'Uncategorized'}
309
+ </Text>
310
+ ) : (
311
+ <Input
312
+ placeholder={field.label}
313
+ value={draft[field.key]}
314
+ onChange={value =>
315
+ setDraft(current => ({ ...current, [field.key]: value }))
316
+ }
317
+ style={[layout.input]}
318
+ />
319
+ )}
320
+ </Field>
321
+ ))}
322
+ <Flex {...stylex.props(layout.actions)}>
323
+ <FeatureAction
324
+ component="expense"
325
+ disabled={store.saving}
326
+ onClick={() => {
327
+ if (!store.saving) {
328
+ setBaseline(null)
329
+ }
330
+ }}
331
+ >
332
+ Cancel
333
+ </FeatureAction>
334
+ <FeatureAction
335
+ component="expense"
336
+ disabled={
337
+ store.saving ||
338
+ !store.ready ||
339
+ !changed ||
340
+ !validAmount ||
341
+ !validTitle ||
342
+ !validCategory
343
+ }
344
+ variant="primary"
345
+ onClick={() => {
346
+ if (
347
+ !baseline ||
348
+ !store.ready ||
349
+ store.saving ||
350
+ !changed ||
351
+ !validAmount ||
352
+ !validTitle ||
353
+ !validCategory
354
+ ) {
355
+ return
356
+ }
357
+ store.set(
358
+ () => ({
359
+ ...(amount !== parseNumberInput(baseline.amount, props.currency) && {
360
+ amount,
361
+ }),
362
+ ...(draft.category !== baseline.category && {
363
+ category: { value: draft.category },
364
+ }),
365
+ ...(draft.title !== baseline.title && {
366
+ title: { value: draft.title },
367
+ }),
368
+ }),
369
+ () => setBaseline(null)
370
+ )
371
+ }}
372
+ >
373
+ {store.saving ? 'Saving…' : 'Save changes'}
374
+ </FeatureAction>
375
+ </Flex>
376
+ {store.saving && <Text {...stylex.props(roles.muted)}>Saving changes…</Text>}
377
+ </>
378
+ ) : (
379
+ <>
380
+ <FeatureWell>
381
+ <Text
382
+ animated="numeric"
383
+ {...stylex.props(
384
+ roles.display,
385
+ expenseStyles.amount(formattedAmount.length)
386
+ )}
387
+ >
388
+ {formattedAmount}
389
+ </Text>
390
+ <Text {...stylex.props(roles.muted)}>
391
+ {store.value.category.value.trim()
392
+ ? store.value.category.value
393
+ : 'Uncategorized'}
394
+ </Text>
395
+ </FeatureWell>
396
+ {!editing && (
397
+ <FeatureAction
398
+ component="expense"
399
+ disabled={!store.ready || store.saving}
400
+ icon="pencil"
401
+ onClick={() => {
402
+ const values = {
403
+ amount: formattedAmount,
404
+ category: store.value.category.value,
405
+ title: store.value.title.value,
406
+ }
407
+ setBaseline(values)
408
+ setDraft(values)
409
+ }}
410
+ >
411
+ Edit expense
412
+ </FeatureAction>
413
+ )}
414
+ </>
415
+ )}
416
+ </FeatureFrame>
417
+ )
418
+ }
419
+
420
+ export function UnitConverter(input: KowalskiInput<'unitConverter'>) {
421
+ const props = kowalskiCatalog.unitConverter.props.parse(input)
422
+ const schema = kowalskiCatalog.unitConverter.storeSchema
423
+ const store = useConfiguredFeature({
424
+ name: props.id ?? 'KowalskiUnitConverter',
425
+ revision: props.revision,
426
+ schema,
427
+ scope: props.scope,
428
+ seed: schema.parse({
429
+ from: { value: props.from },
430
+ to: { value: props.to },
431
+ value: props.value,
432
+ }),
433
+ })
434
+ const units = [
435
+ { label: 'Meters', value: 'm' },
436
+ { label: 'Kilometers', value: 'km' },
437
+ { label: 'Miles', value: 'mi' },
438
+ { label: 'Feet', value: 'ft' },
439
+ { label: 'Kilograms', value: 'kg' },
440
+ { label: 'Grams', value: 'g' },
441
+ { label: 'Pounds', value: 'lb' },
442
+ { label: 'Celsius', value: 'c' },
443
+ { label: 'Fahrenheit', value: 'f' },
444
+ { label: 'Kelvin', value: 'k' },
445
+ ] satisfies {
446
+ label: string
447
+ value: keyof typeof unitDefinitions
448
+ }[]
449
+ const groups = [
450
+ { from: 'm', label: 'Length', to: 'ft' },
451
+ { from: 'kg', label: 'Weight', to: 'lb' },
452
+ { from: 'c', label: 'Temperature', to: 'f' },
453
+ ] satisfies {
454
+ from: keyof typeof unitDefinitions
455
+ label: string
456
+ to: keyof typeof unitDefinitions
457
+ }[]
458
+ const dimension = unitDefinitions[store.value.from.value].dimension
459
+ const compatibleUnits = units.filter(
460
+ unit => unitDefinitions[unit.value].dimension === dimension
461
+ )
462
+ const fromUnit = units.find(unit => unit.value === store.value.from.value)
463
+ const toUnit = units.find(unit => unit.value === store.value.to.value)
464
+ const storedValue = format.formatNumber(store.value.value, { format: 'decimal' })
465
+ const [draft, setDraft] = useState(storedValue)
466
+ const [preserveDraft, setPreserveDraft] = useState<string>()
467
+ useEffect(() => {
468
+ if (store.error && store.ready) {
469
+ setPreserveDraft(current => current ?? draft)
470
+ return
471
+ }
472
+ if (preserveDraft !== undefined) {
473
+ return
474
+ }
475
+ setDraft(current =>
476
+ parseNumberInput(current) === store.value.value ? current : storedValue
477
+ )
478
+ }, [draft, preserveDraft, store.error, store.ready, store.value.value, storedValue])
479
+ const valid = Number.isFinite(parseNumberInput(draft))
480
+ const result = convertUnit(
481
+ valid ? parseNumberInput(draft) : store.value.value,
482
+ store.value.from.value,
483
+ store.value.to.value
484
+ )
485
+ // Four significant digits below one, three decimals above: 6.214 mi, not 6.2137119.
486
+ const formattedResult = valid
487
+ ? Number.isFinite(result)
488
+ ? format.formatNumber(result, {
489
+ notation:
490
+ Math.abs(result) >= 1e9 || (result !== 0 && Math.abs(result) < 1e-6)
491
+ ? 'scientific'
492
+ : 'standard',
493
+ ...(Math.abs(result) < 1
494
+ ? { maximumSignificantDigits: 4 }
495
+ : { maximumFractionDigits: 3 }),
496
+ })
497
+ : 'Out of range'
498
+ : '—'
499
+ return (
500
+ <FeatureFrame
501
+ onRetry={store.retry}
502
+ notice={store.error}
503
+ component="unitConverter"
504
+ title={props.title}
505
+ >
506
+ <Segmented
507
+ disabled={!store.ready}
508
+ options={groups.map(group => ({ label: group.label, value: group.label }))}
509
+ value={
510
+ groups.find(group => unitDefinitions[group.from].dimension === dimension)?.label
511
+ }
512
+ onChange={label => {
513
+ const group = groups.find(option => option.label === label)
514
+ if (!group) {
515
+ return
516
+ }
517
+ store.set(current =>
518
+ unitDefinitions[current.from.value].dimension ===
519
+ unitDefinitions[group.from].dimension
520
+ ? {}
521
+ : { from: { value: group.from }, to: { value: group.to } }
522
+ )
523
+ }}
524
+ />
525
+ <Field error={!valid && 'Enter a valid number.'} label="From">
526
+ {store.ready ? (
527
+ <Input
528
+ placeholder="Value"
529
+ value={draft}
530
+ onChange={text => {
531
+ setDraft(text)
532
+ setPreserveDraft(text)
533
+ if (Number.isFinite(parseNumberInput(text))) {
534
+ store.set(
535
+ () => ({ value: parseNumberInput(text) }),
536
+ () =>
537
+ setPreserveDraft(current => (current === text ? undefined : current))
538
+ )
539
+ }
540
+ }}
541
+ style={[layout.input]}
542
+ />
543
+ ) : (
544
+ <Text {...stylex.props(roles.text)}>{draft}</Text>
545
+ )}
546
+ {store.ready ? (
547
+ <Picker
548
+ label="From unit"
549
+ value={fromUnit?.label}
550
+ onChange={label => {
551
+ store.set(current => {
552
+ const unit = units.find(option => option.label === label)
553
+ return unit &&
554
+ unitDefinitions[unit.value].dimension ===
555
+ unitDefinitions[current.to.value].dimension
556
+ ? { from: { value: unit.value } }
557
+ : {}
558
+ })
559
+ }}
560
+ style={layout.input}
561
+ >
562
+ {compatibleUnits.map(unit => (
563
+ <Text key={unit.value} value={unit.label}>
564
+ {unit.label}
565
+ </Text>
566
+ ))}
567
+ </Picker>
568
+ ) : (
569
+ <Text {...stylex.props(roles.text)}>{fromUnit?.label}</Text>
570
+ )}
571
+ {store.saving && <Text {...stylex.props(roles.muted)}>Saving changes…</Text>}
572
+ {preserveDraft !== undefined &&
573
+ store.ready &&
574
+ valid &&
575
+ parseNumberInput(draft) !== store.value.value && (
576
+ <FeatureAction
577
+ component="unitConverter"
578
+ disabled={store.saving}
579
+ onClick={() =>
580
+ store.set(
581
+ () => ({ value: parseNumberInput(draft) }),
582
+ () =>
583
+ setPreserveDraft(current => (current === draft ? undefined : current))
584
+ )
585
+ }
586
+ >
587
+ Retry saving value
588
+ </FeatureAction>
589
+ )}
590
+ </Field>
591
+ <FeatureWell>
592
+ <Text {...stylex.props(roles.h1, unitStyles.result)}>{formattedResult}</Text>
593
+ {store.ready ? (
594
+ <Picker
595
+ label="To unit"
596
+ value={toUnit?.label}
597
+ onChange={label => {
598
+ store.set(current => {
599
+ const unit = units.find(option => option.label === label)
600
+ return unit &&
601
+ unitDefinitions[unit.value].dimension ===
602
+ unitDefinitions[current.from.value].dimension
603
+ ? { to: { value: unit.value } }
604
+ : {}
605
+ })
606
+ }}
607
+ style={layout.input}
608
+ >
609
+ {compatibleUnits.map(unit => (
610
+ <Text key={unit.value} value={unit.label}>
611
+ {unit.label}
612
+ </Text>
613
+ ))}
614
+ </Picker>
615
+ ) : (
616
+ <Text {...stylex.props(roles.text)}>{toUnit?.label}</Text>
617
+ )}
618
+ </FeatureWell>
619
+ <FeatureAction
620
+ component="unitConverter"
621
+ icon="arrow.left.arrow.right"
622
+ disabled={!store.ready}
623
+ onClick={() => {
624
+ store.set(current => ({ from: current.to, to: current.from }))
625
+ }}
626
+ >
627
+ Swap units
628
+ </FeatureAction>
629
+ </FeatureFrame>
630
+ )
631
+ }
632
+
633
+ export function Calculator(input: KowalskiInput<'calculator'>) {
634
+ const props = kowalskiCatalog.calculator.props.parse(input)
635
+ const schema = kowalskiCatalog.calculator.storeSchema
636
+ const store = useConfiguredFeature({
637
+ name: props.id ?? 'KowalskiCalculator',
638
+ revision: props.revision,
639
+ schema,
640
+ scope: props.scope,
641
+ seed: schema.parse({ calculation: initialCalculator(props) }),
642
+ })
643
+ const calculation = store.value.calculation
644
+ const display = calculatorDisplay(calculation.entry)
645
+ return (
646
+ <FeatureFrame
647
+ onRetry={store.retry}
648
+ notice={store.error}
649
+ component="calculator"
650
+ title={props.title}
651
+ >
652
+ <FeatureWell>
653
+ <Text {...stylex.props(roles.muted, calculatorStyles.equation)}>
654
+ {calculatorEquation(calculation)}
655
+ </Text>
656
+ <Text {...stylex.props(roles.display, calculatorStyles.value(display.length))}>
657
+ {display}
658
+ </Text>
659
+ {calculation.phase === 'error' && (
660
+ <Text {...stylex.props(roles.muted, calculatorStyles.error)}>
661
+ {calculation.operator === '/' && calculation.right === 0
662
+ ? 'Cannot divide by zero'
663
+ : 'Number out of range'}
664
+ </Text>
665
+ )}
666
+ </FeatureWell>
667
+ <Flex {...stylex.props(calculatorStyles.keypad)}>
668
+ {calculatorKeys.map(key => {
669
+ const operation = key === '+' || key === '-' || key === '*' || key === '/'
670
+ const utility =
671
+ key === 'backspace' || key === 'clear' || key === 'sign' || key === '%'
672
+ const selected =
673
+ operation && calculation.phase === 'operator' && calculation.operator === key
674
+ return (
675
+ <Button
676
+ accessibilityLabel={
677
+ key === 'backspace'
678
+ ? '⌫: delete last digit'
679
+ : calculatorKeyLabel(calculation, key)
680
+ }
681
+ key={key}
682
+ disabled={!store.ready}
683
+ selected={selected}
684
+ onClick={() => {
685
+ store.set(current => ({
686
+ calculation: pressCalculatorKey(current.calculation, key),
687
+ }))
688
+ }}
689
+ style={[
690
+ button.secondary,
691
+ (operation || utility) && calculatorStyles.operation,
692
+ key === '=' && button.primary,
693
+ calculatorStyles.key,
694
+ utility && calculatorStyles.utility,
695
+ operation && calculatorStyles.operator,
696
+ ]}
697
+ >
698
+ <Text {...stylex.props(roles.h2)}>
699
+ {calculatorKeyLabel(calculation, key)}
700
+ </Text>
701
+ </Button>
702
+ )
703
+ })}
704
+ </Flex>
705
+ </FeatureFrame>
706
+ )
707
+ }
708
+
709
+ export function RandomPicker(input: KowalskiInput<'randomPicker'>) {
710
+ const props = kowalskiCatalog.randomPicker.props.parse(input)
711
+ const schema = kowalskiCatalog.randomPicker.storeSchema
712
+ const name = props.id ?? 'KowalskiRandomPicker'
713
+ const [poolExpanded, setPoolExpanded] = useState(false)
714
+ const store = useConfiguredFeature({
715
+ name,
716
+ revision: props.revision,
717
+ schema,
718
+ scope: props.scope,
719
+ seed: schema.parse({}),
720
+ })
721
+ const domain: PickerDomain =
722
+ props.mode === 'list'
723
+ ? { mode: props.mode, options: props.options }
724
+ : props.mode === 'range'
725
+ ? { maximum: props.maximum, minimum: props.minimum, mode: props.mode }
726
+ : { mode: props.mode }
727
+ // A stable key for the effect below; the saved domain itself stays structured.
728
+ const domainKey = JSON.stringify(domain)
729
+ const numbers = format.formatters.getNumberFormat(format.locale)
730
+ const detail =
731
+ props.mode === 'range'
732
+ ? `${numbers.format(props.minimum)} to ${numbers.format(props.maximum)}, inclusive`
733
+ : undefined
734
+ const { ready, set, value } = store
735
+ const result =
736
+ value.result && isEqual(value.result.domain, domain) ? value.result.value : null
737
+ const resultDisplay = typeof result === 'number' ? numbers.format(result) : result
738
+ const resultLabel =
739
+ props.mode === 'dice'
740
+ ? 'You rolled'
741
+ : props.mode === 'coin'
742
+ ? 'It landed on'
743
+ : 'Your pick'
744
+ const attemptedReset = useMemo<{ key?: string }>(() => ({}), [name, props.scope])
745
+ useEffect(() => {
746
+ const key = JSON.stringify([domainKey, props.revision, value.result])
747
+ if (
748
+ ready &&
749
+ attemptedReset.key !== key &&
750
+ props.revision >= value.configurationRevision &&
751
+ value.result &&
752
+ JSON.stringify(value.result.domain) !== domainKey
753
+ ) {
754
+ attemptedReset.key = key
755
+ set({ result: null })
756
+ }
757
+ }, [
758
+ attemptedReset,
759
+ props.revision,
760
+ domainKey,
761
+ ready,
762
+ set,
763
+ value.configurationRevision,
764
+ value.result,
765
+ ])
766
+ return (
767
+ <FeatureFrame
768
+ onRetry={store.retry}
769
+ notice={store.error}
770
+ component="randomPicker"
771
+ title={props.title}
772
+ detail={detail}
773
+ >
774
+ {props.mode === 'list' && (
775
+ <Field label="Choices">
776
+ <FeatureFacts>
777
+ {props.options
778
+ .slice(0, poolExpanded ? props.options.length : 4)
779
+ .map((option, index) => (
780
+ <List.Item
781
+ key={index}
782
+ label={`${format.formatNumber(index + 1)}. ${option}`}
783
+ trailing={
784
+ result === option ? (
785
+ <Icon name="checkmark" {...stylex.props(appearance.accent)} />
786
+ ) : undefined
787
+ }
788
+ />
789
+ ))}
790
+ </FeatureFacts>
791
+ {props.options.length > 4 && (
792
+ <Button
793
+ onClick={() => setPoolExpanded(current => !current)}
794
+ style={[button.quiet, randomPickerStyles.disclosure]}
795
+ >
796
+ <Text>
797
+ {poolExpanded
798
+ ? 'Show fewer'
799
+ : `Show all ${format.formatNumber(props.options.length)} options`}
800
+ </Text>
801
+ </Button>
802
+ )}
803
+ </Field>
804
+ )}
805
+ <FeatureWell style={randomPickerStyles.stage}>
806
+ {!store.ready && !store.error ? (
807
+ <Text {...stylex.props(roles.muted)}>Loading last result…</Text>
808
+ ) : props.mode === 'coin' || props.mode === 'dice' ? (
809
+ <>
810
+ <Text {...stylex.props(roles.h2)}>
811
+ {result === null
812
+ ? props.mode === 'coin'
813
+ ? 'Heads or tails?'
814
+ : 'Ready to roll'
815
+ : resultLabel}
816
+ </Text>
817
+ <Flex
818
+ {...stylex.props(
819
+ appearance.surface,
820
+ randomPickerStyles.face,
821
+ props.mode === 'coin' && randomPickerStyles.coin
822
+ )}
823
+ >
824
+ <Text
825
+ {...stylex.props(
826
+ roles.display,
827
+ randomPickerStyles.faceValue,
828
+ props.mode === 'coin' && result !== null && randomPickerStyles.coinValue
829
+ )}
830
+ >
831
+ {resultDisplay ?? '?'}
832
+ </Text>
833
+ </Flex>
834
+ </>
835
+ ) : resultDisplay === null ? (
836
+ <Text {...stylex.props(roles.muted)}>No pick yet</Text>
837
+ ) : (
838
+ <>
839
+ <Text {...stylex.props(roles.muted)}>{resultLabel}</Text>
840
+ <Text
841
+ {...stylex.props(
842
+ roles.display,
843
+ randomPickerStyles.result(resultDisplay.length),
844
+ props.mode === 'list' && randomPickerStyles.listResult
845
+ )}
846
+ >
847
+ {resultDisplay}
848
+ </Text>
849
+ </>
850
+ )}
851
+ </FeatureWell>
852
+ {store.saving && <Text {...stylex.props(roles.muted)}>Saving pick…</Text>}
853
+ <FeatureAction
854
+ component="randomPicker"
855
+ icon="shuffle"
856
+ size="lg"
857
+ variant="primary"
858
+ disabled={!store.ready}
859
+ onClick={() => {
860
+ store.set(() => ({
861
+ result: { domain, value: pickResult(domain, Math.random()) },
862
+ }))
863
+ }}
864
+ >
865
+ {props.mode === 'coin'
866
+ ? result === null
867
+ ? 'Flip coin'
868
+ : 'Flip again'
869
+ : props.mode === 'dice'
870
+ ? result === null
871
+ ? 'Roll die'
872
+ : 'Roll again'
873
+ : result === null
874
+ ? 'Pick one'
875
+ : 'Pick again'}
876
+ </FeatureAction>
877
+ </FeatureFrame>
878
+ )
879
+ }
880
+
881
+ export function ColorPalette(input: KowalskiInput<'colorPalette'>) {
882
+ const props = kowalskiCatalog.colorPalette.props.parse(input)
883
+ const schema = kowalskiCatalog.colorPalette.storeSchema
884
+ const store = useConfiguredFeature({
885
+ name: props.id ?? 'KowalskiColorPalette',
886
+ revision: props.revision,
887
+ schema,
888
+ scope: props.scope,
889
+ seed: schema.parse({ color: { value: props.color } }),
890
+ })
891
+ const color = store.value.color.value
892
+ const [hexDraft, setHexDraft] = useState<string | null>(null)
893
+ const [paletteBase, setPaletteBase] = useState(color.toLowerCase())
894
+ const editing = hexDraft !== null
895
+ const draftColor = `#${hexDraft ?? ''}`
896
+ const validDraft =
897
+ kowalskiCatalog.colorPalette.props.shape.color.safeParse(draftColor).success
898
+ const editDisabled = !store.ready || store.saving || (editing && !validDraft)
899
+ const shadesDisabled = !store.ready || editing
900
+ const shades = uniqBy(
901
+ [
902
+ { label: 'Darker', mix: -0.6 },
903
+ { label: 'Dark', mix: -0.3 },
904
+ { label: 'Base', mix: 0 },
905
+ { label: 'Light', mix: 0.3 },
906
+ { label: 'Lighter', mix: 0.6 },
907
+ ].map(shade => {
908
+ const value = mixColor(paletteBase, shade.mix)
909
+ return { color: value, label: value === paletteBase ? 'Base' : shade.label }
910
+ }),
911
+ 'color'
912
+ )
913
+ useEffect(() => {
914
+ if (
915
+ !editing &&
916
+ !store.saving &&
917
+ ![-0.6, -0.3, 0, 0.3, 0.6].some(
918
+ mix => mixColor(paletteBase, mix) === color.toLowerCase()
919
+ )
920
+ ) {
921
+ setPaletteBase(color.toLowerCase())
922
+ }
923
+ }, [color, editing, paletteBase, store.saving])
924
+ const channels = [1, 3, 5].map(index =>
925
+ Number.parseInt(color.slice(index, index + 2), 16)
926
+ )
927
+ return (
928
+ <FeatureFrame
929
+ onRetry={store.retry}
930
+ notice={store.error}
931
+ component="colorPalette"
932
+ title={props.title}
933
+ >
934
+ <Flex {...stylex.props(colorStyles.specimen)}>
935
+ <Flex {...stylex.props(appearance.surface, colorStyles.preview(color))} />
936
+ <Flex {...stylex.props(colorStyles.identity)}>
937
+ <Flex {...stylex.props(colorStyles.value)}>
938
+ <Text {...stylex.props(roles.h2)}>{color.toUpperCase()}</Text>
939
+ <Text {...stylex.props(roles.muted)}>
940
+ {`RGB (${channels.map(channel => format.formatNumber(channel)).join(', ')})`}
941
+ </Text>
942
+ </Flex>
943
+ </Flex>
944
+ </Flex>
945
+ <Field label="Shades & tints">
946
+ <Flex {...stylex.props(colorStyles.swatches(shades.length))}>
947
+ {shades.map(shade => {
948
+ const selected = shade.color === color.toLowerCase()
949
+ return (
950
+ <Button
951
+ key={shade.color}
952
+ selected={selected}
953
+ disabled={shadesDisabled}
954
+ onClick={() => {
955
+ if (!shadesDisabled) {
956
+ store.set(() => ({ color: { value: shade.color } }))
957
+ }
958
+ }}
959
+ style={[
960
+ button.secondary,
961
+ colorStyles.swatchButton,
962
+ shadesDisabled && button.disabled,
963
+ ]}
964
+ >
965
+ <Flex {...stylex.props(colorStyles.swatchContent)}>
966
+ <Flex
967
+ {...stylex.props(appearance.surface, colorStyles.swatch(shade.color))}
968
+ >
969
+ {selected && (
970
+ <Flex {...stylex.props(colorStyles.selected)}>
971
+ <Icon name="checkmark" />
972
+ </Flex>
973
+ )}
974
+ </Flex>
975
+ <Text {...stylex.props(roles.label, colorStyles.centered)}>
976
+ {shade.label}
977
+ </Text>
978
+ <Text {...stylex.props(roles.muted, colorStyles.centered)}>
979
+ {shade.color.toUpperCase()}
980
+ </Text>
981
+ </Flex>
982
+ </Button>
983
+ )
984
+ })}
985
+ </Flex>
986
+ {(store.saving || (!store.ready && !store.error) || editing) && (
987
+ <Text {...stylex.props(roles.muted)}>
988
+ {store.saving
989
+ ? 'Saving color…'
990
+ : editing
991
+ ? 'Finish editing to choose a shade.'
992
+ : 'Loading saved color…'}
993
+ </Text>
994
+ )}
995
+ </Field>
996
+ {editing && (
997
+ <Field
998
+ error={!validDraft && 'Enter six digits using 0–9 and A–F.'}
999
+ label="Hex color"
1000
+ >
1001
+ <Flex {...stylex.props(colorStyles.inputRow)}>
1002
+ <Text {...stylex.props(roles.h2, colorStyles.prefix)}>#</Text>
1003
+ {store.ready && !store.saving ? (
1004
+ <Input
1005
+ placeholder="Hex color"
1006
+ value={hexDraft}
1007
+ onChange={value => setHexDraft(value.replace(/^#/, '').toUpperCase())}
1008
+ style={[colorStyles.input]}
1009
+ />
1010
+ ) : (
1011
+ <Text {...stylex.props(roles.text, colorStyles.input)}>{hexDraft}</Text>
1012
+ )}
1013
+ </Flex>
1014
+ </Field>
1015
+ )}
1016
+ <Flex {...stylex.props(layout.actions)}>
1017
+ {editing && (
1018
+ <FeatureAction
1019
+ component="colorPalette"
1020
+ disabled={store.saving}
1021
+ onClick={() => setHexDraft(null)}
1022
+ >
1023
+ Cancel
1024
+ </FeatureAction>
1025
+ )}
1026
+ <FeatureAction
1027
+ component="colorPalette"
1028
+ disabled={editDisabled}
1029
+ variant={editing ? 'primary' : 'secondary'}
1030
+ onClick={() => {
1031
+ if (editDisabled) {
1032
+ return
1033
+ }
1034
+ if (!editing) {
1035
+ setHexDraft(color.slice(1).toUpperCase())
1036
+ } else if (draftColor.toLowerCase() === color.toLowerCase()) {
1037
+ setHexDraft(null)
1038
+ } else {
1039
+ store.set(
1040
+ () => ({ color: { value: draftColor } }),
1041
+ () => {
1042
+ setPaletteBase(draftColor.toLowerCase())
1043
+ setHexDraft(null)
1044
+ }
1045
+ )
1046
+ }
1047
+ }}
1048
+ >
1049
+ {store.saving && editing ? 'Saving…' : editing ? 'Done' : 'Edit hex'}
1050
+ </FeatureAction>
1051
+ </Flex>
1052
+ </FeatureFrame>
1053
+ )
1054
+ }
1055
+
1056
+ const randomPickerStyles = stylex.create({
1057
+ coin: { borderRadius: radius['full'] },
1058
+ coinValue: { fontSize: text['3xl'], lineHeight: text['3xlLeading'] },
1059
+ disclosure: { alignSelf: 'flex-start', paddingLeft: 0, paddingRight: 0 },
1060
+ face: {
1061
+ alignItems: 'center',
1062
+ borderRadius: radius['3xl'],
1063
+ height: space[24],
1064
+ justifyContent: 'center',
1065
+ width: space[24],
1066
+ },
1067
+ faceValue: { textAlign: 'center' },
1068
+ listResult: { fontSize: text['3xl'], lineHeight: text['3xlLeading'] },
1069
+ result: (length: number) => ({
1070
+ fontSize: length > 14 ? text['2xl'] : length > 8 ? text['4xl'] : font.displaySize,
1071
+ lineHeight:
1072
+ length > 14
1073
+ ? text['2xlLeading']
1074
+ : length > 8
1075
+ ? text['4xlLeading']
1076
+ : font.displayLeading,
1077
+ textAlign: 'center',
1078
+ }),
1079
+ stage: { alignItems: 'center', gap: space[4], justifyContent: 'center' },
1080
+ })
1081
+
1082
+ // Long totals step down the display scale so they never overflow the well.
1083
+ const billStyles = stylex.create({
1084
+ amount: (length: number) => ({
1085
+ fontSize: length > 16 ? text.lg : length > 9 ? text['4xl'] : font.displaySize,
1086
+ lineHeight:
1087
+ length > 16
1088
+ ? text.lgLeading
1089
+ : length > 9
1090
+ ? text['4xlLeading']
1091
+ : font.displayLeading,
1092
+ }),
1093
+ centered: { textAlign: 'center' },
1094
+ row: {
1095
+ alignItems: 'center',
1096
+ flexDirection: 'row',
1097
+ flexWrap: 'wrap',
1098
+ gap: space[2],
1099
+ justifyContent: 'space-between',
1100
+ },
1101
+ })
1102
+
1103
+ const colorStyles = stylex.create({
1104
+ centered: { textAlign: 'center' },
1105
+ identity: {
1106
+ alignItems: 'center',
1107
+ flexDirection: 'row',
1108
+ flexWrap: 'wrap',
1109
+ gap: space[3],
1110
+ justifyContent: 'space-between',
1111
+ },
1112
+ input: { flexGrow: 1, minWidth: 0 },
1113
+ inputRow: { alignItems: 'center', flexDirection: 'row', gap: space[2] },
1114
+ prefix: { flexShrink: 0 },
1115
+ preview: (color: string) => ({
1116
+ backgroundColor: color,
1117
+ borderRadius: radius['xl'],
1118
+ height: space[24],
1119
+ }),
1120
+ selected: {
1121
+ alignItems: 'center',
1122
+ backgroundColor: colors.accent,
1123
+ borderRadius: radius['xl'],
1124
+ color: colors.onAccent,
1125
+ height: size[6],
1126
+ justifyContent: 'center',
1127
+ width: size[6],
1128
+ },
1129
+ specimen: { gap: space[4] },
1130
+ swatch: (color: string) => ({
1131
+ alignItems: 'center',
1132
+ backgroundColor: color,
1133
+ borderRadius: radius['lg'],
1134
+ height: size[16],
1135
+ justifyContent: 'center',
1136
+ }),
1137
+ swatchButton: {
1138
+ alignItems: 'stretch',
1139
+ minWidth: 0,
1140
+ paddingBottom: space[2],
1141
+ paddingLeft: space[1],
1142
+ paddingRight: space[1],
1143
+ paddingTop: space[1],
1144
+ },
1145
+ swatchContent: { alignItems: 'stretch', gap: space[1.5] },
1146
+ swatches: (count: number) => ({
1147
+ display: 'grid',
1148
+ gap: space[2],
1149
+ gridTemplateColumns: `repeat(${Math.min(count, 3)}, minmax(0, 1fr))`,
1150
+ }),
1151
+ value: { gap: space[1], minWidth: 0 },
1152
+ })
1153
+
1154
+ // Keys sit directly in the tray at p-4, so their control radius stays concentric with it.
1155
+ const calculatorStyles = stylex.create({
1156
+ equation: { textAlign: 'right' },
1157
+ error: { textAlign: 'right' },
1158
+ key: {
1159
+ alignItems: 'center',
1160
+ justifyContent: 'center',
1161
+ minHeight: size[12],
1162
+ minWidth: 0,
1163
+ paddingLeft: 0,
1164
+ paddingRight: 0,
1165
+ textAlign: 'center',
1166
+ },
1167
+ keypad: {
1168
+ display: 'grid',
1169
+ gap: space[2],
1170
+ gridTemplateColumns: 'repeat(4, minmax(0, 1fr))',
1171
+ },
1172
+ operation: { [buttonVars.background]: colors.tray },
1173
+ operator: { [buttonVars.text]: colors.accentInk },
1174
+ utility: { [buttonVars.text]: colors.muted },
1175
+ value: (length: number) => ({
1176
+ fontSize: length > 16 ? text.lg : length > 9 ? text['4xl'] : font.displaySize,
1177
+ lineHeight:
1178
+ length > 16
1179
+ ? text.lgLeading
1180
+ : length > 9
1181
+ ? text['4xlLeading']
1182
+ : font.displayLeading,
1183
+ textAlign: 'right',
1184
+ }),
1185
+ })
1186
+
1187
+ const unitStyles = stylex.create({
1188
+ result: { minWidth: 0 },
1189
+ })
1190
+
1191
+ // Long amounts step down the display scale so they never overflow the well.
1192
+ const expenseStyles = stylex.create({
1193
+ amount: (length: number) => ({
1194
+ fontSize: length > 16 ? text.lg : length > 9 ? text['2xl'] : text['4xl'],
1195
+ lineHeight:
1196
+ length > 16 ? text.lgLeading : length > 9 ? text['2xlLeading'] : text['4xlLeading'],
1197
+ width: '100%',
1198
+ }),
1199
+ })
1200
+
1201
+ function mixColor(hex: string, amount: number) {
1202
+ const target = amount < 0 ? 0 : 255
1203
+ return `#${[1, 3, 5]
1204
+ .map(index =>
1205
+ Math.round(
1206
+ Number.parseInt(hex.slice(index, index + 2), 16) * (1 - Math.abs(amount)) +
1207
+ target * Math.abs(amount)
1208
+ )
1209
+ .toString(16)
1210
+ .padStart(2, '0')
1211
+ )
1212
+ .join('')}`
1213
+ }