@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.
- package/THIRD-PARTY-NOTICES.md +2 -2
- package/browser/editor/components/align.mjs +58 -0
- package/browser/editor/components/code/index.mjs +1 -1
- package/browser/editor/components/embed/page.mjs +1 -1
- package/browser/editor/components/file/page.mjs +4 -4
- package/browser/editor/components/heading/index.mjs +7 -6
- package/browser/editor/components/icons.mjs +3 -0
- package/browser/editor/components/image/index.mjs +2 -0
- package/browser/editor/components/image/page.mjs +172 -90
- package/browser/editor/components/installed/index.mjs +6 -2
- package/browser/editor/components/installed/page.mjs +80 -13
- package/browser/editor/components/media/icons.mjs +13 -8
- package/browser/editor/components/media/page.mjs +5 -3
- package/browser/editor/components/media/toolbar.mjs +12 -3
- package/browser/editor/components/paragraph/index.mjs +4 -3
- package/browser/editor/components/registry.mjs +17 -5
- package/browser/editor/components/table/index.mjs +4 -2
- package/browser/editor/page/after/index.mjs +288 -0
- package/browser/editor/page/align.mjs +91 -0
- package/browser/editor/page/beside.mjs +5 -6
- package/browser/editor/page/block-menu/index.mjs +6 -3
- package/browser/editor/page/blocks.mjs +3 -1
- package/browser/editor/page/board/hint.mjs +64 -27
- package/browser/editor/page/board/index.mjs +160 -71
- package/browser/editor/page/board/motion.mjs +177 -0
- package/browser/editor/page/board/settings.mjs +4 -3
- package/browser/editor/page/clipboard/index.mjs +10 -3
- package/browser/editor/page/clipboard/own/blocks.mjs +3 -4
- package/browser/editor/page/clipboard/own/describe.mjs +6 -2
- package/browser/editor/page/commit.mjs +2 -2
- package/browser/editor/page/core.mjs +3 -1
- package/browser/editor/page/custom-page.mjs +15 -11
- package/browser/editor/page/editor.mjs +31 -14
- package/browser/editor/page/history.mjs +19 -4
- package/browser/editor/page/insert/index.mjs +20 -22
- package/browser/editor/page/menu.mjs +15 -7
- package/browser/editor/page/page-objects.mjs +3 -7
- package/browser/editor/page/present-links.mjs +5 -4
- package/browser/editor/page/present-shell.mjs +63 -0
- package/browser/editor/page/selection/index.mjs +10 -2
- package/browser/editor/page/table/binding.mjs +87 -0
- package/browser/editor/page/table/icons.mjs +0 -2
- package/browser/editor/page/table/index.mjs +10 -14
- package/browser/editor/page/table/views.mjs +5 -52
- package/browser/editor/page/table/widths.mjs +82 -10
- package/browser/editor/page/typing/index.mjs +44 -12
- package/browser/editor/page/typing/leave.mjs +19 -5
- package/browser/shared/block-packs.mjs +23 -8
- package/browser/shared/component-catalog.mjs +15 -8
- package/browser/shared/component-source.mjs +3 -1
- package/browser/shared/components/heading.mjs +2 -1
- package/browser/shared/components/installed.mjs +1 -1
- package/browser/shared/components/page.mjs +2 -0
- package/browser/shared/components/paragraph.mjs +8 -1
- package/browser/shared/components/table.mjs +1 -0
- package/browser/shared/emoji/line-icons.mjs +37 -0
- package/browser/shared/http.mjs +9 -0
- package/browser/shared/i18n/en.mjs +40 -17
- package/browser/shared/outline.mjs +30 -2
- package/browser/shared/pack-manifest.mjs +12 -1
- package/browser/shared/pages.mjs +169 -57
- package/browser/shared/pinyin.mjs +92 -0
- package/browser/shared/present-step.mjs +17 -0
- package/browser/shared/sidebar-head.mjs +70 -0
- package/browser/shared/web-links.mjs +5 -0
- package/dist/chunk-2OHANJ6Q.mjs +2 -0
- package/dist/{chunk-QOUEFUCH.mjs → chunk-3MBRL2PM.mjs} +1 -1
- package/dist/{chunk-NWR2LT5B.mjs → chunk-4OKWZY5E.mjs} +1 -1
- package/dist/{chunk-RMECZA2K.mjs → chunk-5K44HLTZ.mjs} +1 -1
- package/dist/{chunk-NFMCP2HI.mjs → chunk-5XA2ZBAE.mjs} +2 -2
- package/dist/{chunk-OUUP3WKT.mjs → chunk-67N2QJYT.mjs} +1 -1
- package/dist/chunk-6KBYRA2C.mjs +2 -0
- package/dist/{chunk-SX4LJYT6.mjs → chunk-6N3PX4XX.mjs} +1 -1
- package/dist/{chunk-RFDZXFJF.mjs → chunk-7JDX4E65.mjs} +1 -1
- package/dist/chunk-7M2BQQGZ.mjs +2 -0
- package/dist/{chunk-ZUNVAXLH.mjs → chunk-7OG26PGF.mjs} +1 -1
- package/dist/{chunk-B4P2CM2J.mjs → chunk-AHN3NFKJ.mjs} +1 -1
- package/dist/chunk-ALZ6ZENZ.mjs +3 -0
- package/dist/{chunk-UGWYKHBL.mjs → chunk-AR5D57ZD.mjs} +2 -2
- package/dist/{chunk-W3BGBJQW.mjs → chunk-BFJGHSQN.mjs} +1 -1
- package/dist/{chunk-HLYV7RIH.mjs → chunk-CMA72MAG.mjs} +1 -1
- package/dist/{chunk-D4CKRD3V.mjs → chunk-DAZL73VK.mjs} +2 -2
- package/dist/{chunk-6VTU6KEO.mjs → chunk-DJISHPUL.mjs} +1 -1
- package/dist/{chunk-BPL64HGA.mjs → chunk-EAIFITAV.mjs} +1 -1
- package/dist/chunk-EP25HDAJ.mjs +2 -0
- package/dist/{chunk-6EZ7FNIK.mjs → chunk-F4ZA6JRZ.mjs} +1 -1
- package/dist/{chunk-3KJHLHUO.mjs → chunk-F6HVDZR4.mjs} +1 -1
- package/dist/{chunk-3NJACR4W.mjs → chunk-FDMAXMQA.mjs} +1 -1
- package/dist/chunk-FETSREUN.mjs +5 -0
- package/dist/{chunk-RKPWLGZ3.mjs → chunk-GHOYKMQ3.mjs} +1 -1
- package/dist/chunk-HDWTZ763.mjs +12 -0
- package/dist/{chunk-VTKNEYT3.mjs → chunk-HPOI4ZX7.mjs} +1 -1
- package/dist/{chunk-OW5NWPOJ.mjs → chunk-HXG7EOPJ.mjs} +1 -1
- package/dist/{chunk-V6RIYQZZ.mjs → chunk-ITOH5EGO.mjs} +1 -1
- package/dist/{chunk-ZWPMCK7P.mjs → chunk-IV64FFI4.mjs} +1 -1
- package/dist/chunk-J43Z6LSS.mjs +2 -0
- package/dist/chunk-J5PUIV3Y.mjs +4 -0
- package/dist/{chunk-KTKHRC6H.mjs → chunk-JA7G53PC.mjs} +1 -1
- package/dist/{chunk-NWFBE2YA.mjs → chunk-JLTZF4LA.mjs} +1 -1
- package/dist/{chunk-GY3FAUVM.mjs → chunk-JZT7I2OJ.mjs} +1 -1
- package/dist/{chunk-QKXMPUXC.mjs → chunk-K5XATEOO.mjs} +1 -1
- package/dist/chunk-L2SUIS5S.mjs +2 -0
- package/dist/{chunk-TEZAWDSQ.mjs → chunk-LM4XFQHZ.mjs} +1 -1
- package/dist/{chunk-5ZJA7CXT.mjs → chunk-MN7BZL4R.mjs} +1 -1
- package/dist/chunk-MTYYHM2G.mjs +15 -0
- package/dist/{chunk-HMYTM3GT.mjs → chunk-MXUBKDRO.mjs} +1 -1
- package/dist/{chunk-MHW7ZKOU.mjs → chunk-OEXWRJME.mjs} +1 -1
- package/dist/{chunk-CRJEG7MT.mjs → chunk-OFHXGWOX.mjs} +1 -1
- package/dist/chunk-ORID3AE7.mjs +166 -0
- package/dist/{chunk-32ZUEFZ7.mjs → chunk-PGDE24N5.mjs} +1 -1
- package/dist/{chunk-UNOYVIYR.mjs → chunk-POTA2FV4.mjs} +1 -1
- package/dist/{chunk-N7AZZLGT.mjs → chunk-QE5RG76S.mjs} +18 -17
- package/dist/{chunk-N76HWBA3.mjs → chunk-R2ZDLJYT.mjs} +1 -1
- package/dist/chunk-SWUTTZCN.mjs +2 -0
- package/dist/{chunk-POQSE4C2.mjs → chunk-TR7TWGHB.mjs} +1 -1
- package/dist/chunk-U5XL7D6A.mjs +3 -0
- package/dist/{chunk-PVER7LSC.mjs → chunk-U7NZI6N4.mjs} +2 -2
- package/dist/{chunk-Q47PLKIJ.mjs → chunk-UGWMMEJ5.mjs} +1 -1
- package/dist/chunk-UJQJKNGK.mjs +2 -0
- package/dist/chunk-UYVCQK3H.mjs +22 -0
- package/dist/{chunk-FJYN7RTH.mjs → chunk-VFX2KNHP.mjs} +1 -1
- package/dist/{chunk-UBBRTJ2R.mjs → chunk-VS7JIGJW.mjs} +1 -1
- package/dist/{chunk-NVRNLK25.mjs → chunk-W7665ZEC.mjs} +2 -2
- package/dist/{chunk-NG375ILQ.mjs → chunk-WQDQWCX5.mjs} +1 -1
- package/dist/chunk-YLPSMDIS.mjs +5 -0
- package/dist/{chunk-VXACTLO3.mjs → chunk-YNAWBAYN.mjs} +1 -1
- package/dist/{chunk-ZB4H7ZN4.mjs → chunk-YROX2O22.mjs} +1 -1
- package/dist/{chunk-YQGT4AB2.mjs → chunk-ZDLPUOAG.mjs} +1 -1
- package/dist/chunk-ZQS6H473.mjs +16 -0
- package/dist/{chunk-OJKMINIU.mjs → chunk-ZWWC4KLF.mjs} +15 -15
- package/dist/cli.mjs +1 -1
- package/dist/server.mjs +1 -1
- package/dist/worker.mjs +1 -1
- package/kit/board.css +5 -5
- package/kit/code.js +15 -15
- package/kit/report.css +120 -41
- package/kit/runtime.js +45 -7
- package/kit/tables.js +2 -0
- package/kit/ui.js +2 -0
- package/package.json +1 -1
- package/packs/gallery/chart-view/kit.js +3 -3
- package/packs/gallery/chart-view/writing.md +1 -1
- package/packs/gallery/countdown/component.json +1 -1
- package/packs/gallery/countdown/kit.js +89 -29
- package/packs/gallery/countdown/preview.svg +1 -1
- package/packs/gallery/countdown/style.css +88 -100
- package/packs/gallery/countdown/writing.md +2 -2
- package/packs/gallery/habit/component.json +3 -3
- package/packs/gallery/habit/kit.js +114 -75
- package/packs/gallery/habit/preview.svg +1 -1
- package/packs/gallery/habit/style.css +133 -205
- package/packs/gallery/habit/writing.md +2 -2
- package/packs/gallery/heatmap/component.json +2 -2
- package/packs/gallery/heatmap/kit.js +335 -173
- package/packs/gallery/heatmap/preview.svg +1 -1
- package/packs/gallery/heatmap/style.css +158 -170
- package/packs/gallery/heatmap/writing.md +2 -2
- package/packs/gallery/kanban/kit.js +3 -3
- package/packs/gallery/kanban/writing.md +1 -1
- package/packs/gallery/metric/kit.js +3 -3
- package/packs/gallery/metric/writing.md +1 -1
- package/packs/gallery/note/component.json +3 -3
- package/packs/gallery/note/kit.js +40 -13
- package/packs/gallery/note/preview.svg +1 -1
- package/packs/gallery/note/style.css +51 -30
- package/packs/gallery/note/writing.md +2 -2
- package/packs/gallery/recent/component.json +2 -2
- package/packs/gallery/recent/kit.js +141 -86
- package/packs/gallery/recent/preview.svg +1 -1
- package/packs/gallery/recent/style.css +53 -126
- package/packs/gallery/recent/writing.md +2 -2
- package/packs/gallery/static/component.json +2 -2
- package/packs/gallery/static/preview.svg +1 -1
- package/packs/gallery/static/style.css +99 -66
- package/packs/gallery/static/writing.md +2 -2
- package/packs/gallery/stats/component.json +3 -3
- package/packs/gallery/stats/kit.js +253 -72
- package/packs/gallery/stats/preview.svg +1 -1
- package/packs/gallery/stats/style.css +64 -142
- package/packs/gallery/stats/writing.md +2 -2
- package/packs/gallery/today/component.json +2 -2
- package/packs/gallery/today/kit.js +62 -27
- package/packs/gallery/today/preview.svg +1 -1
- package/packs/gallery/today/style.css +101 -72
- package/packs/gallery/today/writing.md +2 -2
- package/packs/gallery/todos/component.json +1 -1
- package/packs/gallery/todos/kit.js +218 -71
- package/packs/gallery/todos/preview.svg +1 -1
- package/packs/gallery/todos/style.css +170 -223
- package/packs/gallery/todos/writing.md +1 -1
- package/packs/gallery/weather/component.json +2 -2
- package/packs/gallery/weather/kit.js +165 -90
- package/packs/gallery/weather/preview.svg +1 -1
- package/packs/gallery/weather/style.css +100 -218
- package/packs/gallery/weather/writing.md +1 -1
- package/packs/gallery-soft/countdown/component.json +18 -0
- package/packs/gallery-soft/countdown/example.html +3 -0
- package/packs/gallery-soft/countdown/kit.js +206 -0
- package/packs/gallery-soft/countdown/preview.svg +1 -0
- package/packs/gallery-soft/countdown/style.css +211 -0
- package/packs/gallery-soft/countdown/template.html +3 -0
- package/packs/gallery-soft/countdown/writing.md +24 -0
- package/packs/gallery-soft/habit/component.json +14 -0
- package/packs/gallery-soft/habit/example.html +3 -0
- package/packs/gallery-soft/habit/kit.js +366 -0
- package/packs/gallery-soft/habit/preview.svg +1 -0
- package/packs/gallery-soft/habit/style.css +251 -0
- package/packs/gallery-soft/habit/template.html +3 -0
- package/packs/gallery-soft/habit/writing.md +23 -0
- package/packs/gallery-soft/heatmap/component.json +18 -0
- package/packs/gallery-soft/heatmap/example.html +3 -0
- package/packs/gallery-soft/heatmap/kit.js +498 -0
- package/packs/gallery-soft/heatmap/preview.svg +1 -0
- package/packs/gallery-soft/heatmap/style.css +181 -0
- package/packs/gallery-soft/heatmap/template.html +3 -0
- package/packs/gallery-soft/heatmap/writing.md +23 -0
- package/packs/gallery-soft/note/component.json +14 -0
- package/packs/gallery-soft/note/example.html +3 -0
- package/packs/gallery-soft/note/kit.js +162 -0
- package/packs/gallery-soft/note/preview.svg +1 -0
- package/packs/gallery-soft/note/style.css +148 -0
- package/packs/gallery-soft/note/template.html +3 -0
- package/packs/gallery-soft/note/writing.md +20 -0
- package/packs/gallery-soft/recent/component.json +13 -0
- package/packs/gallery-soft/recent/example.html +1 -0
- package/packs/gallery-soft/recent/kit.js +335 -0
- package/packs/gallery-soft/recent/preview.svg +1 -0
- package/packs/gallery-soft/recent/style.css +160 -0
- package/packs/gallery-soft/recent/template.html +1 -0
- package/packs/gallery-soft/recent/writing.md +17 -0
- package/packs/gallery-soft/static/component.json +13 -0
- package/packs/gallery-soft/static/example.html +12 -0
- package/packs/gallery-soft/static/preview.svg +1 -0
- package/packs/gallery-soft/static/style.css +186 -0
- package/packs/gallery-soft/static/template.html +1 -0
- package/packs/gallery-soft/static/writing.md +20 -0
- package/packs/gallery-soft/stats/component.json +13 -0
- package/packs/gallery-soft/stats/example.html +1 -0
- package/packs/gallery-soft/stats/kit.js +341 -0
- package/packs/gallery-soft/stats/preview.svg +1 -0
- package/packs/gallery-soft/stats/style.css +119 -0
- package/packs/gallery-soft/stats/template.html +1 -0
- package/packs/gallery-soft/stats/writing.md +13 -0
- package/packs/gallery-soft/today/component.json +12 -0
- package/packs/gallery-soft/today/example.html +1 -0
- package/packs/gallery-soft/today/kit.js +222 -0
- package/packs/gallery-soft/today/preview.svg +1 -0
- package/packs/gallery-soft/today/style.css +178 -0
- package/packs/gallery-soft/today/template.html +1 -0
- package/packs/gallery-soft/today/writing.md +16 -0
- package/packs/gallery-soft/todos/component.json +19 -0
- package/packs/gallery-soft/todos/example.html +3 -0
- package/packs/gallery-soft/todos/kit.js +527 -0
- package/packs/gallery-soft/todos/preview.svg +1 -0
- package/packs/gallery-soft/todos/style.css +212 -0
- package/packs/gallery-soft/todos/template.html +3 -0
- package/packs/gallery-soft/todos/writing.md +32 -0
- package/packs/gallery-soft/weather/component.json +19 -0
- package/packs/gallery-soft/weather/example.html +3 -0
- package/packs/gallery-soft/weather/kit.js +411 -0
- package/packs/gallery-soft/weather/preview.svg +1 -0
- package/packs/gallery-soft/weather/style.css +185 -0
- package/packs/gallery-soft/weather/template.html +3 -0
- package/packs/gallery-soft/weather/writing.md +28 -0
- package/release.json +9 -9
- package/skill/SKILL.md +12 -12
- package/skill/references/building-blocks.md +4 -4
- package/skill/references/pages-and-data.md +13 -9
- package/skill/references/writing-html.md +2 -0
- package/web/assets/index-BM_K-Auu.css +1 -0
- package/web/assets/index-DRslOBiv.js +15 -0
- package/web/index.html +2 -2
- package/dist/chunk-2NRBY6AJ.mjs +0 -2
- package/dist/chunk-35NQ4PLQ.mjs +0 -2
- package/dist/chunk-4AV456RQ.mjs +0 -2
- package/dist/chunk-5XYHDBJ5.mjs +0 -2
- package/dist/chunk-6A53MZYZ.mjs +0 -2
- package/dist/chunk-6EXH3ORZ.mjs +0 -166
- package/dist/chunk-BVEFGE4B.mjs +0 -2
- package/dist/chunk-D4EURAX4.mjs +0 -4
- package/dist/chunk-DQCE7TJY.mjs +0 -5
- package/dist/chunk-FWNRPMCJ.mjs +0 -12
- package/dist/chunk-FWW4CSAZ.mjs +0 -2
- package/dist/chunk-JVHHHGEA.mjs +0 -3
- package/dist/chunk-PZDLJJTJ.mjs +0 -3
- package/dist/chunk-SXHBYZST.mjs +0 -16
- package/dist/chunk-UFNYFFKO.mjs +0 -20
- package/dist/chunk-UKSHDZZ6.mjs +0 -2
- package/dist/chunk-VJPFVNNW.mjs +0 -15
- package/dist/chunk-VS3ZMVGX.mjs +0 -5
- package/web/assets/index-BK2e_07w.js +0 -13
- package/web/assets/index-CWFI83mF.css +0 -1
|
@@ -0,0 +1,498 @@
|
|
|
1
|
+
// gallery-soft/heatmap (编辑热力图, 柔光风格): Gallery 风格's gallery/heatmap in the soft look, the same record.
|
|
2
|
+
// The workspace's record of edits by day, as GitHub draws a year of contributions: a column a week (Monday at
|
|
3
|
+
// the top, as a Chinese calendar starts it), a square a day, darker for a day of more, read through
|
|
4
|
+
// context.workspace.query("activity") (its manifest's needs hold "workspace": the server's record,
|
|
5
|
+
// .gallery/state/activity.json, a reader's commits and the Agent's writes, by day). Its one setting, `words`
|
|
6
|
+
// (按字数显示, the card's form: the grip at its top right › 设置…, kept in its data part `settings`, which the
|
|
7
|
+
// board writes and this block only reads), draws the words a day added (its net change, a day that took more
|
|
8
|
+
// away than it added drawn as none) instead of how many edits it had. It asks for every day 53 weeks draw (the
|
|
9
|
+
// record keeps as many), so that a day drawn apart is one before the record started; it reads again whenever
|
|
10
|
+
// the workspace says its record changed (the `data` signal, "activity") and as a day begins (the kit's clock),
|
|
11
|
+
// and draws again as its card changes size (`resize`, `layout`, and its own ResizeObserver on the squares'
|
|
12
|
+
// room) or its settings change (`source`). Where the workspace cannot be read (an exported file, where it has
|
|
13
|
+
// no server; a server that does not answer) it says so on the cards' empty face. The words are the page's
|
|
14
|
+
// language.
|
|
15
|
+
//
|
|
16
|
+
// The squares are the hero: each at least 20px (22px on a card wider than 6 cells), 5px apart, as many weeks
|
|
17
|
+
// as the room beside the weekdays' initials holds, the last today's, the weeks spread so that they reach the
|
|
18
|
+
// card's right edge (`fitOf`); a square a level of five, by where its day stands among the record's days of
|
|
19
|
+
// any edit (`levelsOf`: none, then the quarters a third, two thirds and nine tenths of them reach); a day
|
|
20
|
+
// before the record began a faint dot; today ringed in the accent; a month's name over the week its first day
|
|
21
|
+
// falls in. The pointer on a square says its day, its count and how much of it was the reader's and how much
|
|
22
|
+
// the Agent's. A card wider than 6 cells has a column of figures beside the squares (`figuresOf`): the edits
|
|
23
|
+
// (or words) of the weeks drawn, large; the days with any of the days recorded, and today's; the average a
|
|
24
|
+
// day and the day of the most; and how the edits were shared between you and the Agent, a bar of two. Less to
|
|
25
|
+
// more (少 … 多) is the head's quiet meta there; a card 6 cells wide says the count and the weeks there instead,
|
|
26
|
+
// less to more under the squares. Laid out by its card's size (`context.size`; data-w, data-h, data-pad).
|
|
27
|
+
|
|
28
|
+
const WORDS = {
|
|
29
|
+
zh: {
|
|
30
|
+
title: "编辑热力图",
|
|
31
|
+
since: (/** @type {string} */ date) => `从 ${date} 开始记录`,
|
|
32
|
+
span: (/** @type {number} */ weeks) => `近 ${weeks} 周`,
|
|
33
|
+
after: (/** @type {string} */ date) => `${date}以来`,
|
|
34
|
+
edited: "编辑",
|
|
35
|
+
written: "写作",
|
|
36
|
+
editsUnit: "次",
|
|
37
|
+
wordsUnit: "字",
|
|
38
|
+
days: "天",
|
|
39
|
+
activeLabel: "有编辑的天",
|
|
40
|
+
todayLabel: "今天",
|
|
41
|
+
averageLabel: "日均",
|
|
42
|
+
mostLabel: (/** @type {string} */ date) => `最多 · ${date}`,
|
|
43
|
+
you: "你",
|
|
44
|
+
agent: "Agent",
|
|
45
|
+
none: "还没有编辑",
|
|
46
|
+
noneHow: (/** @type {string} */ date) => `从 ${date} 开始记录。你保存的修改、Agent 写进来的内容,都记在当天的格子里。`,
|
|
47
|
+
tip: (/** @type {number} */ edits) => `${edits} 次编辑`,
|
|
48
|
+
tipWords: (/** @type {string} */ words) => `${words} 字`,
|
|
49
|
+
tipWho: (/** @type {number} */ reader, /** @type {number} */ agent) => `你 ${reader} · Agent ${agent}`,
|
|
50
|
+
before: "还没开始记录",
|
|
51
|
+
said: (/** @type {string} */ day, /** @type {string} */ what) => `${day}:${what}`,
|
|
52
|
+
date: (/** @type {Date} */ date) => `${date.getMonth() + 1}月${date.getDate()}日`,
|
|
53
|
+
weekday: (/** @type {Date} */ date) => `${date.getMonth() + 1}月${date.getDate()}日 ${["周日", "周一", "周二", "周三", "周四", "周五", "周六"][date.getDay()]}`,
|
|
54
|
+
month: (/** @type {number} */ month) => `${month + 1}月`,
|
|
55
|
+
rows: ["一", "", "三", "", "五", "", "日"],
|
|
56
|
+
less: "少",
|
|
57
|
+
more: "多",
|
|
58
|
+
offline: "在 Gallery 里打开时显示",
|
|
59
|
+
offlineHow: "导出的文件读不到工作区的编辑记录",
|
|
60
|
+
failed: "读不到工作区的编辑记录",
|
|
61
|
+
locale: "zh-CN",
|
|
62
|
+
},
|
|
63
|
+
en: {
|
|
64
|
+
title: "Editing heatmap",
|
|
65
|
+
since: (/** @type {string} */ date) => `Recording since ${date}`,
|
|
66
|
+
span: (/** @type {number} */ weeks) => `${weeks} weeks`,
|
|
67
|
+
after: (/** @type {string} */ date) => `Since ${date}`,
|
|
68
|
+
edited: ": edits",
|
|
69
|
+
written: ": words",
|
|
70
|
+
editsUnit: "edits",
|
|
71
|
+
wordsUnit: "words",
|
|
72
|
+
days: "days",
|
|
73
|
+
activeLabel: "Days with edits",
|
|
74
|
+
todayLabel: "Today",
|
|
75
|
+
averageLabel: "A day",
|
|
76
|
+
mostLabel: (/** @type {string} */ date) => `Most · ${date}`,
|
|
77
|
+
you: "You",
|
|
78
|
+
agent: "Agent",
|
|
79
|
+
none: "No edits yet",
|
|
80
|
+
noneHow: (/** @type {string} */ date) => `Recording since ${date}. What you save and what your Agent writes go in the square of their day.`,
|
|
81
|
+
tip: (/** @type {number} */ edits) => `${edits} ${edits === 1 ? "edit" : "edits"}`,
|
|
82
|
+
tipWords: (/** @type {string} */ words) => `${words} words`,
|
|
83
|
+
tipWho: (/** @type {number} */ reader, /** @type {number} */ agent) => `You ${reader} · Agent ${agent}`,
|
|
84
|
+
before: "not recorded yet",
|
|
85
|
+
said: (/** @type {string} */ day, /** @type {string} */ what) => `${day}: ${what}`,
|
|
86
|
+
date: (/** @type {Date} */ date) => `${["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"][date.getMonth()]} ${date.getDate()}`,
|
|
87
|
+
weekday: (/** @type {Date} */ date) => `${["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"][date.getDay()]}, ${["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"][date.getMonth()]} ${date.getDate()}`,
|
|
88
|
+
month: (/** @type {number} */ month) => ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"][month],
|
|
89
|
+
rows: ["Mon", "", "Wed", "", "Fri", "", "Sun"],
|
|
90
|
+
less: "Less",
|
|
91
|
+
more: "More",
|
|
92
|
+
offline: "Shown when opened in Gallery",
|
|
93
|
+
offlineHow: "An exported file can't read the workspace's record of edits",
|
|
94
|
+
failed: "The workspace's record of edits could not be read",
|
|
95
|
+
locale: "en",
|
|
96
|
+
},
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
/** The language the page speaks: the UI's where Gallery says it (data-ge-language), else its lang's. */
|
|
100
|
+
function languageOf(/** @type {HTMLElement} */ root) {
|
|
101
|
+
const ui = root.getAttribute("data-ge-language");
|
|
102
|
+
return (ui !== null ? ui === "en" : /^en(?:-|$)/i.test(root.lang)) ? "en" : "zh";
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** @typedef {{ date: string, edits: number, words: number, reader: { edits: number, words: number }, agent: { edits: number, words: number } }} Day */
|
|
106
|
+
|
|
107
|
+
const MOST_WEEKS = 53;
|
|
108
|
+
/** The days of the record it reads: as many as 53 weeks draw at the most (on a Sunday, its first square 370 days back). */
|
|
109
|
+
const DAYS = MOST_WEEKS * 7;
|
|
110
|
+
const two = (/** @type {number} */ value) => String(value).padStart(2, "0");
|
|
111
|
+
/** A local day as the record writes it, YYYY-MM-DD. @param {Date} date */
|
|
112
|
+
const dayOf = (date) => `${date.getFullYear()}-${two(date.getMonth() + 1)}-${two(date.getDate())}`;
|
|
113
|
+
/** The day `count` days after `date` (before, negative), at noon. @param {Date} date @param {number} count */
|
|
114
|
+
const after = (date, count) => new Date(date.getFullYear(), date.getMonth(), date.getDate() + count, 12);
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* How the squares fit a room of `width` x `height` px (the weekdays' column and the months' row left out),
|
|
118
|
+
* a card `wide` cells wide: { cell, gap, weeks, pitch }. A square is 22px (20px on a card 6 cells wide or
|
|
119
|
+
* less), less where seven of them and the gaps would not fit the height, 8px at the least; the gap 5px, 3px
|
|
120
|
+
* where that leaves a square under 12px (a card two cells high: the squares alone); as many weeks as fit
|
|
121
|
+
* across, at most 53, at least 1; `pitch` the step from one week to the next, the room shared out so that
|
|
122
|
+
* the last week reaches the room's right edge.
|
|
123
|
+
* @param {number} width
|
|
124
|
+
* @param {number} height
|
|
125
|
+
* @param {number} wide
|
|
126
|
+
*/
|
|
127
|
+
function fitOf(width, height, wide) {
|
|
128
|
+
const most = wide <= 6 ? 20 : 22;
|
|
129
|
+
const gap = Math.floor((height - 6 * 5) / 7) < 12 ? 3 : 5;
|
|
130
|
+
const cell = Math.max(8, Math.min(most, Math.floor((height - 6 * gap) / 7)));
|
|
131
|
+
const weeks = Math.max(1, Math.min(MOST_WEEKS, Math.floor((width + gap) / (cell + gap))));
|
|
132
|
+
return { cell, gap, weeks, pitch: weeks > 1 ? (width + gap) / weeks : cell + gap };
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* The weeks drawn, the last the one of `today` (a Date), `weeks` of them: [[{ date, day, future }…7]…],
|
|
137
|
+
* each a column, Monday first; `future` a day after today, which is not drawn.
|
|
138
|
+
* @param {Date} today
|
|
139
|
+
* @param {number} weeks
|
|
140
|
+
*/
|
|
141
|
+
function weeksOf(today, weeks) {
|
|
142
|
+
const monday = after(today, -((today.getDay() + 6) % 7));
|
|
143
|
+
const columns = [];
|
|
144
|
+
for (let week = weeks - 1; week >= 0; week--) {
|
|
145
|
+
const start = after(monday, -7 * week);
|
|
146
|
+
columns.push(Array.from({ length: 7 }, (_, row) => {
|
|
147
|
+
const date = after(start, row);
|
|
148
|
+
return { date, day: dayOf(date), future: dayOf(date) > dayOf(today) };
|
|
149
|
+
}));
|
|
150
|
+
}
|
|
151
|
+
return columns;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Where a square's levels change, of `values` (each day's count): the counts a third, two thirds and nine
|
|
156
|
+
* tenths of the days of any reach, so that a level says where a day stands among the days of the record.
|
|
157
|
+
* @param {number[]} values
|
|
158
|
+
*/
|
|
159
|
+
function levelsOf(values) {
|
|
160
|
+
const some = values.filter((value) => value > 0).sort((a, b) => a - b);
|
|
161
|
+
const at = (/** @type {number} */ share) => (some.length ? some[Math.min(some.length - 1, Math.floor(share * some.length))] : 0);
|
|
162
|
+
return [at(0.34), at(0.66), at(0.9)];
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* The level of a day's square, 0 (none) to 4 (the most), of `value` (its edits, or the words it added,
|
|
167
|
+
* none for a day that took more away) against `levels` (levelsOf's).
|
|
168
|
+
* @param {number} value
|
|
169
|
+
* @param {number[]} levels
|
|
170
|
+
*/
|
|
171
|
+
function levelOf(value, levels) {
|
|
172
|
+
if (!(value > 0)) return 0;
|
|
173
|
+
return value <= levels[0] ? 1 : value <= levels[1] ? 2 : value <= levels[2] ? 3 : 4;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* What the figures beside the squares say of the record `days` (workspace.query("activity")'s data) over
|
|
178
|
+
* the days drawn, from `first` (YYYY-MM-DD, the first square) to `today`, counting `words` (the setting) or
|
|
179
|
+
* edits, in `language`: the label (近 14 周编辑, or 11月10日以来编辑 where the record began after the first
|
|
180
|
+
* square), the total, how many days had any edit of how many were recorded, today's, the average a day and
|
|
181
|
+
* the day of the most (null for none), and the share of the edits that were the reader's (0 to 100, null
|
|
182
|
+
* for none); `since`, the day the record began (null for no record), and `empty`, nothing counted at all.
|
|
183
|
+
* @param {Array<{ date: string, edits: number, words: number, reader?: { edits: number }, agent?: { edits: number } }>} days
|
|
184
|
+
* @param {boolean} words
|
|
185
|
+
* @param {string} first
|
|
186
|
+
* @param {string} today
|
|
187
|
+
* @param {number} weeks
|
|
188
|
+
* @param {"zh" | "en"} language
|
|
189
|
+
*/
|
|
190
|
+
function figuresOf(days, words, first, today, weeks, language) {
|
|
191
|
+
const said = WORDS[language];
|
|
192
|
+
const since = days.map((day) => day.date).sort()[0] ?? null;
|
|
193
|
+
const drawn = days.filter((day) => day.date >= first && day.date <= today);
|
|
194
|
+
const value = (/** @type {{ edits: number, words: number }} */ day) => (words ? Math.max(0, day.words) : day.edits);
|
|
195
|
+
const total = drawn.reduce((sum, day) => sum + value(day), 0);
|
|
196
|
+
const edits = drawn.reduce((sum, day) => sum + day.edits, 0);
|
|
197
|
+
const reader = drawn.reduce((sum, day) => sum + (day.reader?.edits ?? 0), 0);
|
|
198
|
+
const agent = drawn.reduce((sum, day) => sum + (day.agent?.edits ?? 0), 0);
|
|
199
|
+
let most = null;
|
|
200
|
+
for (const day of drawn) if (value(day) > 0 && (!most || value(day) > value(most))) most = day;
|
|
201
|
+
const sinceDate = since ? new Date(`${since}T12:00:00`) : null;
|
|
202
|
+
const period = since && since > first && sinceDate ? said.after(said.date(sinceDate)) : said.span(weeks);
|
|
203
|
+
return {
|
|
204
|
+
label: `${period}${words ? said.written : said.edited}`,
|
|
205
|
+
total,
|
|
206
|
+
active: drawn.filter((day) => day.edits > 0).length,
|
|
207
|
+
recorded: drawn.length,
|
|
208
|
+
today: value(drawn.find((day) => day.date === today) ?? { edits: 0, words: 0 }),
|
|
209
|
+
average: drawn.length ? Math.round(total / drawn.length) : 0,
|
|
210
|
+
most: most ? { date: most.date, value: value(most) } : null,
|
|
211
|
+
share: reader + agent > 0 ? Math.round((100 * reader) / (reader + agent)) : null,
|
|
212
|
+
since,
|
|
213
|
+
empty: edits === 0,
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/** A count as the card writes it, grouped (1,407), short past 99,999 (12.3万, 123.5K). @param {number} value @param {"zh" | "en"} language */
|
|
218
|
+
const countOf = (value, language) => new Intl.NumberFormat(WORDS[language].locale, Math.abs(value) >= 100_000 ? { notation: "compact", maximumFractionDigits: 1 } : { maximumFractionDigits: 0 }).format(value);
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* The size the face is laid out by: its card's cells (`context.size`), or `fallback` (the manifest's first
|
|
222
|
+
* size) where no card is; and the frame's padding for it, as the soft cards pad theirs: "xs" on a card one
|
|
223
|
+
* cell high, "s" on a small one (2 × 2 and less), "l" on one 3 cells high or more, else "m".
|
|
224
|
+
* @param {DocComponentSize | null | undefined} size
|
|
225
|
+
* @param {[number, number]} fallback
|
|
226
|
+
*/
|
|
227
|
+
function sizeOf(size, fallback) {
|
|
228
|
+
const w = size?.w ?? fallback[0];
|
|
229
|
+
const h = size?.h ?? fallback[1];
|
|
230
|
+
return { w, h, pad: h === 1 ? "xs" : w <= 2 && h <= 2 ? "s" : h >= 3 ? "l" : "m" };
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* An element of the face, of `kind` (its class).
|
|
235
|
+
* @template {keyof HTMLElementTagNameMap} K
|
|
236
|
+
* @param {K} tag
|
|
237
|
+
* @param {string} kind
|
|
238
|
+
* @param {string} [text]
|
|
239
|
+
*/
|
|
240
|
+
function made(tag, kind, text) {
|
|
241
|
+
const node = document.createElement(tag);
|
|
242
|
+
node.className = `gallery-soft-heatmap-${kind}`;
|
|
243
|
+
if (text !== undefined) node.textContent = text;
|
|
244
|
+
return node;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const HEAT = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><rect x="3.8" y="3.8" width="6.4" height="6.4" rx="1.6"/><rect x="13.8" y="3.8" width="6.4" height="6.4" rx="1.6"/><rect x="3.8" y="13.8" width="6.4" height="6.4" rx="1.6"/><rect x="13.8" y="13.8" width="6.4" height="6.4" rx="1.6" fill="currentColor"/></svg>';
|
|
248
|
+
|
|
249
|
+
DocKit.component({
|
|
250
|
+
id: "gallery-soft/heatmap",
|
|
251
|
+
on: ["data", "source", "resize", "layout", "clock"],
|
|
252
|
+
mount(host, context) {
|
|
253
|
+
const language = languageOf(document.documentElement);
|
|
254
|
+
const words = WORDS[language];
|
|
255
|
+
const face = made("div", "face");
|
|
256
|
+
const head = made("div", "head");
|
|
257
|
+
const chip = made("span", "chip");
|
|
258
|
+
chip.setAttribute("aria-hidden", "true");
|
|
259
|
+
chip.innerHTML = HEAT;
|
|
260
|
+
const meta = made("span", "meta");
|
|
261
|
+
head.append(chip, made("span", "title", words.title), meta);
|
|
262
|
+
const body = made("div", "body");
|
|
263
|
+
const aside = made("div", "aside");
|
|
264
|
+
const grid = made("div", "grid");
|
|
265
|
+
const room = made("div", "room");
|
|
266
|
+
room.append(grid);
|
|
267
|
+
body.append(aside, room);
|
|
268
|
+
const legend = made("div", "legend");
|
|
269
|
+
legend.setAttribute("aria-hidden", "true");
|
|
270
|
+
legend.append(made("span", "less", words.less), ...[0, 1, 2, 3, 4].map((level) => {
|
|
271
|
+
const square = made("i", "key");
|
|
272
|
+
square.dataset.level = String(level);
|
|
273
|
+
return square;
|
|
274
|
+
}), made("span", "more", words.more));
|
|
275
|
+
const empty = made("div", "empty");
|
|
276
|
+
// The tip stands over the card, in the page's top layer (a popover; where a browser has none, fixed to the
|
|
277
|
+
// window), never inside the card, whose overflow a tip near its edge would widen into a scrollbar or cut.
|
|
278
|
+
const tip = made("div", "tip");
|
|
279
|
+
const popover = typeof tip.showPopover === "function";
|
|
280
|
+
if (popover) tip.popover = "manual";
|
|
281
|
+
else tip.hidden = true;
|
|
282
|
+
tip.setAttribute("role", "tooltip");
|
|
283
|
+
// As wide as the corner a board's grip takes of the content's top right (--soft-grip-room, which CSS alone
|
|
284
|
+
// resolves), measured where the months' row is the face's first (the squares alone).
|
|
285
|
+
const gripRoom = made("i", "grip-room");
|
|
286
|
+
gripRoom.setAttribute("aria-hidden", "true");
|
|
287
|
+
face.append(head, body, empty, gripRoom);
|
|
288
|
+
host.append(face);
|
|
289
|
+
const workspace = context.workspace;
|
|
290
|
+
if (!workspace) return void context.fail(new Error("its manifest's needs hold \"workspace\": context.workspace is not there"));
|
|
291
|
+
|
|
292
|
+
/** @type {Day[] | null} */
|
|
293
|
+
let days = null;
|
|
294
|
+
let failure = "";
|
|
295
|
+
let drawn = "";
|
|
296
|
+
/** @type {Map<HTMLElement, { date: Date, entry: Day | null, value: number, recorded: boolean }>} */
|
|
297
|
+
const squares = new Map();
|
|
298
|
+
const draw = () => {
|
|
299
|
+
const settings = context.readJSON("settings");
|
|
300
|
+
if (settings === undefined) return; // no JSON: the instance has failed, and Gallery shows what it holds
|
|
301
|
+
const byWords = settings !== null && typeof settings === "object" && settings.words === true;
|
|
302
|
+
const { w, h, pad } = sizeOf(context.size, [8, 4]);
|
|
303
|
+
const narrow = w <= 6;
|
|
304
|
+
// Two cells high (8 × 2): the squares alone, no head, figures or legend (the user's word of 2026-11-09).
|
|
305
|
+
const alone = h <= 2;
|
|
306
|
+
face.toggleAttribute("data-alone", alone);
|
|
307
|
+
Object.assign(face.dataset, { w: String(w), h: String(h), pad });
|
|
308
|
+
face.toggleAttribute("data-narrow", narrow);
|
|
309
|
+
face.toggleAttribute("data-unread", Boolean(failure));
|
|
310
|
+
if (failure) {
|
|
311
|
+
meta.replaceChildren();
|
|
312
|
+
const disc = made("span", "disc");
|
|
313
|
+
disc.innerHTML = HEAT;
|
|
314
|
+
empty.replaceChildren(disc, made("p", "line", failure === "offline" ? words.offline : words.failed), ...(failure === "offline" ? [made("p", "how", words.offlineHow)] : []));
|
|
315
|
+
drawn = "";
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
empty.replaceChildren();
|
|
319
|
+
const today = new Date();
|
|
320
|
+
// The room the squares have: the room's, the months' row and the weekdays' column taken off.
|
|
321
|
+
const width = room.clientWidth - 22;
|
|
322
|
+
const height = room.clientHeight - 18;
|
|
323
|
+
const { cell, gap, weeks, pitch } = fitOf(width, height, w);
|
|
324
|
+
const columns = weeksOf(today, weeks);
|
|
325
|
+
const figures = figuresOf(days ?? [], byWords, columns[0][0].day, dayOf(today), weeks, language);
|
|
326
|
+
const sinceSaid = figures.since ? words.since(words.date(new Date(`${figures.since}T12:00:00`))) : "";
|
|
327
|
+
// The head's quiet meta: less to more beside the figures, the count and the weeks on a narrow card, since when while nothing is counted.
|
|
328
|
+
if (days !== null && figures.empty) meta.replaceChildren(sinceSaid);
|
|
329
|
+
else if (narrow) meta.replaceChildren(made("b", "count", countOf(figures.total, language)), ` ${byWords ? words.wordsUnit : words.editsUnit}`, made("span", "dot"), words.span(weeks));
|
|
330
|
+
else meta.replaceChildren(legend);
|
|
331
|
+
if (narrow) {
|
|
332
|
+
if (legend.parentElement !== room) room.append(legend);
|
|
333
|
+
} else if (legend.parentElement === room) legend.remove();
|
|
334
|
+
// The figures beside the squares, on a card wide enough.
|
|
335
|
+
aside.replaceChildren();
|
|
336
|
+
if (!narrow && days !== null) {
|
|
337
|
+
if (figures.empty) aside.append(made("span", "label", words.none), made("p", "note", figures.since ? words.noneHow(words.date(new Date(`${figures.since}T12:00:00`))) : ""));
|
|
338
|
+
else {
|
|
339
|
+
const unit = byWords ? words.wordsUnit : words.editsUnit;
|
|
340
|
+
const total = made("p", "total", countOf(figures.total, language));
|
|
341
|
+
total.append(made("span", "unit", unit));
|
|
342
|
+
const figure = (/** @type {string} */ value, /** @type {string} */ small) => {
|
|
343
|
+
const node = made("span", "value", value);
|
|
344
|
+
node.append(made("small", "small", small));
|
|
345
|
+
return node;
|
|
346
|
+
};
|
|
347
|
+
const table = made("div", "figures");
|
|
348
|
+
table.append(made("span", "label", words.activeLabel), made("span", "label", words.todayLabel), figure(String(figures.active), `/ ${figures.recorded} ${words.days}`), figure(countOf(figures.today, language), unit));
|
|
349
|
+
if (figures.recorded > 6 && figures.most) {
|
|
350
|
+
const most = new Date(`${figures.most.date}T12:00:00`);
|
|
351
|
+
table.append(made("span", "label", words.averageLabel), made("span", "label", words.mostLabel(words.date(most))), figure(countOf(figures.average, language), unit), figure(countOf(figures.most.value, language), unit));
|
|
352
|
+
}
|
|
353
|
+
aside.append(made("span", "label", figures.label), total, table);
|
|
354
|
+
if (figures.share !== null) {
|
|
355
|
+
const split = made("div", "split");
|
|
356
|
+
const bar = made("div", "bar");
|
|
357
|
+
const you = made("i", "you");
|
|
358
|
+
you.style.width = `${figures.share}%`;
|
|
359
|
+
bar.append(you, made("i", "agent"));
|
|
360
|
+
const key = made("div", "key-line");
|
|
361
|
+
const side = (/** @type {string} */ kind, /** @type {string} */ name, /** @type {number} */ share) => {
|
|
362
|
+
const node = made("span", kind);
|
|
363
|
+
node.append(made("i", "swatch"), `${name} `, made("b", "share", `${share}%`));
|
|
364
|
+
return node;
|
|
365
|
+
};
|
|
366
|
+
key.append(side("you-key", words.you, figures.share), side("agent-key", words.agent, 100 - figures.share));
|
|
367
|
+
split.append(bar, key);
|
|
368
|
+
aside.append(split);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
const key = JSON.stringify([cell, gap, weeks, pitch, width, dayOf(today), byWords, days, language]);
|
|
373
|
+
if (key === drawn) return;
|
|
374
|
+
drawn = key;
|
|
375
|
+
const values = (days ?? []).map((day) => (byWords ? Math.max(0, day.words) : day.edits));
|
|
376
|
+
const levels = levelsOf(values);
|
|
377
|
+
const byDate = new Map((days ?? []).map((item) => [item.date, item]));
|
|
378
|
+
squares.clear();
|
|
379
|
+
hide(); // the squares drawn anew: the one it stood over is gone
|
|
380
|
+
/** @type {HTMLElement[]} */
|
|
381
|
+
const nodes = [];
|
|
382
|
+
const top = (/** @type {number} */ row) => 18 + row * (cell + gap);
|
|
383
|
+
columns.forEach((column, index) => {
|
|
384
|
+
for (const [row, { date, day, future }] of column.entries()) {
|
|
385
|
+
if (future) continue;
|
|
386
|
+
const square = made("i", "day");
|
|
387
|
+
square.style.left = `${(22 + index * pitch).toFixed(1)}px`;
|
|
388
|
+
square.style.top = `${top(row)}px`;
|
|
389
|
+
square.style.width = square.style.height = `${cell}px`;
|
|
390
|
+
const recorded = days !== null && figures.since !== null && day >= figures.since;
|
|
391
|
+
const entry = byDate.get(day) ?? null;
|
|
392
|
+
const value = entry ? (byWords ? Math.max(0, entry.words) : entry.edits) : 0;
|
|
393
|
+
if (!recorded) square.classList.add("gallery-soft-heatmap-before");
|
|
394
|
+
else square.dataset.level = String(levelOf(value, levels));
|
|
395
|
+
if (day === dayOf(today)) square.classList.add("gallery-soft-heatmap-today");
|
|
396
|
+
square.setAttribute("aria-label", words.said(words.weekday(date), recorded ? (byWords ? words.tipWords(countOf(value, language)) : words.tip(value)) : words.before));
|
|
397
|
+
squares.set(square, { date, entry, value, recorded });
|
|
398
|
+
nodes.push(square);
|
|
399
|
+
}
|
|
400
|
+
});
|
|
401
|
+
// A month's name over the week its first day falls in (the first week's too where most of it is that
|
|
402
|
+
// month), none closer to the one before than 34px.
|
|
403
|
+
let lastMonth = -1;
|
|
404
|
+
let lastX = -99;
|
|
405
|
+
columns.forEach((column, index) => {
|
|
406
|
+
for (const { date } of column) {
|
|
407
|
+
if (date.getDate() === 1 || (index === 0 && after(date, 21).getMonth() === date.getMonth())) {
|
|
408
|
+
const x = 22 + index * pitch;
|
|
409
|
+
if (date.getMonth() !== lastMonth && x - lastX >= 34) {
|
|
410
|
+
const label = made("span", "month", words.month(date.getMonth()));
|
|
411
|
+
label.style.left = `${x.toFixed(1)}px`;
|
|
412
|
+
nodes.push(label);
|
|
413
|
+
lastX = x;
|
|
414
|
+
}
|
|
415
|
+
lastMonth = date.getMonth();
|
|
416
|
+
break;
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
});
|
|
420
|
+
words.rows.forEach((name, row) => {
|
|
421
|
+
if (!name) return;
|
|
422
|
+
const label = made("span", "row", name);
|
|
423
|
+
label.style.top = `${top(row) + (cell - 14) / 2}px`;
|
|
424
|
+
nodes.push(label);
|
|
425
|
+
});
|
|
426
|
+
grid.replaceChildren(...nodes, tip);
|
|
427
|
+
// The squares alone: the months' row is the card's first, and a name that would reach into the grip's corner is left out.
|
|
428
|
+
if (alone) {
|
|
429
|
+
const end = room.getBoundingClientRect().right - gripRoom.offsetWidth;
|
|
430
|
+
for (const label of grid.querySelectorAll(".gallery-soft-heatmap-month")) if (label.getBoundingClientRect().right > end) label.remove();
|
|
431
|
+
}
|
|
432
|
+
};
|
|
433
|
+
// The pointer on a square: its day, its count, and the reader's and the Agent's shares of it, over it (under
|
|
434
|
+
// it where the window has no room above), its middle at the square's but held 8px inside the window.
|
|
435
|
+
const show = (/** @type {Event} */ event) => {
|
|
436
|
+
const square = /** @type {HTMLElement} */ (event.target);
|
|
437
|
+
const said = squares.get(square);
|
|
438
|
+
if (!said || !said.recorded) return hide();
|
|
439
|
+
const settings = context.readJSON("settings");
|
|
440
|
+
const byWords = settings !== null && typeof settings === "object" && settings?.words === true;
|
|
441
|
+
const first = made("b", "tip-count", byWords ? words.tipWords(countOf(said.value, language)) : words.tip(said.value));
|
|
442
|
+
tip.replaceChildren(first, made("span", "tip-day", ` · ${words.weekday(said.date)}`), document.createElement("br"), made("span", "tip-who", words.tipWho(said.entry?.reader?.edits ?? 0, said.entry?.agent?.edits ?? 0)));
|
|
443
|
+
if (popover) {
|
|
444
|
+
if (!tip.matches(":popover-open")) tip.showPopover();
|
|
445
|
+
} else tip.hidden = false;
|
|
446
|
+
const box = square.getBoundingClientRect();
|
|
447
|
+
const width = tip.offsetWidth;
|
|
448
|
+
const height = tip.offsetHeight;
|
|
449
|
+
const left = Math.max(8, Math.min(innerWidth - 8 - width, box.left + box.width / 2 - width / 2));
|
|
450
|
+
const top = box.top - 8 - height < 8 ? box.bottom + 8 : box.top - 8 - height;
|
|
451
|
+
tip.style.left = `${Math.round(left)}px`;
|
|
452
|
+
tip.style.top = `${Math.round(top)}px`;
|
|
453
|
+
for (const other of grid.querySelectorAll(".gallery-soft-heatmap-pointed")) other.classList.remove("gallery-soft-heatmap-pointed");
|
|
454
|
+
square.classList.add("gallery-soft-heatmap-pointed");
|
|
455
|
+
};
|
|
456
|
+
function hide() {
|
|
457
|
+
if (popover) {
|
|
458
|
+
if (tip.matches(":popover-open")) tip.hidePopover();
|
|
459
|
+
} else tip.hidden = true;
|
|
460
|
+
for (const other of grid.querySelectorAll(".gallery-soft-heatmap-pointed")) other.classList.remove("gallery-soft-heatmap-pointed");
|
|
461
|
+
}
|
|
462
|
+
context.listen(grid, "pointerover", show);
|
|
463
|
+
context.listen(grid, "pointerleave", hide);
|
|
464
|
+
// A scroll (the page's, or a card's) moves the square from under it: gone until the pointer is on one again.
|
|
465
|
+
context.listen(document, "scroll", hide, { capture: true, passive: true });
|
|
466
|
+
context.signal?.addEventListener("abort", hide, { once: true });
|
|
467
|
+
let asking = 0; // the last read asked: an older answer that comes after it is dropped
|
|
468
|
+
const read = () => {
|
|
469
|
+
const ticket = ++asking;
|
|
470
|
+
workspace.query("activity", { days: DAYS }).then(({ data }) => {
|
|
471
|
+
if (ticket !== asking || context.signal.aborted) return;
|
|
472
|
+
days = data;
|
|
473
|
+
failure = "";
|
|
474
|
+
draw();
|
|
475
|
+
}, (/** @type {any} */ error) => {
|
|
476
|
+
if (ticket !== asking || context.signal.aborted) return;
|
|
477
|
+
days = null;
|
|
478
|
+
failure = error?.code === "offline" ? "offline" : "failed";
|
|
479
|
+
draw();
|
|
480
|
+
});
|
|
481
|
+
};
|
|
482
|
+
draw();
|
|
483
|
+
read();
|
|
484
|
+
// The room the squares have, its height too (the kit's `resize` hears its width alone): a card given its
|
|
485
|
+
// height after the first draw (a board laid out late) is drawn again as it gets it.
|
|
486
|
+
if (typeof ResizeObserver === "function") {
|
|
487
|
+
const watch = new ResizeObserver(() => draw());
|
|
488
|
+
watch.observe(room);
|
|
489
|
+
context.signal?.addEventListener("abort", () => watch.disconnect(), { once: true });
|
|
490
|
+
}
|
|
491
|
+
return {
|
|
492
|
+
update(reason, detail) {
|
|
493
|
+
if ((reason === "data" && detail === "activity") || (reason === "clock" && detail === "day")) read();
|
|
494
|
+
else if (reason !== "data" && reason !== "clock") draw();
|
|
495
|
+
},
|
|
496
|
+
};
|
|
497
|
+
},
|
|
498
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 240 120"><defs><linearGradient id="w" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#3e7ac6" stop-opacity=".07"/><stop offset=".52" stop-color="#3e7ac6" stop-opacity="0"/></linearGradient></defs><rect width="240" height="120" rx="8" fill="#f3f3f1"/><rect x="16.5" y="12.5" width="207" height="95" rx="14" fill="#ffffff" stroke="#e8e7e4"/><rect x="17" y="13" width="206" height="94" rx="13.5" fill="url(#w)"/><rect x="30" y="24" width="14" height="14" rx="4.5" fill="#ecf3fb"/><rect x="34" y="30" width="6" height="2" rx="1" fill="#3e7ac6"/><rect x="50" y="28" width="40" height="6" rx="3" fill="#37352f"/><rect x="170" y="28" width="5" height="5" rx="1.5" fill="#ececed"/><rect x="177" y="28" width="5" height="5" rx="1.5" fill="#d6e4f4"/><rect x="184" y="28" width="5" height="5" rx="1.5" fill="#a6c4e8"/><rect x="191" y="28" width="5" height="5" rx="1.5" fill="#6b9bd4"/><rect x="198" y="28" width="5" height="5" rx="1.5" fill="#3e7ac6"/><rect x="30" y="48" width="30" height="4" rx="2" fill="#c4c2bd"/><rect x="30" y="58" width="40" height="14" rx="7" fill="#1f1f1f"/><rect x="30" y="82" width="22" height="4" rx="2" fill="#c4c2bd"/><rect x="30" y="90" width="22" height="5" rx="2.5" fill="#4a4843"/><rect x="30" y="99" width="18" height="3" rx="1.5" fill="#3e7ac6"/><rect x="50" y="99" width="24" height="3" rx="1.5" fill="#afcaea"/><rect x="96" y="44" width="8" height="7" rx="2" fill="#ececed"/><rect x="96" y="52.6" width="8" height="7" rx="2" fill="#6b9bd4"/><rect x="96" y="61.2" width="8" height="7" rx="2" fill="#d6e4f4"/><rect x="96" y="69.8" width="8" height="7" rx="2" fill="#3e7ac6"/><rect x="96" y="78.4" width="8" height="7" rx="2" fill="#a6c4e8"/><rect x="96" y="87" width="8" height="7" rx="2" fill="#ececed"/><rect x="96" y="95.6" width="8" height="7" rx="2" fill="#6b9bd4"/><rect x="107" y="44" width="8" height="7" rx="2" fill="#a6c4e8"/><rect x="107" y="52.6" width="8" height="7" rx="2" fill="#d6e4f4"/><rect x="107" y="61.2" width="8" height="7" rx="2" fill="#ececed"/><rect x="107" y="69.8" width="8" height="7" rx="2" fill="#3e7ac6"/><rect x="107" y="78.4" width="8" height="7" rx="2" fill="#6b9bd4"/><rect x="107" y="87" width="8" height="7" rx="2" fill="#a6c4e8"/><rect x="107" y="95.6" width="8" height="7" rx="2" fill="#d6e4f4"/><rect x="118" y="44" width="8" height="7" rx="2" fill="#3e7ac6"/><rect x="118" y="52.6" width="8" height="7" rx="2" fill="#3e7ac6"/><rect x="118" y="61.2" width="8" height="7" rx="2" fill="#3e7ac6"/><rect x="118" y="69.8" width="8" height="7" rx="2" fill="#3e7ac6"/><rect x="118" y="78.4" width="8" height="7" rx="2" fill="#3e7ac6"/><rect x="118" y="87" width="8" height="7" rx="2" fill="#3e7ac6"/><rect x="118" y="95.6" width="8" height="7" rx="2" fill="#3e7ac6"/><rect x="129" y="44" width="8" height="7" rx="2" fill="#d6e4f4"/><rect x="129" y="52.6" width="8" height="7" rx="2" fill="#a6c4e8"/><rect x="129" y="61.2" width="8" height="7" rx="2" fill="#6b9bd4"/><rect x="129" y="69.8" width="8" height="7" rx="2" fill="#3e7ac6"/><rect x="129" y="78.4" width="8" height="7" rx="2" fill="#ececed"/><rect x="129" y="87" width="8" height="7" rx="2" fill="#d6e4f4"/><rect x="129" y="95.6" width="8" height="7" rx="2" fill="#a6c4e8"/><rect x="140" y="44" width="8" height="7" rx="2" fill="#6b9bd4"/><rect x="140" y="52.6" width="8" height="7" rx="2" fill="#ececed"/><rect x="140" y="61.2" width="8" height="7" rx="2" fill="#a6c4e8"/><rect x="140" y="69.8" width="8" height="7" rx="2" fill="#3e7ac6"/><rect x="140" y="78.4" width="8" height="7" rx="2" fill="#d6e4f4"/><rect x="140" y="87" width="8" height="7" rx="2" fill="#6b9bd4"/><rect x="140" y="95.6" width="8" height="7" rx="2" fill="#ececed"/><rect x="151" y="44" width="8" height="7" rx="2" fill="#ececed"/><rect x="151" y="52.6" width="8" height="7" rx="2" fill="#6b9bd4"/><rect x="151" y="61.2" width="8" height="7" rx="2" fill="#d6e4f4"/><rect x="151" y="69.8" width="8" height="7" rx="2" fill="#3e7ac6"/><rect x="151" y="78.4" width="8" height="7" rx="2" fill="#a6c4e8"/><rect x="151" y="87" width="8" height="7" rx="2" fill="#ececed"/><rect x="151" y="95.6" width="8" height="7" rx="2" fill="#6b9bd4"/><rect x="162" y="44" width="8" height="7" rx="2" fill="#a6c4e8"/><rect x="162" y="52.6" width="8" height="7" rx="2" fill="#d6e4f4"/><rect x="162" y="61.2" width="8" height="7" rx="2" fill="#ececed"/><rect x="162" y="69.8" width="8" height="7" rx="2" fill="#3e7ac6"/><rect x="162" y="78.4" width="8" height="7" rx="2" fill="#6b9bd4"/><rect x="162" y="87" width="8" height="7" rx="2" fill="#a6c4e8"/><rect x="162" y="95.6" width="8" height="7" rx="2" fill="#d6e4f4"/><rect x="173" y="44" width="8" height="7" rx="2" fill="#3e7ac6"/><rect x="173" y="52.6" width="8" height="7" rx="2" fill="#3e7ac6"/><rect x="173" y="61.2" width="8" height="7" rx="2" fill="#3e7ac6"/><rect x="173" y="69.8" width="8" height="7" rx="2" fill="#3e7ac6"/><rect x="173" y="78.4" width="8" height="7" rx="2" fill="#3e7ac6"/><rect x="173" y="87" width="8" height="7" rx="2" fill="#3e7ac6"/><rect x="173" y="95.6" width="8" height="7" rx="2" fill="#3e7ac6"/><rect x="184" y="44" width="8" height="7" rx="2" fill="#d6e4f4"/><rect x="184" y="52.6" width="8" height="7" rx="2" fill="#a6c4e8"/><rect x="184" y="61.2" width="8" height="7" rx="2" fill="#6b9bd4"/><rect x="184" y="69.8" width="8" height="7" rx="2" fill="#3e7ac6"/><rect x="184" y="78.4" width="8" height="7" rx="2" fill="#ececed"/><rect x="184" y="87" width="8" height="7" rx="2" fill="#d6e4f4"/><rect x="184" y="95.6" width="8" height="7" rx="2" fill="#a6c4e8"/><rect x="195" y="44" width="8" height="7" rx="2" fill="#6b9bd4"/><rect x="195" y="52.6" width="8" height="7" rx="2" fill="#ececed"/><rect x="195" y="61.2" width="8" height="7" rx="2" fill="#a6c4e8"/><rect x="195" y="69.8" width="8" height="7" rx="2" fill="#3e7ac6"/><rect x="195" y="78.4" width="8" height="7" rx="2" fill="#d6e4f4"/><rect x="195" y="87" width="8" height="7" rx="2" fill="#6b9bd4"/><rect x="195" y="95.6" width="8" height="7" rx="2" fill="#ececed"/></svg>
|