@flickfyi/flkr 0.0.140 → 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 +9 -9
  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,1387 @@
1
+ import { Flex, Icon, Text } from '@flickfyi/core'
2
+ import { Button as NativeButton, Input } from '@flickfyi/core/atoms'
3
+ import { List } from '@flickfyi/core/molecules'
4
+ import * as stylex from '@stylexjs/stylex'
5
+ import { toArray } from 'lodash'
6
+ import { useMemo, useState } from 'react'
7
+
8
+ import { size, space, text } from '~/themes/scale.stylex'
9
+ import { appearance, button, roles } from '~/themes/shared'
10
+
11
+ import { nextReminder } from './calculations'
12
+ import { kowalskiCatalog, type KowalskiInput } from './catalog'
13
+ import { FeatureDate, FeatureDateTime } from './date-time'
14
+ import { format } from './format'
15
+ import {
16
+ FeatureAction,
17
+ FeatureEdit,
18
+ FeatureFacts,
19
+ FeatureFrame,
20
+ FeatureWell,
21
+ layout,
22
+ } from './frame'
23
+ import { useClock } from './use-clock'
24
+ import { useConfiguredFeature } from './use-configured-feature'
25
+
26
+ export function Event(input: KowalskiInput<'event'>) {
27
+ const props = kowalskiCatalog.event.props.parse(input)
28
+ const schema = kowalskiCatalog.event.storeSchema
29
+ const store = useConfiguredFeature({
30
+ name: props.id ?? 'KowalskiEvent',
31
+ revision: props.revision,
32
+ schema,
33
+ scope: props.scope,
34
+ seed: schema.parse({
35
+ location: { value: props.location },
36
+ startsAt: { value: props.startsAt },
37
+ title: { value: props.title },
38
+ }),
39
+ })
40
+ const [draft, setDraft] = useState({
41
+ location: '',
42
+ startsAt: props.startsAt,
43
+ title: '',
44
+ })
45
+ const [baseline, setBaseline] = useState<typeof draft | null>(null)
46
+ const [editingDate, setEditingDate] = useState(false)
47
+ const editing = baseline !== null
48
+ const title = store.value.title.value.trim()
49
+ ? store.value.title.value
50
+ : 'Untitled event'
51
+ const instant = store.value.startsAt.value
52
+ ? Date.parse(store.value.startsAt.value)
53
+ : null
54
+ const timeParts =
55
+ instant === null
56
+ ? []
57
+ : format.formatters
58
+ .getDateTimeFormat(format.locale, {
59
+ fractionalSecondDigits: instant % 1000 === 0 ? undefined : 3,
60
+ hour: 'numeric',
61
+ hour12: true,
62
+ minute: '2-digit',
63
+ second: instant % 60_000 === 0 ? undefined : '2-digit',
64
+ timeZone: props.timeZone,
65
+ timeZoneName: 'long',
66
+ })
67
+ .formatToParts(instant)
68
+ const validTitle =
69
+ draft.title.trim().length > 0 &&
70
+ kowalskiCatalog.event.props.shape.title.safeParse(draft.title).success
71
+ const validLocation = kowalskiCatalog.event.props.shape.location.safeParse(
72
+ draft.location
73
+ ).success
74
+ const changed =
75
+ baseline &&
76
+ (draft.title !== baseline.title ||
77
+ draft.location !== baseline.location ||
78
+ draft.startsAt !== baseline.startsAt)
79
+ return (
80
+ <FeatureFrame
81
+ onRetry={store.retry}
82
+ notice={store.error}
83
+ component="event"
84
+ title={title}
85
+ detail={
86
+ instant === null
87
+ ? 'Date to be decided'
88
+ : format.formatters
89
+ .getDateTimeFormat(format.locale, {
90
+ day: 'numeric',
91
+ month: 'long',
92
+ timeZone: props.timeZone,
93
+ weekday: 'long',
94
+ })
95
+ .format(instant)
96
+ }
97
+ >
98
+ {editing && !store.saving ? (
99
+ <Flex {...stylex.props(eventStyles.editor)}>
100
+ <Flex {...stylex.props(layout.field)}>
101
+ <Text {...stylex.props(roles.label, layout.fieldLabel)}>Title</Text>
102
+ <Input
103
+ placeholder="Title"
104
+ value={draft.title}
105
+ onChange={title => setDraft(current => ({ ...current, title }))}
106
+ style={[layout.input]}
107
+ />
108
+ {!validTitle && (
109
+ <Text {...stylex.props(roles.muted)}>
110
+ {draft.title.trim()
111
+ ? 'Use 160 characters or fewer.'
112
+ : 'Add an event title.'}
113
+ </Text>
114
+ )}
115
+ </Flex>
116
+ <FeatureDateTime
117
+ inline
118
+ clearable
119
+ component="event"
120
+ label="Start date & time"
121
+ onChange={startsAt => setDraft(current => ({ ...current, startsAt }))}
122
+ onEditingChange={setEditingDate}
123
+ ready={store.ready}
124
+ timeZone={props.timeZone}
125
+ value={draft.startsAt}
126
+ />
127
+ <Flex {...stylex.props(layout.field)}>
128
+ <Flex {...stylex.props(eventStyles.fieldLabel)}>
129
+ <Text {...stylex.props(roles.label, layout.fieldLabel)}>Location</Text>
130
+ <Text {...stylex.props(roles.muted, eventStyles.optional)}>Optional</Text>
131
+ </Flex>
132
+ <Input
133
+ placeholder="Location"
134
+ value={draft.location}
135
+ onChange={location => setDraft(current => ({ ...current, location }))}
136
+ style={[layout.input]}
137
+ />
138
+ {!validLocation && (
139
+ <Text {...stylex.props(roles.muted)}>Use 500 characters or fewer.</Text>
140
+ )}
141
+ </Flex>
142
+ <Flex {...stylex.props(layout.actions)}>
143
+ <FeatureAction component="event" onClick={() => setBaseline(null)}>
144
+ Cancel
145
+ </FeatureAction>
146
+ <FeatureAction
147
+ component="event"
148
+ disabled={
149
+ editingDate ||
150
+ store.saving ||
151
+ !store.ready ||
152
+ !changed ||
153
+ !validTitle ||
154
+ !validLocation
155
+ }
156
+ onClick={() => {
157
+ if (
158
+ editingDate ||
159
+ !baseline ||
160
+ !changed ||
161
+ !validTitle ||
162
+ !validLocation
163
+ ) {
164
+ return
165
+ }
166
+ store.set(
167
+ current => ({
168
+ ...(draft.title !== baseline.title &&
169
+ draft.title !== current.title.value && {
170
+ title: { value: draft.title },
171
+ }),
172
+ ...(draft.startsAt !== baseline.startsAt &&
173
+ draft.startsAt !== current.startsAt.value && {
174
+ startsAt: { value: draft.startsAt },
175
+ }),
176
+ ...(draft.location !== baseline.location &&
177
+ draft.location !== current.location.value && {
178
+ location: { value: draft.location },
179
+ }),
180
+ }),
181
+ () => setBaseline(null)
182
+ )
183
+ }}
184
+ variant="primary"
185
+ >
186
+ Save changes
187
+ </FeatureAction>
188
+ </Flex>
189
+ </Flex>
190
+ ) : (
191
+ <>
192
+ {store.saving ? (
193
+ <Text {...stylex.props(roles.muted)}>Saving changes…</Text>
194
+ ) : !store.ready && !store.error ? (
195
+ <Text {...stylex.props(roles.muted)}>Loading event…</Text>
196
+ ) : null}
197
+ <FeatureFacts>
198
+ <List.Item
199
+ label="Time"
200
+ trailing={
201
+ instant === null
202
+ ? 'Not set'
203
+ : timeParts
204
+ .filter(part => part.type !== 'timeZoneName')
205
+ .map(part => part.value)
206
+ .join('')
207
+ .trim()
208
+ }
209
+ />
210
+ <List.Item
211
+ label="Where"
212
+ trailing={
213
+ store.value.location.value.trim()
214
+ ? store.value.location.value
215
+ : 'To be decided'
216
+ }
217
+ />
218
+ </FeatureFacts>
219
+ {!editing && (
220
+ <FeatureAction
221
+ component="event"
222
+ disabled={!store.ready || store.saving}
223
+ icon="pencil"
224
+ onClick={() => {
225
+ const values = {
226
+ location: store.value.location.value,
227
+ startsAt: store.value.startsAt.value,
228
+ title: store.value.title.value,
229
+ }
230
+ setDraft(values)
231
+ setBaseline(values)
232
+ }}
233
+ >
234
+ Edit event
235
+ </FeatureAction>
236
+ )}
237
+ </>
238
+ )}
239
+ </FeatureFrame>
240
+ )
241
+ }
242
+
243
+ export function Reminder(input: KowalskiInput<'reminder'>) {
244
+ const props = kowalskiCatalog.reminder.props.parse(input)
245
+ const timeZone = props.timeZone ?? Intl.DateTimeFormat().resolvedOptions().timeZone
246
+ const schema = kowalskiCatalog.reminder.storeSchema
247
+ const store = useConfiguredFeature({
248
+ name: props.id ?? 'KowalskiReminder',
249
+ revision: props.revision,
250
+ schema,
251
+ scope: props.scope,
252
+ seed: schema.parse({ dueAt: { value: props.dueAt }, title: { value: props.title } }),
253
+ })
254
+ const [draft, setDraft] = useState({ dueAt: props.dueAt, title: props.title })
255
+ const [baseline, setBaseline] = useState<typeof draft | null>(null)
256
+ const [editingDate, setEditingDate] = useState(false)
257
+ const editing = baseline !== null
258
+ const done = store.value.completedAt !== null
259
+ const due = store.value.dueAt.value ? Date.parse(store.value.dueAt.value) : null
260
+ const now = useClock(due !== null)
261
+ const title = store.value.title.value.trim()
262
+ ? store.value.title.value
263
+ : 'Untitled reminder'
264
+ const dateFormat = format.formatters.getDateTimeFormat(format.locale, {
265
+ day: 'numeric',
266
+ month: 'long',
267
+ timeZone,
268
+ weekday: 'short',
269
+ year: 'numeric',
270
+ })
271
+ const timeFormat = format.formatters.getDateTimeFormat(format.locale, {
272
+ fractionalSecondDigits: due !== null && due % 1000 !== 0 ? 3 : undefined,
273
+ hour: 'numeric',
274
+ hour12: true,
275
+ minute: '2-digit',
276
+ second: due !== null && due % 60_000 !== 0 ? '2-digit' : undefined,
277
+ timeZone,
278
+ })
279
+ const dueStatus =
280
+ due === null
281
+ ? 'No due date'
282
+ : done
283
+ ? 'Due date'
284
+ : due < now
285
+ ? 'Overdue'
286
+ : dateFormat.format(due) === dateFormat.format(now)
287
+ ? 'Due today'
288
+ : 'Upcoming'
289
+ const next = done ? nextReminder(store.value.dueAt.value, props.recurrence, now) : null
290
+ const validTitle =
291
+ draft.title.trim().length > 0 &&
292
+ kowalskiCatalog.reminder.props.shape.title.safeParse(draft.title).success
293
+ const changed =
294
+ baseline && (draft.title !== baseline.title || draft.dueAt !== baseline.dueAt)
295
+ const edit = () => {
296
+ const values = { dueAt: store.value.dueAt.value, title: store.value.title.value }
297
+ setDraft(values)
298
+ setBaseline(values)
299
+ }
300
+ return (
301
+ <FeatureFrame
302
+ onRetry={store.retry}
303
+ notice={store.error}
304
+ component="reminder"
305
+ title={title}
306
+ detail={
307
+ store.value.completedAt === null
308
+ ? undefined
309
+ : `Completed ${format.formatters
310
+ .getDateTimeFormat(format.locale, {
311
+ dateStyle: 'medium',
312
+ hour12: true,
313
+ timeStyle: 'short',
314
+ timeZone,
315
+ })
316
+ .format(store.value.completedAt)}`
317
+ }
318
+ trailing={
319
+ !editing && (
320
+ <FeatureEdit disabled={!store.ready} label="Edit reminder" onClick={edit} />
321
+ )
322
+ }
323
+ >
324
+ {editing && store.saving ? (
325
+ <Text {...stylex.props(roles.muted)}>Saving changes…</Text>
326
+ ) : editing ? (
327
+ <Flex {...stylex.props(reminderStyles.editor)}>
328
+ <Flex {...stylex.props(layout.field)}>
329
+ <Text {...stylex.props(roles.label, layout.fieldLabel)}>Reminder</Text>
330
+ <Input
331
+ placeholder="Reminder"
332
+ value={draft.title}
333
+ onChange={title => setDraft(current => ({ ...current, title }))}
334
+ style={[layout.input]}
335
+ />
336
+ {!validTitle && (
337
+ <Text {...stylex.props(roles.muted)}>
338
+ {draft.title.trim()
339
+ ? 'Use 500 characters or fewer.'
340
+ : 'Add a reminder title.'}
341
+ </Text>
342
+ )}
343
+ </Flex>
344
+ <FeatureDateTime
345
+ inline
346
+ clearable
347
+ component="reminder"
348
+ label="Due date & time"
349
+ onChange={dueAt => setDraft(current => ({ ...current, dueAt }))}
350
+ onEditingChange={setEditingDate}
351
+ ready={store.ready}
352
+ timeZone={timeZone}
353
+ value={draft.dueAt}
354
+ />
355
+ <Flex {...stylex.props(layout.actions)}>
356
+ <FeatureAction component="reminder" onClick={() => setBaseline(null)}>
357
+ Cancel
358
+ </FeatureAction>
359
+ <FeatureAction
360
+ component="reminder"
361
+ disabled={
362
+ editingDate || store.saving || !store.ready || !changed || !validTitle
363
+ }
364
+ onClick={() => {
365
+ if (editingDate || !baseline || !changed || !validTitle) {
366
+ return
367
+ }
368
+ store.set(
369
+ current => ({
370
+ ...(draft.title !== baseline.title &&
371
+ draft.title !== current.title.value && {
372
+ title: { value: draft.title },
373
+ }),
374
+ ...(draft.dueAt !== baseline.dueAt &&
375
+ draft.dueAt !== current.dueAt.value && {
376
+ dueAt: { value: draft.dueAt },
377
+ }),
378
+ }),
379
+ () => setBaseline(null)
380
+ )
381
+ }}
382
+ variant="primary"
383
+ >
384
+ {store.saving ? 'Saving…' : 'Save changes'}
385
+ </FeatureAction>
386
+ </Flex>
387
+ </Flex>
388
+ ) : (
389
+ <>
390
+ <FeatureFacts>
391
+ <List.Item
392
+ label={dueStatus}
393
+ trailing={due === null ? 'Not set' : dateFormat.format(due)}
394
+ />
395
+ {due !== null && <List.Item label="Time" trailing={timeFormat.format(due)} />}
396
+ {props.recurrence !== 'none' && (
397
+ <List.Item
398
+ description={
399
+ due === null ? 'Add a due date to start repeating.' : undefined
400
+ }
401
+ label="Repeats"
402
+ trailing={
403
+ props.recurrence === 'daily' ? 'Every 24 hours' : 'Every 7 days'
404
+ }
405
+ />
406
+ )}
407
+ {next && (
408
+ <List.Item
409
+ label="Next occurrence"
410
+ trailing={`${dateFormat.format(Date.parse(next))} · ${timeFormat.format(Date.parse(next))}`}
411
+ />
412
+ )}
413
+ </FeatureFacts>
414
+ <Flex {...stylex.props(layout.actions)}>
415
+ {next && (
416
+ <FeatureAction
417
+ component="reminder"
418
+ disabled={!store.ready}
419
+ icon="arrow.right"
420
+ onClick={() =>
421
+ store.set(current => {
422
+ const dueAt = nextReminder(
423
+ current.dueAt.value,
424
+ props.recurrence,
425
+ Date.now()
426
+ )
427
+ return current.completedAt !== null && dueAt
428
+ ? { completedAt: null, dueAt: { value: dueAt } }
429
+ : {}
430
+ })
431
+ }
432
+ variant="primary"
433
+ >
434
+ Start next reminder
435
+ </FeatureAction>
436
+ )}
437
+ <FeatureAction
438
+ component="reminder"
439
+ disabled={!store.ready}
440
+ icon={done ? 'arrow.uturn.backward' : 'checkmark'}
441
+ onClick={() =>
442
+ store.set(current => ({
443
+ completedAt: current.completedAt === null ? Date.now() : null,
444
+ }))
445
+ }
446
+ variant={next ? 'secondary' : 'primary'}
447
+ >
448
+ {done ? 'Reopen' : 'Complete'}
449
+ </FeatureAction>
450
+ </Flex>
451
+ </>
452
+ )}
453
+ </FeatureFrame>
454
+ )
455
+ }
456
+
457
+ export function Trip(input: KowalskiInput<'trip'>) {
458
+ const props = kowalskiCatalog.trip.props.parse(input)
459
+ const schema = kowalskiCatalog.trip.storeSchema
460
+ const store = useConfiguredFeature({
461
+ name: props.id ?? 'KowalskiTrip',
462
+ revision: props.revision,
463
+ schema,
464
+ scope: props.scope,
465
+ seed: schema.parse({
466
+ destination: { value: props.destination },
467
+ endDate: { value: props.endDate },
468
+ origin: { value: props.origin },
469
+ startDate: { value: props.startDate },
470
+ }),
471
+ })
472
+ const [draft, setDraft] = useState({
473
+ destination: props.destination,
474
+ endDate: props.endDate,
475
+ origin: props.origin,
476
+ startDate: props.startDate,
477
+ })
478
+ const [baseline, setBaseline] = useState<typeof draft | null>(null)
479
+ const [editingEndDate, setEditingEndDate] = useState(false)
480
+ const [editingStartDate, setEditingStartDate] = useState(false)
481
+ const [saveError, setSaveError] = useState(false)
482
+ const editing = baseline !== null
483
+ const transport = {
484
+ car: { icon: 'car.fill', label: 'By car' },
485
+ other: { icon: 'suitcase.fill', label: 'Travel plans' },
486
+ plane: { icon: 'airplane', label: 'By plane' },
487
+ train: { icon: 'tram.fill', label: 'By train' },
488
+ walk: { icon: 'figure.walk', label: 'On foot' },
489
+ }[props.transport]
490
+ const start = store.value.startDate.value
491
+ const end = store.value.endDate.value
492
+ const dateFormat = format.formatters.getDateTimeFormat(format.locale, {
493
+ day: 'numeric',
494
+ era: start?.startsWith('0000-') || end?.startsWith('0000-') ? 'short' : undefined,
495
+ month: 'short',
496
+ timeZone: 'UTC',
497
+ year: 'numeric',
498
+ })
499
+ const startInstant = start ? Date.parse(`${start}T12:00:00Z`) : null
500
+ const endInstant = end ? Date.parse(`${end}T12:00:00Z`) : null
501
+ const nights =
502
+ startInstant !== null && endInstant !== null
503
+ ? Math.round((endInstant - startInstant) / 86_400_000)
504
+ : null
505
+ const dates =
506
+ startInstant !== null && endInstant !== null
507
+ ? start === end
508
+ ? dateFormat.format(startInstant)
509
+ : `${dateFormat.format(startInstant)} – ${dateFormat.format(endInstant)}`
510
+ : startInstant === null
511
+ ? endInstant === null
512
+ ? 'Dates open'
513
+ : `Until ${dateFormat.format(endInstant)}`
514
+ : `From ${dateFormat.format(startInstant)}`
515
+ const duration =
516
+ nights === null
517
+ ? start
518
+ ? 'End date to be decided'
519
+ : end
520
+ ? 'Start date to be decided'
521
+ : 'Set dates when you’re ready'
522
+ : nights === 0
523
+ ? 'Day trip'
524
+ : format.formatMessage(
525
+ {
526
+ defaultMessage:
527
+ '{days, number} days · {nights, plural, one {# night} other {# nights}}',
528
+ id: 'kowalski.trip.duration',
529
+ },
530
+ { days: nights + 1, nights }
531
+ )
532
+ const changed =
533
+ baseline &&
534
+ (draft.destination !== baseline.destination ||
535
+ draft.endDate !== baseline.endDate ||
536
+ draft.origin !== baseline.origin ||
537
+ draft.startDate !== baseline.startDate)
538
+ const validOrigin = kowalskiCatalog.trip.props.shape.origin.safeParse(
539
+ draft.origin
540
+ ).success
541
+ const validDestination = kowalskiCatalog.trip.props.shape.destination.safeParse(
542
+ draft.destination
543
+ ).success
544
+ const nextStart =
545
+ baseline && draft.startDate !== baseline.startDate ? draft.startDate : start
546
+ const nextEnd = baseline && draft.endDate !== baseline.endDate ? draft.endDate : end
547
+ const validDates = !nextStart || !nextEnd || nextStart <= nextEnd
548
+ return (
549
+ <FeatureFrame
550
+ onRetry={store.retry}
551
+ notice={store.error}
552
+ component="trip"
553
+ title={props.title}
554
+ detail={transport.label}
555
+ >
556
+ {editing && store.saving ? (
557
+ <Text {...stylex.props(roles.muted)}>Saving changes…</Text>
558
+ ) : editing ? (
559
+ <Flex {...stylex.props(tripStyles.editor)}>
560
+ <Flex {...stylex.props(layout.field)}>
561
+ <Text {...stylex.props(roles.label, layout.fieldLabel)}>From</Text>
562
+ <Input
563
+ placeholder="From"
564
+ value={draft.origin}
565
+ onChange={origin => setDraft(current => ({ ...current, origin }))}
566
+ style={[layout.input]}
567
+ />
568
+ {!validOrigin && (
569
+ <Text {...stylex.props(roles.muted)}>Use 160 characters or fewer.</Text>
570
+ )}
571
+ </Flex>
572
+ <Flex {...stylex.props(layout.field)}>
573
+ <Text {...stylex.props(roles.label, layout.fieldLabel)}>To</Text>
574
+ <Input
575
+ placeholder="To"
576
+ value={draft.destination}
577
+ onChange={destination => setDraft(current => ({ ...current, destination }))}
578
+ style={[layout.input]}
579
+ />
580
+ {!validDestination && (
581
+ <Text {...stylex.props(roles.muted)}>Use 160 characters or fewer.</Text>
582
+ )}
583
+ </Flex>
584
+ <FeatureDate
585
+ inline
586
+ clearable
587
+ component="trip"
588
+ label="Start date"
589
+ onChange={startDate => {
590
+ setDraft(current => ({ ...current, startDate }))
591
+ setSaveError(false)
592
+ }}
593
+ onEditingChange={setEditingStartDate}
594
+ ready={store.ready}
595
+ value={draft.startDate}
596
+ />
597
+ <FeatureDate
598
+ inline
599
+ clearable
600
+ component="trip"
601
+ label="End date"
602
+ onChange={endDate => {
603
+ setDraft(current => ({ ...current, endDate }))
604
+ setSaveError(false)
605
+ }}
606
+ onEditingChange={setEditingEndDate}
607
+ ready={store.ready}
608
+ value={draft.endDate}
609
+ />
610
+ {(!validDates || saveError) && (
611
+ <Text {...stylex.props(roles.muted)}>
612
+ The trip cannot end before it starts. Adjust either date before saving.
613
+ </Text>
614
+ )}
615
+ <Flex {...stylex.props(layout.actions)}>
616
+ <FeatureAction
617
+ component="trip"
618
+ onClick={() => {
619
+ setBaseline(null)
620
+ setEditingEndDate(false)
621
+ setEditingStartDate(false)
622
+ }}
623
+ >
624
+ Cancel
625
+ </FeatureAction>
626
+ <FeatureAction
627
+ component="trip"
628
+ disabled={
629
+ store.saving ||
630
+ !store.ready ||
631
+ editingEndDate ||
632
+ editingStartDate ||
633
+ !changed ||
634
+ !validOrigin ||
635
+ !validDestination ||
636
+ !validDates
637
+ }
638
+ onClick={() => {
639
+ if (
640
+ store.saving ||
641
+ !store.ready ||
642
+ editingEndDate ||
643
+ editingStartDate ||
644
+ !baseline ||
645
+ !changed ||
646
+ !validOrigin ||
647
+ !validDestination ||
648
+ !validDates
649
+ ) {
650
+ return
651
+ }
652
+ try {
653
+ store.set(
654
+ current => ({
655
+ ...(draft.destination !== baseline.destination &&
656
+ draft.destination !== current.destination.value && {
657
+ destination: { value: draft.destination },
658
+ }),
659
+ ...(draft.endDate !== baseline.endDate &&
660
+ draft.endDate !== current.endDate.value && {
661
+ endDate: { value: draft.endDate },
662
+ }),
663
+ ...(draft.origin !== baseline.origin &&
664
+ draft.origin !== current.origin.value && {
665
+ origin: { value: draft.origin },
666
+ }),
667
+ ...(draft.startDate !== baseline.startDate &&
668
+ draft.startDate !== current.startDate.value && {
669
+ startDate: { value: draft.startDate },
670
+ }),
671
+ }),
672
+ () => setBaseline(null)
673
+ )
674
+ } catch {
675
+ setSaveError(true)
676
+ }
677
+ }}
678
+ variant="primary"
679
+ >
680
+ {store.saving ? 'Saving…' : 'Save changes'}
681
+ </FeatureAction>
682
+ </Flex>
683
+ </Flex>
684
+ ) : (
685
+ <>
686
+ <FeatureFacts>
687
+ <List.Item
688
+ label="From"
689
+ trailing={
690
+ store.value.origin.value.trim()
691
+ ? store.value.origin.value
692
+ : 'To be decided'
693
+ }
694
+ />
695
+ <List.Item
696
+ label="To"
697
+ trailing={
698
+ store.value.destination.value.trim()
699
+ ? store.value.destination.value
700
+ : 'To be decided'
701
+ }
702
+ />
703
+ <List.Item description={duration} label="Dates" trailing={dates} />
704
+ </FeatureFacts>
705
+ {!editing && (
706
+ <FeatureAction
707
+ component="trip"
708
+ disabled={!store.ready || store.saving}
709
+ icon="pencil"
710
+ onClick={() => {
711
+ const values = {
712
+ destination: store.value.destination.value,
713
+ endDate: store.value.endDate.value,
714
+ origin: store.value.origin.value,
715
+ startDate: store.value.startDate.value,
716
+ }
717
+ setDraft(values)
718
+ setBaseline(values)
719
+ setSaveError(false)
720
+ }}
721
+ >
722
+ Edit trip
723
+ </FeatureAction>
724
+ )}
725
+ </>
726
+ )}
727
+ </FeatureFrame>
728
+ )
729
+ }
730
+
731
+ export function Contact(input: KowalskiInput<'contact'>) {
732
+ const props = kowalskiCatalog.contact.props.parse(input)
733
+ const schema = kowalskiCatalog.contact.storeSchema
734
+ const store = useConfiguredFeature({
735
+ name: props.id ?? 'KowalskiContact',
736
+ revision: props.revision,
737
+ schema,
738
+ scope: props.scope,
739
+ seed: schema.parse({
740
+ email: { value: props.email ?? null },
741
+ name: { value: props.name },
742
+ phone: { value: props.phone ?? null },
743
+ }),
744
+ })
745
+ const [draft, setDraft] = useState({ email: '', name: '', phone: '' })
746
+ const [baseline, setBaseline] = useState<typeof draft | null>(null)
747
+ const editing = baseline !== null
748
+ const name = store.value.name.value.trim() ? store.value.name.value : 'Unnamed contact'
749
+ const fields = [
750
+ {
751
+ key: 'name',
752
+ label: 'Name',
753
+ valid:
754
+ draft.name.trim().length > 0 &&
755
+ kowalskiCatalog.contact.props.shape.name.safeParse(draft.name).success,
756
+ validation: draft.name.trim() ? 'Use 160 characters or fewer.' : 'Add a name.',
757
+ },
758
+ {
759
+ key: 'phone',
760
+ label: 'Phone',
761
+ valid:
762
+ draft.phone === '' ||
763
+ kowalskiCatalog.contact.props.shape.phone.safeParse(draft.phone).success,
764
+ validation: 'Use 80 characters or fewer.',
765
+ },
766
+ {
767
+ key: 'email',
768
+ label: 'Email',
769
+ valid:
770
+ draft.email === '' ||
771
+ kowalskiCatalog.contact.props.shape.email.safeParse(draft.email).success,
772
+ validation: 'Enter a valid email address.',
773
+ },
774
+ ] as const
775
+ const changed =
776
+ baseline &&
777
+ (draft.email !== baseline.email ||
778
+ draft.name !== baseline.name ||
779
+ draft.phone !== baseline.phone)
780
+ return (
781
+ <FeatureFrame
782
+ onRetry={store.retry}
783
+ notice={store.error}
784
+ component="contact"
785
+ title={name}
786
+ trailing={
787
+ !editing && (
788
+ <FeatureEdit
789
+ disabled={!store.ready || store.saving}
790
+ label="Edit contact"
791
+ onClick={() => {
792
+ const values = {
793
+ email: store.value.email.value ?? '',
794
+ name: store.value.name.value,
795
+ phone: store.value.phone.value ?? '',
796
+ }
797
+ setBaseline(values)
798
+ setDraft(values)
799
+ }}
800
+ />
801
+ )
802
+ }
803
+ >
804
+ {editing && !store.saving ? (
805
+ <Flex {...stylex.props(contactStyles.editor)}>
806
+ {fields.map(field => (
807
+ <Flex key={field.key} {...stylex.props(layout.field)}>
808
+ <Flex {...stylex.props(contactStyles.fieldLabel)}>
809
+ <Text {...stylex.props(roles.label, layout.fieldLabel)}>
810
+ {field.label}
811
+ </Text>
812
+ {field.key !== 'name' && (
813
+ <Text {...stylex.props(roles.muted, contactStyles.optional)}>
814
+ Optional
815
+ </Text>
816
+ )}
817
+ </Flex>
818
+ <Input
819
+ placeholder={field.label}
820
+ value={draft[field.key]}
821
+ onChange={value =>
822
+ setDraft(current => ({ ...current, [field.key]: value }))
823
+ }
824
+ style={[layout.input]}
825
+ />
826
+ {!field.valid && (
827
+ <Text {...stylex.props(roles.muted)}>{field.validation}</Text>
828
+ )}
829
+ </Flex>
830
+ ))}
831
+ <Flex {...stylex.props(layout.actions)}>
832
+ <FeatureAction component="contact" onClick={() => setBaseline(null)}>
833
+ Cancel
834
+ </FeatureAction>
835
+ <FeatureAction
836
+ component="contact"
837
+ disabled={
838
+ store.saving ||
839
+ !store.ready ||
840
+ !changed ||
841
+ fields.some(field => !field.valid)
842
+ }
843
+ variant="primary"
844
+ onClick={() => {
845
+ if (!baseline || !changed || fields.some(field => !field.valid)) {
846
+ return
847
+ }
848
+ store.set(
849
+ current => ({
850
+ ...(draft.email !== baseline.email &&
851
+ draft.email !== (current.email.value ?? '') && {
852
+ email: { value: draft.email || null },
853
+ }),
854
+ ...(draft.name !== baseline.name &&
855
+ draft.name !== current.name.value && {
856
+ name: { value: draft.name },
857
+ }),
858
+ ...(draft.phone !== baseline.phone &&
859
+ draft.phone !== (current.phone.value ?? '') && {
860
+ phone: { value: draft.phone || null },
861
+ }),
862
+ }),
863
+ () => setBaseline(null)
864
+ )
865
+ }}
866
+ >
867
+ Save changes
868
+ </FeatureAction>
869
+ </Flex>
870
+ </Flex>
871
+ ) : (
872
+ <>
873
+ {store.saving ? (
874
+ <Text {...stylex.props(roles.muted)}>Saving changes…</Text>
875
+ ) : !store.ready && !store.error ? (
876
+ <Text {...stylex.props(roles.muted)}>Loading contact…</Text>
877
+ ) : null}
878
+ <FeatureFacts>
879
+ <List.Item
880
+ label="Phone"
881
+ trailing={store.value.phone.value ?? 'No phone number'}
882
+ />
883
+ <List.Item
884
+ label="Email"
885
+ trailing={store.value.email.value ?? 'No email address'}
886
+ />
887
+ </FeatureFacts>
888
+ <Flex {...stylex.props(layout.actions)}>
889
+ {(
890
+ [
891
+ { address: store.value.phone.value, icon: 'phone', verb: 'Call' },
892
+ { address: store.value.email.value, icon: 'envelope', verb: 'Email' },
893
+ ] as const
894
+ ).map(
895
+ field =>
896
+ field.address !== null && (
897
+ <NativeButton
898
+ accessibilityLabel={`${field.verb} ${name}`}
899
+ disabled={!store.ready || store.saving}
900
+ href={
901
+ field.verb === 'Call'
902
+ ? `tel:${encodeURIComponent(field.address.replaceAll(/[()\s.-]/g, ''))}`
903
+ : `mailto:${encodeURIComponent(field.address)}`
904
+ }
905
+ key={field.verb}
906
+ style={[button.secondary, button.lg, layout.action]}
907
+ >
908
+ <Flex {...stylex.props(layout.actionContent)}>
909
+ <Icon name={field.icon} />
910
+ <Text>{field.verb}</Text>
911
+ </Flex>
912
+ </NativeButton>
913
+ )
914
+ )}
915
+ </Flex>
916
+ </>
917
+ )}
918
+ </FeatureFrame>
919
+ )
920
+ }
921
+
922
+ export function Bookmark(input: KowalskiInput<'bookmark'>) {
923
+ const props = kowalskiCatalog.bookmark.props.parse(input)
924
+ const schema = kowalskiCatalog.bookmark.storeSchema
925
+ const store = useConfiguredFeature({
926
+ name: props.id ?? 'KowalskiBookmark',
927
+ revision: props.revision,
928
+ schema,
929
+ scope: props.scope,
930
+ seed: schema.parse({
931
+ note: { value: props.note },
932
+ title: { value: props.title },
933
+ url: { value: props.url },
934
+ }),
935
+ })
936
+ const [draft, setDraft] = useState({ note: '', title: '', url: '' })
937
+ const [baseline, setBaseline] = useState<typeof draft | null>(null)
938
+ const [showAddress, setShowAddress] = useState(false)
939
+ const [showFullNote, setShowFullNote] = useState(false)
940
+ const editing = baseline !== null
941
+ const domain = (store.value.url.value ?? '')
942
+ .replace(/^https?:\/\//i, '')
943
+ .split(/[/?#]/)[0]
944
+ ?.split('@')
945
+ .pop()
946
+ ?.replace(/^www\./i, '')
947
+ const title = store.value.title.value.trim()
948
+ ? store.value.title.value
949
+ : domain || 'Untitled bookmark'
950
+ const note = store.value.note.value
951
+ const noteCharacters = toArray(note)
952
+ const longNote = noteCharacters.length > 360
953
+ const valid = {
954
+ note: kowalskiCatalog.bookmark.props.shape.note.safeParse(draft.note).success,
955
+ title: kowalskiCatalog.bookmark.props.shape.title.safeParse(draft.title).success,
956
+ url: kowalskiCatalog.bookmark.props.shape.url.safeParse(draft.url).success,
957
+ }
958
+ const changed =
959
+ baseline &&
960
+ (draft.note !== baseline.note ||
961
+ draft.title !== baseline.title ||
962
+ draft.url !== baseline.url)
963
+ function editBookmark() {
964
+ if (!store.ready || store.saving) {
965
+ return
966
+ }
967
+ const values = {
968
+ note: store.value.note.value,
969
+ title: store.value.title.value,
970
+ url: store.value.url.value ?? '',
971
+ }
972
+ setDraft(values)
973
+ setBaseline(values)
974
+ }
975
+ return (
976
+ <FeatureFrame
977
+ onRetry={store.retry}
978
+ notice={store.error}
979
+ component="bookmark"
980
+ title={title}
981
+ detail={domain || 'No web address'}
982
+ trailing={
983
+ !editing && (
984
+ <FeatureEdit
985
+ disabled={!store.ready || store.saving}
986
+ label="Edit bookmark"
987
+ onClick={editBookmark}
988
+ />
989
+ )
990
+ }
991
+ >
992
+ {editing ? (
993
+ <Flex {...stylex.props(bookmarkStyles.editor)}>
994
+ {store.saving && <Text {...stylex.props(roles.muted)}>Saving changes…</Text>}
995
+ {(
996
+ [
997
+ { key: 'title', label: 'Title', message: 'Use 160 characters or fewer.' },
998
+ {
999
+ key: 'url',
1000
+ label: 'Web address',
1001
+ message: 'Use a valid address starting with https:// or http://.',
1002
+ },
1003
+ { key: 'note', label: 'Note', message: 'Use 10,000 characters or fewer.' },
1004
+ ] as const
1005
+ ).map(field => (
1006
+ <Flex key={field.key} {...stylex.props(layout.field)}>
1007
+ <Flex {...stylex.props(bookmarkStyles.fieldLabel)}>
1008
+ <Text {...stylex.props(roles.label, layout.fieldLabel)}>
1009
+ {field.label}
1010
+ </Text>
1011
+ {field.key !== 'url' && (
1012
+ <Text {...stylex.props(roles.muted, bookmarkStyles.caption)}>
1013
+ Optional
1014
+ </Text>
1015
+ )}
1016
+ </Flex>
1017
+ {store.saving ? (
1018
+ <Text
1019
+ {...stylex.props(
1020
+ roles.text,
1021
+ appearance.surface,
1022
+ layout.input,
1023
+ bookmarkStyles.submittedValue
1024
+ )}
1025
+ >
1026
+ {draft[field.key] || (field.key === 'note' ? 'No note' : 'No title')}
1027
+ </Text>
1028
+ ) : (
1029
+ <Input
1030
+ multiline={field.key === 'note'}
1031
+ placeholder={field.label}
1032
+ value={draft[field.key]}
1033
+ onChange={value =>
1034
+ setDraft(current => ({ ...current, [field.key]: value }))
1035
+ }
1036
+ style={[layout.input, field.key === 'note' && layout.multiline]}
1037
+ />
1038
+ )}
1039
+ {!valid[field.key] && (
1040
+ <Text {...stylex.props(roles.muted)}>{field.message}</Text>
1041
+ )}
1042
+ </Flex>
1043
+ ))}
1044
+ <Flex {...stylex.props(layout.actions)}>
1045
+ <FeatureAction
1046
+ component="bookmark"
1047
+ disabled={store.saving}
1048
+ onClick={() => setBaseline(null)}
1049
+ >
1050
+ Cancel
1051
+ </FeatureAction>
1052
+ <FeatureAction
1053
+ component="bookmark"
1054
+ disabled={
1055
+ store.saving ||
1056
+ !store.ready ||
1057
+ !changed ||
1058
+ !valid.title ||
1059
+ !valid.url ||
1060
+ !valid.note
1061
+ }
1062
+ onClick={() => {
1063
+ if (
1064
+ !store.ready ||
1065
+ store.saving ||
1066
+ !baseline ||
1067
+ !changed ||
1068
+ !valid.title ||
1069
+ !valid.url ||
1070
+ !valid.note
1071
+ ) {
1072
+ return
1073
+ }
1074
+ store.set(
1075
+ current => ({
1076
+ ...(draft.title !== baseline.title &&
1077
+ draft.title !== current.title.value && {
1078
+ title: { value: draft.title },
1079
+ }),
1080
+ ...(draft.url !== baseline.url &&
1081
+ draft.url !== (current.url.value ?? '') && {
1082
+ url: { value: draft.url || null },
1083
+ }),
1084
+ ...(draft.note !== baseline.note &&
1085
+ draft.note !== current.note.value && {
1086
+ note: { value: draft.note },
1087
+ }),
1088
+ }),
1089
+ () => setBaseline(null)
1090
+ )
1091
+ }}
1092
+ variant="primary"
1093
+ >
1094
+ {store.saving ? 'Saving…' : 'Save changes'}
1095
+ </FeatureAction>
1096
+ </Flex>
1097
+ </Flex>
1098
+ ) : (
1099
+ <Flex {...stylex.props(bookmarkStyles.details)}>
1100
+ {store.saving ? (
1101
+ <Text {...stylex.props(roles.muted)}>Saving changes…</Text>
1102
+ ) : !store.ready && !store.error ? (
1103
+ <Text {...stylex.props(roles.muted)}>Loading bookmark…</Text>
1104
+ ) : null}
1105
+ <Flex {...stylex.props(bookmarkStyles.note)}>
1106
+ {note && (
1107
+ <Text {...stylex.props(roles.text, bookmarkStyles.noteText)}>
1108
+ {longNote && !showFullNote
1109
+ ? `${noteCharacters.slice(0, 360).join('')}…`
1110
+ : note}
1111
+ </Text>
1112
+ )}
1113
+ {longNote && (
1114
+ <FeatureAction
1115
+ component="bookmark"
1116
+ onClick={() => setShowFullNote(current => !current)}
1117
+ variant="quiet"
1118
+ >
1119
+ {showFullNote ? 'Show less' : 'Read full note'}
1120
+ </FeatureAction>
1121
+ )}
1122
+ {!note && (
1123
+ <FeatureAction
1124
+ component="bookmark"
1125
+ disabled={!store.ready || store.saving}
1126
+ icon="plus"
1127
+ onClick={editBookmark}
1128
+ variant="quiet"
1129
+ >
1130
+ Add a note
1131
+ </FeatureAction>
1132
+ )}
1133
+ </Flex>
1134
+ {store.value.url.value && (
1135
+ <Flex {...stylex.props(bookmarkStyles.address)}>
1136
+ <NativeButton
1137
+ onClick={() => setShowAddress(current => !current)}
1138
+ style={[button.quiet, bookmarkStyles.addressButton]}
1139
+ >
1140
+ <Flex {...stylex.props(bookmarkStyles.addressRow)}>
1141
+ <Text {...stylex.props(roles.strong, bookmarkStyles.caption)}>
1142
+ {showAddress ? 'Hide web address' : 'Show web address'}
1143
+ </Text>
1144
+ <Icon
1145
+ name={showAddress ? 'chevron.up' : 'chevron.down'}
1146
+ {...stylex.props(roles.muted, bookmarkStyles.chevron)}
1147
+ />
1148
+ </Flex>
1149
+ </NativeButton>
1150
+ {showAddress && (
1151
+ <Flex {...stylex.props(appearance.surface, bookmarkStyles.addressValue)}>
1152
+ <Text {...stylex.props(roles.muted, bookmarkStyles.caption)}>
1153
+ {store.value.url.value}
1154
+ </Text>
1155
+ </Flex>
1156
+ )}
1157
+ </Flex>
1158
+ )}
1159
+ {store.value.url.value && (
1160
+ <NativeButton
1161
+ accessibilityLabel={`Open ${title}`}
1162
+ disabled={!store.ready || store.saving}
1163
+ href={store.value.url.value}
1164
+ style={[button.primary, button.lg, layout.action]}
1165
+ >
1166
+ <Flex {...stylex.props(layout.actionContent)}>
1167
+ <Icon name="arrow.up.right" />
1168
+ <Text>Open link</Text>
1169
+ </Flex>
1170
+ </NativeButton>
1171
+ )}
1172
+ </Flex>
1173
+ )}
1174
+ </FeatureFrame>
1175
+ )
1176
+ }
1177
+
1178
+ export function Note(input: KowalskiInput<'note'>) {
1179
+ const props = kowalskiCatalog.note.props.parse(input)
1180
+ const schema = kowalskiCatalog.note.storeSchema
1181
+ const store = useConfiguredFeature({
1182
+ name: props.id ?? 'KowalskiNote',
1183
+ revision: props.revision,
1184
+ schema,
1185
+ scope: props.scope,
1186
+ seed: schema.parse({ body: { value: props.body }, title: { value: props.title } }),
1187
+ })
1188
+ const [bodyDraft, setBodyDraft] = useState<{ value: string } | null>(null)
1189
+ const [titleDraft, setTitleDraft] = useState<{ value: string } | null>(null)
1190
+ const body = bodyDraft?.value ?? store.value.body.value
1191
+ const title = titleDraft?.value ?? store.value.title.value
1192
+ const validBody = body.length <= 20_000
1193
+ const validTitle = title.length <= 160
1194
+ const text = store.ready ? body : store.value.body.value
1195
+ const wordCount = useMemo(
1196
+ () =>
1197
+ [...new Intl.Segmenter('en', { granularity: 'word' }).segment(text)].filter(
1198
+ // Older JavaScriptCore versions mark numeric words as non-word-like.
1199
+ word => word.isWordLike || /\p{N}/u.test(word.segment)
1200
+ ).length,
1201
+ [text]
1202
+ )
1203
+ const hasDraft = bodyDraft !== null || titleDraft !== null
1204
+ const status = store.ready
1205
+ ? !validBody || !validTitle
1206
+ ? 'Not saved'
1207
+ : store.saving
1208
+ ? 'Saving…'
1209
+ : hasDraft || store.error
1210
+ ? 'Not saved'
1211
+ : 'Saved'
1212
+ : store.error
1213
+ ? 'Read-only'
1214
+ : 'Loading…'
1215
+ return (
1216
+ <FeatureFrame
1217
+ onRetry={store.retry}
1218
+ notice={store.error}
1219
+ component="note"
1220
+ title={store.value.title.value || 'Untitled note'}
1221
+ header={false}
1222
+ >
1223
+ <FeatureWell>
1224
+ {store.ready ? (
1225
+ <Input
1226
+ multiline
1227
+ placeholder="Untitled note"
1228
+ value={title}
1229
+ onChange={next => {
1230
+ const draft = { value: next }
1231
+ setTitleDraft(draft)
1232
+ if (next.length <= 160) {
1233
+ store.set(
1234
+ () => ({ title: draft }),
1235
+ () => setTitleDraft(current => (current === draft ? null : current))
1236
+ )
1237
+ }
1238
+ }}
1239
+ style={[roles.h2, noteStyles.page]}
1240
+ />
1241
+ ) : (
1242
+ <Text {...stylex.props(roles.h2)}>
1243
+ {store.value.title.value || 'Untitled note'}
1244
+ </Text>
1245
+ )}
1246
+ {store.ready ? (
1247
+ <Input
1248
+ multiline
1249
+ placeholder="Start writing…"
1250
+ value={body}
1251
+ onChange={next => {
1252
+ const draft = { value: next }
1253
+ setBodyDraft(draft)
1254
+ if (next.length <= 20_000) {
1255
+ store.set(
1256
+ () => ({ body: draft }),
1257
+ () => setBodyDraft(current => (current === draft ? null : current))
1258
+ )
1259
+ }
1260
+ }}
1261
+ style={[roles.text, noteStyles.page, noteStyles.body]}
1262
+ />
1263
+ ) : (
1264
+ <Text {...stylex.props(roles.text, noteStyles.body)}>
1265
+ {text || (store.error ? 'This note is empty.' : 'Loading note…')}
1266
+ </Text>
1267
+ )}
1268
+ </FeatureWell>
1269
+ {!validTitle && (
1270
+ <Text {...stylex.props(roles.muted, layout.notice)}>
1271
+ Title is too long. Use 160 characters or fewer.
1272
+ </Text>
1273
+ )}
1274
+ <Flex {...stylex.props(noteStyles.writing)}>
1275
+ {!validBody && (
1276
+ <Text {...stylex.props(roles.muted, layout.notice)}>
1277
+ Note is too long. Use 20,000 characters or fewer to save.
1278
+ </Text>
1279
+ )}
1280
+ <Flex {...stylex.props(noteStyles.footer)}>
1281
+ <Text {...stylex.props(roles.muted)}>
1282
+ {format.formatMessage(
1283
+ {
1284
+ defaultMessage: '{count, plural, one {# word} other {# words}}',
1285
+ id: 'kowalski.note.words',
1286
+ },
1287
+ { count: wordCount }
1288
+ )}
1289
+ </Text>
1290
+ <Text {...stylex.props(roles.muted)}>{status}</Text>
1291
+ </Flex>
1292
+ {store.ready && !store.saving && hasDraft && validBody && validTitle && (
1293
+ <FeatureAction
1294
+ component="note"
1295
+ onClick={() => {
1296
+ store.set(
1297
+ () => ({
1298
+ ...(bodyDraft && { body: bodyDraft }),
1299
+ ...(titleDraft && { title: titleDraft }),
1300
+ }),
1301
+ () => {
1302
+ setBodyDraft(current => (current === bodyDraft ? null : current))
1303
+ setTitleDraft(current => (current === titleDraft ? null : current))
1304
+ }
1305
+ )
1306
+ }}
1307
+ >
1308
+ Retry save
1309
+ </FeatureAction>
1310
+ )}
1311
+ </Flex>
1312
+ </FeatureFrame>
1313
+ )
1314
+ }
1315
+
1316
+ const eventStyles = stylex.create({
1317
+ editor: { gap: space[5] },
1318
+ fieldLabel: {
1319
+ alignItems: 'center',
1320
+ flexDirection: 'row',
1321
+ justifyContent: 'space-between',
1322
+ },
1323
+ optional: { fontSize: text['xs'] },
1324
+ })
1325
+
1326
+ const contactStyles = stylex.create({
1327
+ editor: { gap: space[5] },
1328
+ fieldLabel: {
1329
+ alignItems: 'center',
1330
+ flexDirection: 'row',
1331
+ justifyContent: 'space-between',
1332
+ },
1333
+ optional: { fontSize: text['xs'] },
1334
+ })
1335
+
1336
+ const reminderStyles = stylex.create({
1337
+ editor: { gap: space[5] },
1338
+ })
1339
+
1340
+ const tripStyles = stylex.create({
1341
+ editor: { gap: space[5] },
1342
+ })
1343
+
1344
+ const bookmarkStyles = stylex.create({
1345
+ address: { gap: space[1], minWidth: size[0.5] },
1346
+ addressButton: { padding: `${space[2]} 0`, textAlign: 'left' },
1347
+ addressRow: {
1348
+ alignItems: 'center',
1349
+ flexDirection: 'row',
1350
+ gap: space[3],
1351
+ justifyContent: 'space-between',
1352
+ },
1353
+ addressValue: { minWidth: size[0.5], padding: `${space[3]} ${space[3.5]}` },
1354
+ caption: { fontSize: text['sm'] },
1355
+ chevron: { fontSize: text['xs'], fontWeight: '600' },
1356
+ details: { gap: space[4] },
1357
+ editor: { gap: space[5] },
1358
+ fieldLabel: {
1359
+ alignItems: 'center',
1360
+ flexDirection: 'row',
1361
+ justifyContent: 'space-between',
1362
+ },
1363
+ note: { gap: space[3] },
1364
+ noteText: { lineHeight: '1.6' },
1365
+ submittedValue: { minWidth: size[0.5] },
1366
+ })
1367
+
1368
+ // The note is a page: borderless text on one surface, word count and status below it.
1369
+ const noteStyles = stylex.create({
1370
+ body: { minHeight: space[24] },
1371
+ footer: {
1372
+ alignItems: 'center',
1373
+ flexDirection: 'row',
1374
+ flexWrap: 'wrap',
1375
+ gap: space[2],
1376
+ justifyContent: 'space-between',
1377
+ },
1378
+ page: {
1379
+ backgroundColor: 'transparent',
1380
+ borderWidth: 0,
1381
+ paddingLeft: 0,
1382
+ paddingRight: 0,
1383
+ textAlign: 'left',
1384
+ width: '100%',
1385
+ },
1386
+ writing: { gap: space[2] },
1387
+ })