@heycys/gallery 0.1.2 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (292) hide show
  1. package/THIRD-PARTY-NOTICES.md +2 -2
  2. package/browser/editor/components/align.mjs +58 -0
  3. package/browser/editor/components/code/index.mjs +1 -1
  4. package/browser/editor/components/embed/page.mjs +1 -1
  5. package/browser/editor/components/file/page.mjs +4 -4
  6. package/browser/editor/components/heading/index.mjs +7 -6
  7. package/browser/editor/components/icons.mjs +3 -0
  8. package/browser/editor/components/image/index.mjs +2 -0
  9. package/browser/editor/components/image/page.mjs +172 -90
  10. package/browser/editor/components/installed/index.mjs +6 -2
  11. package/browser/editor/components/installed/page.mjs +80 -13
  12. package/browser/editor/components/media/icons.mjs +13 -8
  13. package/browser/editor/components/media/page.mjs +5 -3
  14. package/browser/editor/components/media/toolbar.mjs +12 -3
  15. package/browser/editor/components/paragraph/index.mjs +4 -3
  16. package/browser/editor/components/registry.mjs +17 -5
  17. package/browser/editor/components/table/index.mjs +4 -2
  18. package/browser/editor/page/after/index.mjs +288 -0
  19. package/browser/editor/page/align.mjs +91 -0
  20. package/browser/editor/page/beside.mjs +5 -6
  21. package/browser/editor/page/block-menu/index.mjs +6 -3
  22. package/browser/editor/page/blocks.mjs +3 -1
  23. package/browser/editor/page/board/hint.mjs +64 -27
  24. package/browser/editor/page/board/index.mjs +160 -71
  25. package/browser/editor/page/board/motion.mjs +177 -0
  26. package/browser/editor/page/board/settings.mjs +4 -3
  27. package/browser/editor/page/clipboard/index.mjs +10 -3
  28. package/browser/editor/page/clipboard/own/blocks.mjs +3 -4
  29. package/browser/editor/page/clipboard/own/describe.mjs +6 -2
  30. package/browser/editor/page/commit.mjs +2 -2
  31. package/browser/editor/page/core.mjs +3 -1
  32. package/browser/editor/page/custom-page.mjs +15 -11
  33. package/browser/editor/page/editor.mjs +31 -14
  34. package/browser/editor/page/history.mjs +19 -4
  35. package/browser/editor/page/insert/index.mjs +20 -22
  36. package/browser/editor/page/menu.mjs +15 -7
  37. package/browser/editor/page/page-objects.mjs +3 -7
  38. package/browser/editor/page/present-links.mjs +5 -4
  39. package/browser/editor/page/present-shell.mjs +63 -0
  40. package/browser/editor/page/selection/index.mjs +10 -2
  41. package/browser/editor/page/table/binding.mjs +87 -0
  42. package/browser/editor/page/table/icons.mjs +0 -2
  43. package/browser/editor/page/table/index.mjs +10 -14
  44. package/browser/editor/page/table/views.mjs +5 -52
  45. package/browser/editor/page/table/widths.mjs +82 -10
  46. package/browser/editor/page/typing/index.mjs +44 -12
  47. package/browser/editor/page/typing/leave.mjs +19 -5
  48. package/browser/shared/block-packs.mjs +23 -8
  49. package/browser/shared/component-catalog.mjs +15 -8
  50. package/browser/shared/component-source.mjs +3 -1
  51. package/browser/shared/components/heading.mjs +2 -1
  52. package/browser/shared/components/installed.mjs +1 -1
  53. package/browser/shared/components/page.mjs +2 -0
  54. package/browser/shared/components/paragraph.mjs +8 -1
  55. package/browser/shared/components/table.mjs +1 -0
  56. package/browser/shared/emoji/line-icons.mjs +37 -0
  57. package/browser/shared/http.mjs +9 -0
  58. package/browser/shared/i18n/en.mjs +40 -17
  59. package/browser/shared/outline.mjs +30 -2
  60. package/browser/shared/pack-manifest.mjs +12 -1
  61. package/browser/shared/pages.mjs +169 -57
  62. package/browser/shared/pinyin.mjs +92 -0
  63. package/browser/shared/present-step.mjs +17 -0
  64. package/browser/shared/sidebar-head.mjs +70 -0
  65. package/browser/shared/web-links.mjs +5 -0
  66. package/dist/chunk-2OHANJ6Q.mjs +2 -0
  67. package/dist/{chunk-QOUEFUCH.mjs → chunk-3MBRL2PM.mjs} +1 -1
  68. package/dist/{chunk-NWR2LT5B.mjs → chunk-4OKWZY5E.mjs} +1 -1
  69. package/dist/{chunk-RMECZA2K.mjs → chunk-5K44HLTZ.mjs} +1 -1
  70. package/dist/{chunk-NFMCP2HI.mjs → chunk-5XA2ZBAE.mjs} +2 -2
  71. package/dist/{chunk-OUUP3WKT.mjs → chunk-67N2QJYT.mjs} +1 -1
  72. package/dist/chunk-6KBYRA2C.mjs +2 -0
  73. package/dist/{chunk-SX4LJYT6.mjs → chunk-6N3PX4XX.mjs} +1 -1
  74. package/dist/{chunk-RFDZXFJF.mjs → chunk-7JDX4E65.mjs} +1 -1
  75. package/dist/chunk-7M2BQQGZ.mjs +2 -0
  76. package/dist/{chunk-ZUNVAXLH.mjs → chunk-7OG26PGF.mjs} +1 -1
  77. package/dist/{chunk-B4P2CM2J.mjs → chunk-AHN3NFKJ.mjs} +1 -1
  78. package/dist/chunk-ALZ6ZENZ.mjs +3 -0
  79. package/dist/{chunk-UGWYKHBL.mjs → chunk-AR5D57ZD.mjs} +2 -2
  80. package/dist/{chunk-W3BGBJQW.mjs → chunk-BFJGHSQN.mjs} +1 -1
  81. package/dist/{chunk-HLYV7RIH.mjs → chunk-CMA72MAG.mjs} +1 -1
  82. package/dist/{chunk-D4CKRD3V.mjs → chunk-DAZL73VK.mjs} +2 -2
  83. package/dist/{chunk-6VTU6KEO.mjs → chunk-DJISHPUL.mjs} +1 -1
  84. package/dist/{chunk-BPL64HGA.mjs → chunk-EAIFITAV.mjs} +1 -1
  85. package/dist/chunk-EP25HDAJ.mjs +2 -0
  86. package/dist/{chunk-6EZ7FNIK.mjs → chunk-F4ZA6JRZ.mjs} +1 -1
  87. package/dist/{chunk-3KJHLHUO.mjs → chunk-F6HVDZR4.mjs} +1 -1
  88. package/dist/{chunk-3NJACR4W.mjs → chunk-FDMAXMQA.mjs} +1 -1
  89. package/dist/chunk-FETSREUN.mjs +5 -0
  90. package/dist/{chunk-RKPWLGZ3.mjs → chunk-GHOYKMQ3.mjs} +1 -1
  91. package/dist/chunk-HDWTZ763.mjs +12 -0
  92. package/dist/{chunk-VTKNEYT3.mjs → chunk-HPOI4ZX7.mjs} +1 -1
  93. package/dist/{chunk-OW5NWPOJ.mjs → chunk-HXG7EOPJ.mjs} +1 -1
  94. package/dist/{chunk-V6RIYQZZ.mjs → chunk-ITOH5EGO.mjs} +1 -1
  95. package/dist/{chunk-ZWPMCK7P.mjs → chunk-IV64FFI4.mjs} +1 -1
  96. package/dist/chunk-J43Z6LSS.mjs +2 -0
  97. package/dist/chunk-J5PUIV3Y.mjs +4 -0
  98. package/dist/{chunk-KTKHRC6H.mjs → chunk-JA7G53PC.mjs} +1 -1
  99. package/dist/{chunk-NWFBE2YA.mjs → chunk-JLTZF4LA.mjs} +1 -1
  100. package/dist/{chunk-GY3FAUVM.mjs → chunk-JZT7I2OJ.mjs} +1 -1
  101. package/dist/{chunk-QKXMPUXC.mjs → chunk-K5XATEOO.mjs} +1 -1
  102. package/dist/chunk-L2SUIS5S.mjs +2 -0
  103. package/dist/{chunk-TEZAWDSQ.mjs → chunk-LM4XFQHZ.mjs} +1 -1
  104. package/dist/{chunk-5ZJA7CXT.mjs → chunk-MN7BZL4R.mjs} +1 -1
  105. package/dist/chunk-MTYYHM2G.mjs +15 -0
  106. package/dist/{chunk-HMYTM3GT.mjs → chunk-MXUBKDRO.mjs} +1 -1
  107. package/dist/{chunk-MHW7ZKOU.mjs → chunk-OEXWRJME.mjs} +1 -1
  108. package/dist/{chunk-CRJEG7MT.mjs → chunk-OFHXGWOX.mjs} +1 -1
  109. package/dist/chunk-ORID3AE7.mjs +166 -0
  110. package/dist/{chunk-32ZUEFZ7.mjs → chunk-PGDE24N5.mjs} +1 -1
  111. package/dist/{chunk-UNOYVIYR.mjs → chunk-POTA2FV4.mjs} +1 -1
  112. package/dist/{chunk-N7AZZLGT.mjs → chunk-QE5RG76S.mjs} +18 -17
  113. package/dist/{chunk-N76HWBA3.mjs → chunk-R2ZDLJYT.mjs} +1 -1
  114. package/dist/chunk-SWUTTZCN.mjs +2 -0
  115. package/dist/{chunk-POQSE4C2.mjs → chunk-TR7TWGHB.mjs} +1 -1
  116. package/dist/chunk-U5XL7D6A.mjs +3 -0
  117. package/dist/{chunk-PVER7LSC.mjs → chunk-U7NZI6N4.mjs} +2 -2
  118. package/dist/{chunk-Q47PLKIJ.mjs → chunk-UGWMMEJ5.mjs} +1 -1
  119. package/dist/chunk-UJQJKNGK.mjs +2 -0
  120. package/dist/chunk-UYVCQK3H.mjs +22 -0
  121. package/dist/{chunk-FJYN7RTH.mjs → chunk-VFX2KNHP.mjs} +1 -1
  122. package/dist/{chunk-UBBRTJ2R.mjs → chunk-VS7JIGJW.mjs} +1 -1
  123. package/dist/{chunk-NVRNLK25.mjs → chunk-W7665ZEC.mjs} +2 -2
  124. package/dist/{chunk-NG375ILQ.mjs → chunk-WQDQWCX5.mjs} +1 -1
  125. package/dist/chunk-YLPSMDIS.mjs +5 -0
  126. package/dist/{chunk-VXACTLO3.mjs → chunk-YNAWBAYN.mjs} +1 -1
  127. package/dist/{chunk-ZB4H7ZN4.mjs → chunk-YROX2O22.mjs} +1 -1
  128. package/dist/{chunk-YQGT4AB2.mjs → chunk-ZDLPUOAG.mjs} +1 -1
  129. package/dist/chunk-ZQS6H473.mjs +16 -0
  130. package/dist/{chunk-OJKMINIU.mjs → chunk-ZWWC4KLF.mjs} +15 -15
  131. package/dist/cli.mjs +1 -1
  132. package/dist/server.mjs +1 -1
  133. package/dist/worker.mjs +1 -1
  134. package/kit/board.css +5 -5
  135. package/kit/code.js +15 -15
  136. package/kit/report.css +120 -41
  137. package/kit/runtime.js +45 -7
  138. package/kit/tables.js +2 -0
  139. package/kit/ui.js +2 -0
  140. package/package.json +1 -1
  141. package/packs/gallery/chart-view/kit.js +3 -3
  142. package/packs/gallery/chart-view/writing.md +1 -1
  143. package/packs/gallery/countdown/component.json +1 -1
  144. package/packs/gallery/countdown/kit.js +89 -29
  145. package/packs/gallery/countdown/preview.svg +1 -1
  146. package/packs/gallery/countdown/style.css +88 -100
  147. package/packs/gallery/countdown/writing.md +2 -2
  148. package/packs/gallery/habit/component.json +3 -3
  149. package/packs/gallery/habit/kit.js +114 -75
  150. package/packs/gallery/habit/preview.svg +1 -1
  151. package/packs/gallery/habit/style.css +133 -205
  152. package/packs/gallery/habit/writing.md +2 -2
  153. package/packs/gallery/heatmap/component.json +2 -2
  154. package/packs/gallery/heatmap/kit.js +335 -173
  155. package/packs/gallery/heatmap/preview.svg +1 -1
  156. package/packs/gallery/heatmap/style.css +158 -170
  157. package/packs/gallery/heatmap/writing.md +2 -2
  158. package/packs/gallery/kanban/kit.js +3 -3
  159. package/packs/gallery/kanban/writing.md +1 -1
  160. package/packs/gallery/metric/kit.js +3 -3
  161. package/packs/gallery/metric/writing.md +1 -1
  162. package/packs/gallery/note/component.json +3 -3
  163. package/packs/gallery/note/kit.js +40 -13
  164. package/packs/gallery/note/preview.svg +1 -1
  165. package/packs/gallery/note/style.css +51 -30
  166. package/packs/gallery/note/writing.md +2 -2
  167. package/packs/gallery/recent/component.json +2 -2
  168. package/packs/gallery/recent/kit.js +141 -86
  169. package/packs/gallery/recent/preview.svg +1 -1
  170. package/packs/gallery/recent/style.css +53 -126
  171. package/packs/gallery/recent/writing.md +2 -2
  172. package/packs/gallery/static/component.json +2 -2
  173. package/packs/gallery/static/preview.svg +1 -1
  174. package/packs/gallery/static/style.css +99 -66
  175. package/packs/gallery/static/writing.md +2 -2
  176. package/packs/gallery/stats/component.json +3 -3
  177. package/packs/gallery/stats/kit.js +253 -72
  178. package/packs/gallery/stats/preview.svg +1 -1
  179. package/packs/gallery/stats/style.css +64 -142
  180. package/packs/gallery/stats/writing.md +2 -2
  181. package/packs/gallery/today/component.json +2 -2
  182. package/packs/gallery/today/kit.js +62 -27
  183. package/packs/gallery/today/preview.svg +1 -1
  184. package/packs/gallery/today/style.css +101 -72
  185. package/packs/gallery/today/writing.md +2 -2
  186. package/packs/gallery/todos/component.json +1 -1
  187. package/packs/gallery/todos/kit.js +218 -71
  188. package/packs/gallery/todos/preview.svg +1 -1
  189. package/packs/gallery/todos/style.css +170 -223
  190. package/packs/gallery/todos/writing.md +1 -1
  191. package/packs/gallery/weather/component.json +2 -2
  192. package/packs/gallery/weather/kit.js +165 -90
  193. package/packs/gallery/weather/preview.svg +1 -1
  194. package/packs/gallery/weather/style.css +100 -218
  195. package/packs/gallery/weather/writing.md +1 -1
  196. package/packs/gallery-soft/countdown/component.json +18 -0
  197. package/packs/gallery-soft/countdown/example.html +3 -0
  198. package/packs/gallery-soft/countdown/kit.js +206 -0
  199. package/packs/gallery-soft/countdown/preview.svg +1 -0
  200. package/packs/gallery-soft/countdown/style.css +211 -0
  201. package/packs/gallery-soft/countdown/template.html +3 -0
  202. package/packs/gallery-soft/countdown/writing.md +24 -0
  203. package/packs/gallery-soft/habit/component.json +14 -0
  204. package/packs/gallery-soft/habit/example.html +3 -0
  205. package/packs/gallery-soft/habit/kit.js +366 -0
  206. package/packs/gallery-soft/habit/preview.svg +1 -0
  207. package/packs/gallery-soft/habit/style.css +251 -0
  208. package/packs/gallery-soft/habit/template.html +3 -0
  209. package/packs/gallery-soft/habit/writing.md +23 -0
  210. package/packs/gallery-soft/heatmap/component.json +18 -0
  211. package/packs/gallery-soft/heatmap/example.html +3 -0
  212. package/packs/gallery-soft/heatmap/kit.js +498 -0
  213. package/packs/gallery-soft/heatmap/preview.svg +1 -0
  214. package/packs/gallery-soft/heatmap/style.css +181 -0
  215. package/packs/gallery-soft/heatmap/template.html +3 -0
  216. package/packs/gallery-soft/heatmap/writing.md +23 -0
  217. package/packs/gallery-soft/note/component.json +14 -0
  218. package/packs/gallery-soft/note/example.html +3 -0
  219. package/packs/gallery-soft/note/kit.js +162 -0
  220. package/packs/gallery-soft/note/preview.svg +1 -0
  221. package/packs/gallery-soft/note/style.css +148 -0
  222. package/packs/gallery-soft/note/template.html +3 -0
  223. package/packs/gallery-soft/note/writing.md +20 -0
  224. package/packs/gallery-soft/recent/component.json +13 -0
  225. package/packs/gallery-soft/recent/example.html +1 -0
  226. package/packs/gallery-soft/recent/kit.js +335 -0
  227. package/packs/gallery-soft/recent/preview.svg +1 -0
  228. package/packs/gallery-soft/recent/style.css +160 -0
  229. package/packs/gallery-soft/recent/template.html +1 -0
  230. package/packs/gallery-soft/recent/writing.md +17 -0
  231. package/packs/gallery-soft/static/component.json +13 -0
  232. package/packs/gallery-soft/static/example.html +12 -0
  233. package/packs/gallery-soft/static/preview.svg +1 -0
  234. package/packs/gallery-soft/static/style.css +186 -0
  235. package/packs/gallery-soft/static/template.html +1 -0
  236. package/packs/gallery-soft/static/writing.md +20 -0
  237. package/packs/gallery-soft/stats/component.json +13 -0
  238. package/packs/gallery-soft/stats/example.html +1 -0
  239. package/packs/gallery-soft/stats/kit.js +341 -0
  240. package/packs/gallery-soft/stats/preview.svg +1 -0
  241. package/packs/gallery-soft/stats/style.css +119 -0
  242. package/packs/gallery-soft/stats/template.html +1 -0
  243. package/packs/gallery-soft/stats/writing.md +13 -0
  244. package/packs/gallery-soft/today/component.json +12 -0
  245. package/packs/gallery-soft/today/example.html +1 -0
  246. package/packs/gallery-soft/today/kit.js +222 -0
  247. package/packs/gallery-soft/today/preview.svg +1 -0
  248. package/packs/gallery-soft/today/style.css +178 -0
  249. package/packs/gallery-soft/today/template.html +1 -0
  250. package/packs/gallery-soft/today/writing.md +16 -0
  251. package/packs/gallery-soft/todos/component.json +19 -0
  252. package/packs/gallery-soft/todos/example.html +3 -0
  253. package/packs/gallery-soft/todos/kit.js +527 -0
  254. package/packs/gallery-soft/todos/preview.svg +1 -0
  255. package/packs/gallery-soft/todos/style.css +212 -0
  256. package/packs/gallery-soft/todos/template.html +3 -0
  257. package/packs/gallery-soft/todos/writing.md +32 -0
  258. package/packs/gallery-soft/weather/component.json +19 -0
  259. package/packs/gallery-soft/weather/example.html +3 -0
  260. package/packs/gallery-soft/weather/kit.js +411 -0
  261. package/packs/gallery-soft/weather/preview.svg +1 -0
  262. package/packs/gallery-soft/weather/style.css +185 -0
  263. package/packs/gallery-soft/weather/template.html +3 -0
  264. package/packs/gallery-soft/weather/writing.md +28 -0
  265. package/release.json +9 -9
  266. package/skill/SKILL.md +12 -12
  267. package/skill/references/building-blocks.md +4 -4
  268. package/skill/references/pages-and-data.md +13 -9
  269. package/skill/references/writing-html.md +2 -0
  270. package/web/assets/index-BM_K-Auu.css +1 -0
  271. package/web/assets/index-DRslOBiv.js +15 -0
  272. package/web/index.html +2 -2
  273. package/dist/chunk-2NRBY6AJ.mjs +0 -2
  274. package/dist/chunk-35NQ4PLQ.mjs +0 -2
  275. package/dist/chunk-4AV456RQ.mjs +0 -2
  276. package/dist/chunk-5XYHDBJ5.mjs +0 -2
  277. package/dist/chunk-6A53MZYZ.mjs +0 -2
  278. package/dist/chunk-6EXH3ORZ.mjs +0 -166
  279. package/dist/chunk-BVEFGE4B.mjs +0 -2
  280. package/dist/chunk-D4EURAX4.mjs +0 -4
  281. package/dist/chunk-DQCE7TJY.mjs +0 -5
  282. package/dist/chunk-FWNRPMCJ.mjs +0 -12
  283. package/dist/chunk-FWW4CSAZ.mjs +0 -2
  284. package/dist/chunk-JVHHHGEA.mjs +0 -3
  285. package/dist/chunk-PZDLJJTJ.mjs +0 -3
  286. package/dist/chunk-SXHBYZST.mjs +0 -16
  287. package/dist/chunk-UFNYFFKO.mjs +0 -20
  288. package/dist/chunk-UKSHDZZ6.mjs +0 -2
  289. package/dist/chunk-VJPFVNNW.mjs +0 -15
  290. package/dist/chunk-VS3ZMVGX.mjs +0 -5
  291. package/web/assets/index-BK2e_07w.js +0 -13
  292. package/web/assets/index-CWFI83mF.css +0 -1
@@ -1,37 +1,46 @@
1
1
  // gallery/weather (天气): a city's weather now and for the days ahead, from Open-Meteo (open-meteo.com,
2
2
  // no key), for a custom page. Its settings, `city` (a name as the reader writes it: 上海, Berlin) and
3
- // `unit` (℃ or ℉), are the card's form (the grip at its top right › 设置…), kept in the data part `settings`, which the board
4
- // writes and this block only reads. The city is looked up by name (geocoding-api.open-meteo.com: its
5
- // first match, its coordinates), its weather fetched for them (api.open-meteo.com: now, and each day's
6
- // highest, lowest and sky for five days), both through context.fetch, the server fetching for it: its
7
- // needs name the two hosts, and nothing is fetched until the reader allows them (the kit then shows
8
- // 允许访问 on the card and starts the block again once the reader says yes). Fetched again as its settings
9
- // change (`source`) and every 30 minutes; the server keeps an answer a minute and gives what it kept
10
- // when the network is not there, which the card says by its time (更新于 …). Its face: the city and when,
11
- // the sky's picture, the temperature large, the sky's words and today's range, how it feels, the air's
12
- // humidity and the wind, and the days ahead, as much of it as the card's size holds (style.css,
13
- // container queries); what it says while there is nothing to show (no city yet, one not found, waiting
14
- // for the reader's consent, the network not there, this computer's network setup sending Open-Meteo's
15
- // names to an address of its own or of a private network, which the server refuses: code "private")
16
- // stands in the face's note. In the page's language. Its head is the cards' (the kit's card tokens): the
17
- // chip, a place's pin, before the city; each day ahead its lowest and highest at either end of a bar, the
18
- // bar drawn from the lowest to the highest of all the days shown, so that the days read against each other.
3
+ // `unit` (℃ or ℉), are the card's form (the grip at its top right › 设置…), kept in the data part
4
+ // `settings`, which the board writes and this block only reads. The city is looked up by name
5
+ // (geocoding-api.open-meteo.com: its first match, its coordinates), its weather fetched for them
6
+ // (api.open-meteo.com: now, and each day's highest, lowest and sky for six days, today and the five after
7
+ // it), both through context.fetch, the server fetching for it: its needs name the two hosts, and nothing
8
+ // is fetched until the reader allows them (the kit then shows 允许访问 on the card and starts the block
9
+ // again once the reader says yes). Fetched again as its settings change (`source`) and every 30 minutes;
10
+ // the server keeps an answer a minute and gives what it kept when the network is not there, which the
11
+ // card says by its time (更新于 09:05; an answer kept more than 10 minutes, when the network is not there,
12
+ // the head's sign of no network and when it was fetched, 昨天 21:40). In the page's language.
13
+ //
14
+ // Its face (the pack's design B, in the kit's card tokens), laid out by the card's cells (`data-layout`, by
15
+ // context.size, read again on `layout`): a head of the chip (a place's pin), the city and when; the sky's
16
+ // picture (drawn in its own colors: the sun's orange, a cloud's gray, the rain's blue) beside the
17
+ // temperature, the hero, and the sky's words and today's lowest and highest; under them how it feels,
18
+ // the air's humidity and the wind (its direction and speed). A narrow card (2 wide) the picture and the
19
+ // temperature at the bottom and one line under them; a tall one (4 high) the next five days at the
20
+ // bottom, each its weekday, its sky, its lowest, a bar from its lowest to its highest on the scale of all
21
+ // of them, cold to warm, and its highest. What it says while there is nothing to show (no city yet, one
22
+ // not found, waiting for the reader's consent, the network not there, this computer's network setup
23
+ // sending Open-Meteo's names to an address of its own or of a private network, which the server refuses:
24
+ // code "private") is the cards' empty face under the head: what is so, and what to do, faint.
19
25
 
20
26
  const WORDS = {
21
27
  zh: {
22
28
  title: "天气",
23
- pick: "写上一个城市",
24
- where: "右上角手柄 › 设置…",
25
- loading: "正在取天气…",
26
- unknown: (/** @type {string} */ city) => `找不到「${city}」:换一个写法试试`,
27
- consent: "允许联网之后显示天气",
28
- offline: "现在连不上网,取不到天气",
29
- private: "网络设置把天气服务指向了本地地址,取不到天气",
30
- failed: "没能取到天气",
29
+ states: {
30
+ pick: ["写上一个城市", "在卡片右上角的 设置… 里填,比如「杭州」"],
31
+ loading: ["正在取天气…", ""],
32
+ consent: ["允许联网之后显示天气", "数据来自 open-meteo.com"],
33
+ offline: ["现在连不上网,取不到天气", "连上以后会自己再取"],
34
+ private: ["取不到天气", "网络设置把天气服务指向了本地地址"],
35
+ failed: ["没能取到天气", "过一会儿会自己再取"],
36
+ },
37
+ unknown: (/** @type {string} */ city) => [`找不到「${city}」`, "换个写法试试,比如「上海」"],
31
38
  at: (/** @type {string} */ time) => `更新于 ${time}`,
39
+ yesterday: (/** @type {string} */ time) => `昨天 ${time}`,
40
+ day: (/** @type {number} */ month, /** @type {number} */ day, /** @type {string} */ time) => `${month}月${day}日 ${time}`,
32
41
  feels: (/** @type {string} */ value) => `体感 ${value}`,
33
- humidity: (/** @type {number} */ value) => `湿度 ${value}%`,
34
- wind: (/** @type {number} */ value) => `风 ${value} km/h`,
42
+ winds: ["北风", "东北风", "东风", "东南风", "南风", "西南风", "西风", "西北风"],
43
+ wind: (/** @type {string} */ from, /** @type {number} */ value) => `${from} ${value} km/h`,
35
44
  today: "今天",
36
45
  weekdays: ["周日", "周一", "周二", "周三", "周四", "周五", "周六"],
37
46
  sky: {
@@ -42,18 +51,21 @@ const WORDS = {
42
51
  },
43
52
  en: {
44
53
  title: "Weather",
45
- pick: "Name a city",
46
- where: "The grip at the top right › Settings…",
47
- loading: "Fetching the weather…",
48
- unknown: (/** @type {string} */ city) => `No place called “${city}”: try another spelling`,
49
- consent: "Shown once it may go online",
50
- offline: "Offline: no weather now",
51
- private: "The network setup sends the weather service to a local address",
52
- failed: "The weather could not be fetched",
54
+ states: {
55
+ pick: ["Name a city", "In Settings… at the card's top right, such as “Berlin”"],
56
+ loading: ["Fetching the weather…", ""],
57
+ consent: ["Allow internet access to show the weather", "From open-meteo.com"],
58
+ offline: ["Offline: no weather now", "It fetches again once online"],
59
+ private: ["No weather", "The network setup sends the weather service to a local address"],
60
+ failed: ["The weather could not be fetched", "It tries again in a while"],
61
+ },
62
+ unknown: (/** @type {string} */ city) => [`No place called “${city}”`, "Try another spelling, such as “Berlin”"],
53
63
  at: (/** @type {string} */ time) => `Updated ${time}`,
64
+ yesterday: (/** @type {string} */ time) => `Yesterday ${time}`,
65
+ day: (/** @type {number} */ month, /** @type {number} */ day, /** @type {string} */ time) => `${["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"][month - 1]} ${day} ${time}`,
54
66
  feels: (/** @type {string} */ value) => `Feels ${value}`,
55
- humidity: (/** @type {number} */ value) => `Humidity ${value}%`,
56
- wind: (/** @type {number} */ value) => `Wind ${value} km/h`,
67
+ winds: ["N", "NE", "E", "SE", "S", "SW", "W", "NW"],
68
+ wind: (/** @type {string} */ from, /** @type {number} */ value) => `${from} ${value} km/h`,
57
69
  today: "Today",
58
70
  weekdays: ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"],
59
71
  sky: {
@@ -64,6 +76,9 @@ const WORDS = {
64
76
  },
65
77
  };
66
78
 
79
+ /** How old a kept answer is before the head says it is (with no network): 10 minutes. */
80
+ const STALE = 10 * 60_000;
81
+
67
82
  /** The language the page speaks: the UI's where Gallery says it (data-ge-language), else its lang's. */
68
83
  function languageOf(/** @type {HTMLElement} */ root) {
69
84
  const ui = root.getAttribute("data-ge-language");
@@ -72,7 +87,7 @@ function languageOf(/** @type {HTMLElement} */ root) {
72
87
 
73
88
  /**
74
89
  * The sky a WMO weather code says (what Open-Meteo answers with), as { words (its key in WORDS' sky),
75
- * picture (which of PICTURES draws it) }; `day` false draws a clear or mostly clear sky with the moon.
90
+ * picture (which of the pictures draws it) }; `day` false draws a clear or mostly clear sky with the moon.
76
91
  * @param {unknown} code
77
92
  * @param {boolean} day
78
93
  */
@@ -100,13 +115,14 @@ const searchAddress = (city, language) => `https://geocoding-api.open-meteo.com/
100
115
 
101
116
  /**
102
117
  * The address of the weather at a place (its coordinates, to two decimals: one address for one place,
103
- * which the server keeps), in `unit` (℃ or ℉): now, and five days, by the place's own clock.
118
+ * which the server keeps), in `unit` (℃ or ℉): now, and six days (today and the five after it), by the
119
+ * place's own clock.
104
120
  * @param {{ latitude: number, longitude: number }} place
105
121
  * @param {string} unit
106
122
  */
107
123
  const forecastAddress = (place, unit) => `https://api.open-meteo.com/v1/forecast?latitude=${place.latitude.toFixed(2)}&longitude=${place.longitude.toFixed(2)}`
108
- + "&current=temperature_2m,apparent_temperature,relative_humidity_2m,wind_speed_10m,weather_code,is_day"
109
- + `&daily=weather_code,temperature_2m_max,temperature_2m_min&timezone=auto&forecast_days=5${unit === "℉" ? "&temperature_unit=fahrenheit" : ""}`;
124
+ + "&current=temperature_2m,apparent_temperature,relative_humidity_2m,wind_speed_10m,wind_direction_10m,weather_code,is_day"
125
+ + `&daily=weather_code,temperature_2m_max,temperature_2m_min&timezone=auto&forecast_days=6${unit === "℉" ? "&temperature_unit=fahrenheit" : ""}`;
110
126
 
111
127
  /**
112
128
  * The first place a lookup's answer gives ({ name, latitude, longitude, region }), or null for none.
@@ -122,60 +138,89 @@ function placeOf(answer) {
122
138
  const degrees = (value) => (typeof value === "number" && Number.isFinite(value) ? `${Math.round(value)}°`.replace(/^-/, "−") : "–");
123
139
 
124
140
  /**
125
- * What the card says, in `language`, of `state`: { kind: "pick" } (no city), { kind: "loading", city },
126
- * { kind: "unknown", city } (no such place), { kind: "consent", city }, { kind: "offline", city },
141
+ * What the card says, in `language`, of `state` at `now`: { kind: "pick" } (no city), { kind: "loading",
142
+ * city }, { kind: "unknown", city } (no such place), { kind: "consent", city }, { kind: "offline", city },
127
143
  * { kind: "private", city } (the server refused the address this computer's resolver gave: one of this
128
144
  * computer or of a private network, its network setup's doing), { kind: "failed", city }, or
129
- * { kind: "weather", place, forecast (Open-Meteo's answer), at (when it was fetched, ISO) }. Its head
130
- * (the city, and when), and either a note or the weather: the sky's picture and words, the temperature,
131
- * today's range, the more line's pieces, and the days ahead.
145
+ * { kind: "weather", place, forecast (Open-Meteo's answer), at (when it was fetched, ISO), cached }. Its head
146
+ * (the city, when, and whether that is an answer kept from before the network went: `stale`), and either a
147
+ * note and its hint (the empty face) or the weather: the sky's picture and words, the temperature, today's
148
+ * lowest and highest, the more line's pieces ({ icon, text }), and the five days after today.
132
149
  * @param {any} state
133
150
  * @param {"zh" | "en"} language
151
+ * @param {Date} [now]
134
152
  */
135
- function faceOf(state, language) {
153
+ function faceOf(state, language, now = new Date()) {
136
154
  const words = WORDS[language];
137
- const head = { city: state.city || state.place?.name || words.title, at: "" };
155
+ const named = state.kind === "pick" || state.kind === "unknown";
156
+ const head = { city: named ? words.title : state.city || state.place?.name || words.title, at: "", stale: false };
138
157
  if (state.kind !== "weather") {
139
- /** @type {Record<string, string>} */
140
- const notes = { pick: words.pick, loading: words.loading, consent: words.consent, offline: words.offline, private: words.private, failed: words.failed };
141
- const note = state.kind === "unknown" ? words.unknown(state.city) : notes[state.kind] ?? words.failed;
142
- return { head, note, hint: state.kind === "pick" || state.kind === "unknown" ? words.where : "", now: null, days: [] };
158
+ /** @type {Record<string, string[]>} */
159
+ const states = words.states;
160
+ const [note, hint] = state.kind === "unknown" ? words.unknown(state.city) : states[state.kind] ?? states.failed;
161
+ return { head, note, hint, now: null, days: [] };
143
162
  }
144
163
  const { current = {}, daily = {} } = state.forecast ?? {};
145
164
  const sky = skyOf(current.weather_code, current.is_day !== 0);
146
165
  const fetched = typeof state.at === "string" ? new Date(state.at) : null;
147
166
  const two = (/** @type {number} */ value) => String(value).padStart(2, "0");
148
- head.at = fetched && !Number.isNaN(fetched.getTime()) ? words.at(`${two(fetched.getHours())}:${two(fetched.getMinutes())}`) : "";
149
- const range = (/** @type {number} */ index) => `${degrees(daily.temperature_2m_max?.[index])} / ${degrees(daily.temperature_2m_min?.[index])}`;
167
+ if (fetched && !Number.isNaN(fetched.getTime())) {
168
+ const time = `${two(fetched.getHours())}:${two(fetched.getMinutes())}`;
169
+ head.stale = state.cached === true && now.getTime() - fetched.getTime() > STALE;
170
+ const day = (/** @type {Date} */ date) => Date.UTC(date.getFullYear(), date.getMonth(), date.getDate()) / 86_400_000;
171
+ const ago = day(now) - day(fetched);
172
+ head.at = !head.stale ? words.at(time) : ago === 0 ? time : ago === 1 ? words.yesterday(time) : words.day(fetched.getMonth() + 1, fetched.getDate(), time);
173
+ }
174
+ const range = (/** @type {number} */ index) => `${degrees(daily.temperature_2m_min?.[index])} / ${degrees(daily.temperature_2m_max?.[index])}`;
150
175
  const more = [
151
- typeof current.apparent_temperature === "number" ? words.feels(degrees(current.apparent_temperature)) : null,
152
- typeof current.relative_humidity_2m === "number" ? words.humidity(Math.round(current.relative_humidity_2m)) : null,
153
- typeof current.wind_speed_10m === "number" ? words.wind(Math.round(current.wind_speed_10m)) : null,
176
+ typeof current.apparent_temperature === "number" ? { icon: "feels", text: words.feels(degrees(current.apparent_temperature)) } : null,
177
+ typeof current.relative_humidity_2m === "number" ? { icon: "humidity", text: `${Math.round(current.relative_humidity_2m)}%` } : null,
178
+ typeof current.wind_speed_10m === "number" ? { icon: "wind", text: words.wind(typeof current.wind_direction_10m === "number" ? words.winds[Math.round(current.wind_direction_10m / 45) % 8] : "", Math.round(current.wind_speed_10m)).trim() } : null,
154
179
  ].filter((piece) => piece !== null);
155
180
  const dates = Array.isArray(daily.time) ? daily.time : [];
156
- const days = dates.slice(0, 5).map((/** @type {string} */ date, /** @type {number} */ index) => {
181
+ const days = dates.slice(1, 6).map((/** @type {string} */ date, /** @type {number} */ at) => {
182
+ const index = at + 1;
157
183
  const [year, month, day] = String(date).split("-").map(Number);
158
- const weekday = index === 0 ? words.today : words.weekdays[new Date(Date.UTC(year, month - 1, day)).getUTCDay()] ?? "";
184
+ const weekday = words.weekdays[new Date(Date.UTC(year, month - 1, day)).getUTCDay()] ?? "";
159
185
  const [high, low] = [daily.temperature_2m_max?.[index], daily.temperature_2m_min?.[index]];
160
- return { weekday, picture: skyOf(daily.weather_code?.[index]).picture, sky: words.sky[skyOf(daily.weather_code?.[index]).words], range: range(index), high: degrees(high), low: degrees(low), span: typeof high === "number" && typeof low === "number" && Number.isFinite(high) && Number.isFinite(low) ? [low, high] : null };
186
+ const kind = skyOf(daily.weather_code?.[index]);
187
+ return { weekday, picture: kind.picture, sky: words.sky[kind.words], range: range(index), high: degrees(high), low: degrees(low), span: typeof high === "number" && typeof low === "number" && Number.isFinite(high) && Number.isFinite(low) ? [low, high] : null };
161
188
  });
162
- return { head, note: "", hint: "", now: { picture: sky.picture, sky: words.sky[sky.words], temperature: degrees(current.temperature_2m), range: dates.length ? range(0) : "", more }, days };
189
+ const span = typeof daily.temperature_2m_max?.[0] === "number" && typeof daily.temperature_2m_min?.[0] === "number" ? [daily.temperature_2m_min[0], daily.temperature_2m_max[0]] : null;
190
+ return { head, note: "", hint: "", now: { picture: sky.picture, sky: words.sky[sky.words], temperature: degrees(current.temperature_2m), range: dates.length ? range(0) : "", span, more }, days };
163
191
  }
164
192
 
165
- // The sky's pictures, a 24 x 24 drawing each in the text's color (style.css colors the sun and the rain).
166
- const SUN = '<circle cx="12" cy="12" r="4.2" class="gallery-weather-sun"/><g class="gallery-weather-sun" stroke-linecap="round" stroke-width="1.8" fill="none"><path d="M12 2.5v2.2M12 19.3v2.2M2.5 12h2.2M19.3 12h2.2M5.3 5.3l1.6 1.6M17.1 17.1l1.6 1.6M5.3 18.7l1.6-1.6M17.1 6.9l1.6-1.6"/></g>';
167
- const CLOUD = '<path class="gallery-weather-cloud" d="M7.5 19h9.3a4.2 4.2 0 0 0 .5-8.4 5.6 5.6 0 0 0-10.7 1.6A3.4 3.4 0 0 0 7.5 19z"/>';
193
+ // The sky's pictures, each drawn on a box of 64 in its own colors (style.css: the sun's orange, a cloud's
194
+ // gray, the rain's blue, as the instance's custom properties say them in light and dark).
195
+ const cloud = (x = 0, y = 0, scale = 1, tone = "cloud") => `<g transform="translate(${x} ${y}) scale(${scale})" class="gallery-weather-${tone}"><rect x="13" y="34" width="39" height="15" rx="7.5"/><circle cx="24" cy="35" r="8.5"/><circle cx="35.5" cy="30" r="11.5"/><circle cx="45.5" cy="38" r="7.5"/></g>`;
196
+ function sunAt(cx, cy, r) {
197
+ let rays = "";
198
+ for (let i = 0; i < 8; i++) {
199
+ const a = (i * Math.PI) / 4;
200
+ rays += `<line x1="${(cx + (r + 5) * Math.cos(a)).toFixed(1)}" y1="${(cy + (r + 5) * Math.sin(a)).toFixed(1)}" x2="${(cx + (r + 9.5) * Math.cos(a)).toFixed(1)}" y2="${(cy + (r + 9.5) * Math.sin(a)).toFixed(1)}"/>`;
201
+ }
202
+ return `<g class="gallery-weather-sun"><circle cx="${cx}" cy="${cy}" r="${r}"/><g class="gallery-weather-rays" stroke-width="3" stroke-linecap="round">${rays}</g></g>`;
203
+ }
204
+ const moonAt = (cx, cy, r) => `<path class="gallery-weather-moon" d="M${cx + r * 0.35} ${cy - r}a${r} ${r} 0 1 0 ${r * 0.65} ${r * 1.55}a${r * 0.8} ${r * 0.8} 0 0 1 -${r * 0.65} -${r * 1.55}z"/>`;
168
205
  const PICTURES = {
169
- sun: SUN,
170
- moon: '<path class="gallery-weather-moon" d="M15.6 3.4a8.6 8.6 0 1 0 5 15.2A7.2 7.2 0 0 1 15.6 3.4z"/>',
171
- partly: '<g transform="translate(-3 -4) scale(.8)">' + SUN + "</g>" + CLOUD,
172
- "partly-night": '<path class="gallery-weather-moon" d="M9.6 2.6a6.2 6.2 0 1 0 6.2 8.6A5.2 5.2 0 0 1 9.6 2.6z"/>' + CLOUD,
173
- cloud: CLOUD,
174
- fog: '<g class="gallery-weather-cloud-line" stroke-linecap="round" stroke-width="1.9" fill="none"><path d="M4 8.5h16M6 12.5h12M4 16.5h16"/></g>',
175
- rain: '<g transform="translate(0 -3)">' + CLOUD + '</g><g class="gallery-weather-rain" stroke-linecap="round" stroke-width="1.8" fill="none"><path d="M9 18.5l-1 3M13 18.5l-1 3M17 18.5l-1 3"/></g>',
176
- snow: '<g transform="translate(0 -3)">' + CLOUD + '</g><g class="gallery-weather-snow"><circle cx="8.5" cy="20" r="1.1"/><circle cx="12.5" cy="21.5" r="1.1"/><circle cx="16.5" cy="20" r="1.1"/></g>',
177
- thunder: '<g transform="translate(0 -3)">' + CLOUD + '</g><path class="gallery-weather-bolt" d="M12.5 14.5l-2.6 4.2h2.4l-1.2 4 3.8-5.4h-2.4l1.4-2.8z"/>',
206
+ sun: sunAt(32, 32, 11),
207
+ moon: moonAt(30, 32, 14),
208
+ partly: sunAt(24, 23, 9) + cloud(5, 5, 0.95),
209
+ "partly-night": moonAt(22, 22, 11) + cloud(5, 5, 0.95),
210
+ cloud: cloud(-6, -8, 0.9, "cloud-2") + cloud(4, 2, 1),
211
+ fog: `${cloud(0, -8, 1)}<g class="gallery-weather-mist" stroke-width="3.2" stroke-linecap="round"><line x1="14" y1="49" x2="50" y2="49"/><line x1="20" y1="56" x2="44" y2="56"/></g>`,
212
+ rain: `${cloud(0, -6, 1)}<g class="gallery-weather-rain" stroke-width="3.2" stroke-linecap="round"><line x1="24" y1="49" x2="21.5" y2="55"/><line x1="33.5" y1="49" x2="31" y2="55"/><line x1="43" y1="49" x2="40.5" y2="55"/></g>`,
213
+ snow: `${cloud(0, -6, 1)}<g class="gallery-weather-snow"><circle cx="23" cy="52" r="2.4"/><circle cx="32.5" cy="56" r="2.4"/><circle cx="42" cy="52" r="2.4"/></g>`,
214
+ thunder: `${cloud(0, -8, 1)}<path class="gallery-weather-bolt" d="M34 40l-7 10h6l-3 9 9-12h-6l3-7z"/>`,
215
+ };
216
+ // The more line's small pictures, of lines, 24 x 24.
217
+ const ICONS = {
218
+ feels: '<path d="M10 13.8V5.5a2 2 0 1 1 4 0v8.3a3.8 3.8 0 1 1-4 0z"/>',
219
+ humidity: '<path d="M12 4s5.5 6 5.5 10a5.5 5.5 0 0 1-11 0C6.5 10 12 4 12 4z"/>',
220
+ wind: '<path d="M3.5 9.5h10a2.8 2.8 0 1 0-2.8-2.8M3.5 14.5h14a2.8 2.8 0 1 1-2.8 2.8M3.5 12h6"/>',
221
+ offline: '<path d="M3.5 3.5l17 17M9 16.2a4.5 4.5 0 0 1 5.5-.5M5.5 12.5a9.5 9.5 0 0 1 3.6-2.3M18.6 12.6a9.5 9.5 0 0 0-3-2.1M2.5 9a14 14 0 0 1 3.5-2.3M21.5 9A14 14 0 0 0 10.6 5.3"/><circle cx="12" cy="19.5" r=".9" fill="currentColor"/>',
178
222
  };
223
+ const lines = (/** @type {string} */ drawing) => `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">${drawing}</svg>`;
179
224
 
180
225
  /**
181
226
  * An element of the face, of `kind` (its class).
@@ -192,30 +237,43 @@ function made(tag, kind) {
192
237
  /** A sky's picture, as an svg element. @param {string} picture */
193
238
  function pictureOf(picture) {
194
239
  const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
195
- svg.setAttribute("viewBox", "0 0 24 24");
240
+ svg.setAttribute("viewBox", "0 0 64 64");
196
241
  svg.setAttribute("aria-hidden", "true");
197
242
  svg.setAttribute("class", "gallery-weather-picture");
198
- svg.innerHTML = PICTURES[/** @type {keyof typeof PICTURES} */ (picture)] ?? CLOUD;
243
+ svg.innerHTML = PICTURES[/** @type {keyof typeof PICTURES} */ (picture)] ?? PICTURES.cloud;
199
244
  return svg;
200
245
  }
201
246
 
202
247
  DocKit.component({
203
248
  id: "gallery/weather",
204
- on: ["source"],
249
+ on: ["source", "layout"],
205
250
  mount(host, context) {
206
251
  const language = languageOf(document.documentElement);
207
252
  const face = made("div", "face");
208
253
  host.append(face);
254
+ const layout = () => {
255
+ const size = context.size;
256
+ face.dataset.layout = !size ? "wide" : size.w <= 2 ? "narrow" : size.h >= 4 ? "tall" : "wide";
257
+ };
209
258
  const draw = (/** @type {any} */ state) => {
210
259
  const said = faceOf(state, language);
211
260
  const head = made("p", "head");
212
261
  const chip = made("span", "chip");
213
262
  chip.setAttribute("aria-hidden", "true");
214
- chip.innerHTML = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><path d="M9 11a3 3 0 1 0 6 0 3 3 0 0 0-6 0"/><path d="M17.7 16.7l-4.3 4.2a2 2 0 0 1-2.8 0l-4.3-4.2a8 8 0 1 1 11.4 0z"/></svg>';
263
+ chip.innerHTML = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><path d="M12 21s-6.5-5.6-6.5-11a6.5 6.5 0 0 1 13 0c0 5.4-6.5 11-6.5 11z"/><circle cx="12" cy="10" r="2.3"/></svg>';
215
264
  const city = made("span", "city");
216
265
  city.textContent = said.head.city;
217
266
  const at = made("span", "at");
218
- at.textContent = said.head.at;
267
+ if (said.head.stale) {
268
+ const sign = made("span", "stale");
269
+ sign.innerHTML = lines(ICONS.offline);
270
+ sign.title = said.head.at;
271
+ at.append(sign);
272
+ }
273
+ // The time in words of its own: a narrow card shows the sign of an answer kept from before alone (style.css).
274
+ const when = made("span", "when");
275
+ when.textContent = said.head.at;
276
+ at.append(when);
219
277
  head.append(chip, city, at);
220
278
  /** @type {HTMLElement[]} */
221
279
  const parts = [head];
@@ -231,52 +289,68 @@ DocKit.component({
231
289
  words.append(sky, range);
232
290
  now.append(pictureOf(said.now.picture), temperature, words);
233
291
  const more = made("p", "more");
234
- more.replaceChildren(...said.now.more.map((piece) => {
292
+ more.replaceChildren(...said.now.more.map(({ icon, text }) => {
235
293
  const span = made("span", "piece");
236
- span.textContent = piece;
294
+ span.innerHTML = lines(ICONS[/** @type {keyof typeof ICONS} */ (icon)]);
295
+ span.append(text);
237
296
  return span;
238
297
  }));
239
298
  const days = made("ol", "days");
240
- const spans = said.days.flatMap((day) => day.span ?? []);
241
- const [lowest, highest] = [Math.min(...spans), Math.max(...spans)];
299
+ const spans = [...said.days.flatMap((day) => (day.span ? [day.span] : [])), ...(said.now.span ? [said.now.span] : [])];
300
+ const lowest = Math.min(...spans.map(([low]) => low));
301
+ const highest = Math.max(...spans.map(([, high]) => high));
242
302
  days.replaceChildren(...said.days.map((day) => {
243
303
  const item = made("li", "day");
244
304
  const weekday = made("span", "weekday");
245
305
  weekday.textContent = day.weekday;
246
- const range = made("span", "day-range");
247
306
  const low = made("span", "low");
248
307
  low.textContent = day.low;
249
308
  const bar = made("span", "bar");
250
309
  bar.setAttribute("aria-hidden", "true");
251
310
  if (day.span && highest > lowest) {
311
+ // The day's part of the whole scale, its colors the scale's own there: the gradient as wide as the scale, moved under it.
312
+ const from = (day.span[0] - lowest) / (highest - lowest);
313
+ const to = (day.span[1] - lowest) / (highest - lowest);
252
314
  const fill = made("span", "fill");
253
- fill.style.setProperty("--from", String((day.span[0] - lowest) / (highest - lowest)));
254
- fill.style.setProperty("--to", String((day.span[1] - lowest) / (highest - lowest)));
315
+ fill.style.left = `${from * 100}%`;
316
+ fill.style.right = `${(1 - to) * 100}%`;
317
+ const part = Math.max(to - from, 0.01);
318
+ fill.style.backgroundSize = `${100 / part}% 100%`;
319
+ fill.style.backgroundPosition = `${1 - part > 0 ? (from / (1 - part)) * 100 : 0}% 0`;
255
320
  bar.append(fill);
256
321
  }
257
322
  const high = made("span", "high");
258
323
  high.textContent = day.high;
259
- range.append(low, bar, high);
260
324
  const picture = pictureOf(day.picture);
261
325
  picture.setAttribute("aria-label", day.sky);
262
326
  picture.removeAttribute("aria-hidden");
263
327
  picture.setAttribute("role", "img");
264
- item.append(weekday, picture, range);
328
+ item.append(weekday, picture, low, bar, high);
265
329
  return item;
266
330
  }));
267
- parts.push(now, more, days);
331
+ // A narrow card's one line under the temperature: the sky, then today's lowest and highest.
332
+ const line = made("p", "line");
333
+ const lineSky = document.createElement("b");
334
+ lineSky.textContent = said.now.sky;
335
+ const dot = made("span", "dot");
336
+ dot.textContent = "·";
337
+ line.append(lineSky, dot, said.now.range);
338
+ parts.push(now, line, more, days);
268
339
  } else {
340
+ const empty = made("div", "empty");
269
341
  const note = made("p", "note");
270
342
  note.textContent = said.note;
271
- parts.push(note);
343
+ empty.append(note);
272
344
  if (said.hint) {
273
345
  const hint = made("p", "hint");
274
346
  hint.textContent = said.hint;
275
- parts.push(hint);
347
+ empty.append(hint);
276
348
  }
349
+ parts.push(empty);
277
350
  }
278
351
  face.replaceChildren(...parts);
279
352
  face.dataset.kind = state.kind;
353
+ layout();
280
354
  };
281
355
  let asked = 0; // the latest load: an earlier one that answers later draws nothing
282
356
  const load = async () => {
@@ -293,7 +367,7 @@ DocKit.component({
293
367
  if (!found) return draw({ kind: "unknown", city });
294
368
  const answer = await context.fetch(forecastAddress(found, unit));
295
369
  if (mine !== asked) return;
296
- draw({ kind: "weather", city, place: found, forecast: answer.data, at: answer.at });
370
+ draw({ kind: "weather", city, place: found, forecast: answer.data, at: answer.at, cached: answer.cached });
297
371
  } catch (error) {
298
372
  if (mine !== asked) return;
299
373
  const code = /** @type {DocFetchError} */ (error).code;
@@ -306,6 +380,7 @@ DocKit.component({
306
380
  return {
307
381
  update(reason) {
308
382
  if (reason === "source") load();
383
+ else if (reason === "layout") layout();
309
384
  },
310
385
  };
311
386
  },
@@ -1 +1 @@
1
- <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 240 120"><rect width="240" height="120" rx="8" fill="#f3f3f1"/><rect x="16.5" y="12.5" width="207" height="95" rx="12" fill="#ffffff" stroke="#e7e6e3"/><rect x="30" y="24" width="14" height="14" rx="4.5" fill="#ebf2fa"/><g transform="translate(32 26) scale(.4167)" fill="none" stroke="#2e7cd1" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><path d="M9 11a3 3 0 1 0 6 0 3 3 0 0 0-6 0"/><path d="M17.7 16.7l-4.3 4.2a2 2 0 0 1-2.8 0l-4.3-4.2a8 8 0 1 1 11.4 0z"/></g><rect x="50" y="28" width="22" height="6" rx="3" fill="#3b3a37"/><rect x="174" y="29" width="36" height="5" rx="2.5" fill="#c4c2bd"/><circle cx="40" cy="64" r="6.5" fill="#d98a1c"/><path d="M40 52.5v2.5M28.5 64H31M31.9 55.9l1.8 1.8M48.1 55.9l-1.8 1.8" stroke="#d98a1c" stroke-width="2" stroke-linecap="round"/><path d="M40 80h14a6.5 6.5 0 0 0 .8-12.9 8.6 8.6 0 0 0-16.5 2.5A5.2 5.2 0 0 0 40 80z" fill="#a3abb8"/><text x="64" y="80" font-family="Segoe UI, Helvetica, Arial, sans-serif" font-size="30" font-weight="600" letter-spacing="-.5" fill="#3b3a37">18°</text><rect x="116" y="62" width="26" height="6" rx="3" fill="#3b3a37"/><rect x="116" y="73" width="40" height="5" rx="2.5" fill="#c4c2bd"/><rect x="30" y="93" width="34" height="5" rx="2.5" fill="#c4c2bd"/><circle cx="70" cy="95.5" r="1.3" fill="#c4c2bd"/><rect x="76" y="93" width="32" height="5" rx="2.5" fill="#c4c2bd"/><circle cx="114" cy="95.5" r="1.3" fill="#c4c2bd"/><rect x="120" y="93" width="40" height="5" rx="2.5" fill="#c4c2bd"/></svg>
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 240 120"><rect width="240" height="120" rx="8" fill="#f3f3f1"/><rect x="16.5" y="12.5" width="207" height="95" rx="8" fill="#ffffff" stroke="#e6e6e3"/><rect x="28" y="22" width="10" height="10" rx="3" fill="#efefed"/><rect x="42" y="24.5" width="18" height="5" rx="2.5" fill="#37352f"/><rect x="64" y="24.5" width="34" height="5" rx="2.5" fill="#d4d3cf"/><g fill="#f3a935" stroke="#f3a935"><circle cx="40" cy="52" r="6"/><path d="M40 41v3M40 60v3M29 52h3M48 52h3M32.5 44.5l2 2M45.5 44.5l-2 2M32.5 59.5l2-2" stroke-width="1.6" stroke-linecap="round"/></g><g fill="#dde2e9"><rect x="38" y="56" width="22" height="9" rx="4.5"/><circle cx="45" cy="57" r="5"/><circle cx="52" cy="54" r="6.5"/></g><rect x="70" y="44" width="34" height="22" rx="3" fill="#1d1d1b"/><rect x="112" y="46" width="16" height="6" rx="3" fill="#37352f"/><rect x="112" y="57" width="26" height="5" rx="2.5" fill="#a6a5a1"/><rect x="28" y="88" width="40" height="5" rx="2.5" fill="#a6a5a1"/><rect x="80" y="88" width="26" height="5" rx="2.5" fill="#a6a5a1"/><rect x="132" y="88" width="54" height="5" rx="2.5" fill="#a6a5a1"/></svg>