lotuspod 0.1.0__tar.gz

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 (98) hide show
  1. lotuspod-0.1.0/LICENSE +21 -0
  2. lotuspod-0.1.0/PKG-INFO +123 -0
  3. lotuspod-0.1.0/README.md +113 -0
  4. lotuspod-0.1.0/pyproject.toml +20 -0
  5. lotuspod-0.1.0/setup.cfg +4 -0
  6. lotuspod-0.1.0/src/lotuspod/__init__.py +3 -0
  7. lotuspod-0.1.0/src/lotuspod/__main__.py +6 -0
  8. lotuspod-0.1.0/src/lotuspod/_templates/artifact.html +59 -0
  9. lotuspod-0.1.0/src/lotuspod/_templates/index.html +130 -0
  10. lotuspod-0.1.0/src/lotuspod/_theme/css/attachments.css +126 -0
  11. lotuspod-0.1.0/src/lotuspod/_theme/css/base.css +576 -0
  12. lotuspod-0.1.0/src/lotuspod/_theme/css/comments.css +1491 -0
  13. lotuspod-0.1.0/src/lotuspod/_theme/css/decisions.css +344 -0
  14. lotuspod-0.1.0/src/lotuspod/_theme/css/forms.css +89 -0
  15. lotuspod-0.1.0/src/lotuspod/_theme/css/image-viewer.css +100 -0
  16. lotuspod-0.1.0/src/lotuspod/_theme/css/index.css +238 -0
  17. lotuspod-0.1.0/src/lotuspod/_theme/css/live-page.css +65 -0
  18. lotuspod-0.1.0/src/lotuspod/_theme/css/narrow.css +239 -0
  19. lotuspod-0.1.0/src/lotuspod/_theme/css/panel-resize.css +66 -0
  20. lotuspod-0.1.0/src/lotuspod/_theme/css/prose.css +243 -0
  21. lotuspod-0.1.0/src/lotuspod/_theme/css/report.css +119 -0
  22. lotuspod-0.1.0/src/lotuspod/_theme/css/table-expand.css +56 -0
  23. lotuspod-0.1.0/src/lotuspod/_theme/favicon.svg +1 -0
  24. lotuspod-0.1.0/src/lotuspod/_theme/js/attachments.js +352 -0
  25. lotuspod-0.1.0/src/lotuspod/_theme/js/comments.js +2395 -0
  26. lotuspod-0.1.0/src/lotuspod/_theme/js/decisions.js +268 -0
  27. lotuspod-0.1.0/src/lotuspod/_theme/js/image-viewer.js +131 -0
  28. lotuspod-0.1.0/src/lotuspod/_theme/js/live-page.js +185 -0
  29. lotuspod-0.1.0/src/lotuspod/_theme/js/narrow.js +542 -0
  30. lotuspod-0.1.0/src/lotuspod/_theme/js/page-close.js +1 -0
  31. lotuspod-0.1.0/src/lotuspod/_theme/js/page-open.js +494 -0
  32. lotuspod-0.1.0/src/lotuspod/_theme/js/panel-resize.js +216 -0
  33. lotuspod-0.1.0/src/lotuspod/_theme/js/table-expand.js +144 -0
  34. lotuspod-0.1.0/src/lotuspod/_theme/js/tables.js +35 -0
  35. lotuspod-0.1.0/src/lotuspod/_theme/tokens.json +63 -0
  36. lotuspod-0.1.0/src/lotuspod/access.py +324 -0
  37. lotuspod-0.1.0/src/lotuspod/agents.py +505 -0
  38. lotuspod-0.1.0/src/lotuspod/api.py +551 -0
  39. lotuspod-0.1.0/src/lotuspod/backup.py +496 -0
  40. lotuspod-0.1.0/src/lotuspod/cli.py +2505 -0
  41. lotuspod-0.1.0/src/lotuspod/comments.py +153 -0
  42. lotuspod-0.1.0/src/lotuspod/db.py +897 -0
  43. lotuspod-0.1.0/src/lotuspod/decisions.py +374 -0
  44. lotuspod-0.1.0/src/lotuspod/machine.py +780 -0
  45. lotuspod-0.1.0/src/lotuspod/markdown.py +269 -0
  46. lotuspod-0.1.0/src/lotuspod/media.py +360 -0
  47. lotuspod-0.1.0/src/lotuspod/py.typed +0 -0
  48. lotuspod-0.1.0/src/lotuspod/responder.py +784 -0
  49. lotuspod-0.1.0/src/lotuspod/routing.py +162 -0
  50. lotuspod-0.1.0/src/lotuspod/sections.py +84 -0
  51. lotuspod-0.1.0/src/lotuspod.egg-info/PKG-INFO +123 -0
  52. lotuspod-0.1.0/src/lotuspod.egg-info/SOURCES.txt +96 -0
  53. lotuspod-0.1.0/src/lotuspod.egg-info/dependency_links.txt +1 -0
  54. lotuspod-0.1.0/src/lotuspod.egg-info/entry_points.txt +2 -0
  55. lotuspod-0.1.0/src/lotuspod.egg-info/top_level.txt +1 -0
  56. lotuspod-0.1.0/tests/test_access.py +677 -0
  57. lotuspod-0.1.0/tests/test_agent_pull.py +901 -0
  58. lotuspod-0.1.0/tests/test_agent_pull_witness.py +482 -0
  59. lotuspod-0.1.0/tests/test_answers_witness.py +431 -0
  60. lotuspod-0.1.0/tests/test_api.py +1038 -0
  61. lotuspod-0.1.0/tests/test_backup.py +459 -0
  62. lotuspod-0.1.0/tests/test_backup_witness.py +393 -0
  63. lotuspod-0.1.0/tests/test_browser_checks.py +48 -0
  64. lotuspod-0.1.0/tests/test_capture_site.py +462 -0
  65. lotuspod-0.1.0/tests/test_ci_workflow.py +126 -0
  66. lotuspod-0.1.0/tests/test_claims.py +611 -0
  67. lotuspod-0.1.0/tests/test_cli_surface.py +153 -0
  68. lotuspod-0.1.0/tests/test_comments.py +419 -0
  69. lotuspod-0.1.0/tests/test_decisions.py +599 -0
  70. lotuspod-0.1.0/tests/test_demo_build.py +424 -0
  71. lotuspod-0.1.0/tests/test_demo_checks.py +48 -0
  72. lotuspod-0.1.0/tests/test_demo_deploy.py +235 -0
  73. lotuspod-0.1.0/tests/test_demo_shapes.py +83 -0
  74. lotuspod-0.1.0/tests/test_deploy.py +368 -0
  75. lotuspod-0.1.0/tests/test_docs_links.py +329 -0
  76. lotuspod-0.1.0/tests/test_forms.py +175 -0
  77. lotuspod-0.1.0/tests/test_history.py +89 -0
  78. lotuspod-0.1.0/tests/test_leak_guard.py +180 -0
  79. lotuspod-0.1.0/tests/test_machine.py +438 -0
  80. lotuspod-0.1.0/tests/test_manifest_v2.py +1846 -0
  81. lotuspod-0.1.0/tests/test_markdown.py +590 -0
  82. lotuspod-0.1.0/tests/test_media.py +278 -0
  83. lotuspod-0.1.0/tests/test_page_policy.py +195 -0
  84. lotuspod-0.1.0/tests/test_palette.py +126 -0
  85. lotuspod-0.1.0/tests/test_public_ready.py +132 -0
  86. lotuspod-0.1.0/tests/test_publish.py +581 -0
  87. lotuspod-0.1.0/tests/test_publish_remote.py +561 -0
  88. lotuspod-0.1.0/tests/test_publish_remote_witness.py +191 -0
  89. lotuspod-0.1.0/tests/test_publish_witness.py +281 -0
  90. lotuspod-0.1.0/tests/test_release.py +415 -0
  91. lotuspod-0.1.0/tests/test_render_commits.py +228 -0
  92. lotuspod-0.1.0/tests/test_resolutions.py +212 -0
  93. lotuspod-0.1.0/tests/test_responder.py +718 -0
  94. lotuspod-0.1.0/tests/test_responder_witness.py +483 -0
  95. lotuspod-0.1.0/tests/test_sections.py +222 -0
  96. lotuspod-0.1.0/tests/test_serve_theme.py +122 -0
  97. lotuspod-0.1.0/tests/test_theme_hash.py +136 -0
  98. lotuspod-0.1.0/tests/test_theme_sources.py +217 -0
lotuspod-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Ko Vial
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,123 @@
1
+ Metadata-Version: 2.4
2
+ Name: lotuspod
3
+ Version: 0.1.0
4
+ Summary: Lotuspod: publish pages people comment on, with decision forms and agents that answer.
5
+ License: MIT
6
+ Requires-Python: >=3.9
7
+ Description-Content-Type: text/markdown
8
+ License-File: LICENSE
9
+ Dynamic: license-file
10
+
11
+ # Lotuspod
12
+
13
+ Lotuspod publishes pages for a small, signed-in circle of readers, and lets
14
+ those readers answer on them. A page is written in markdown or HTML and
15
+ published in one command, drawn in one shared lotus theme. Readers comment on
16
+ a section or on a passage they highlight, and answer the decision tables a
17
+ plan page asks the maintainer. Agents (a Claude Code or Codex session, or
18
+ the default responder) pull the comments and answers meant for them, reply in
19
+ the thread, and revise the page when asked.
20
+
21
+ Try the demo: [https://lotuspod.kovial.co/](https://lotuspod.kovial.co/) (your comments stay in your browser; replies are scripted)
22
+
23
+ The site runs on one writer host, behind Cloudflare Access, and is reached
24
+ through a Cloudflare Tunnel or over a tailnet.
25
+
26
+ Lotuspod is a personal project, and issues are off.
27
+
28
+ ## See it
29
+
30
+ A reader comments on a section of a plan page, the thread waits for the
31
+ page's agent, `hermes`, and its reply appears; then the reader answers the
32
+ page's decisions and comments on a passage:
33
+
34
+ ![A reader comments on the Rollout section of a sample plan page; the comments panel shows the thread checking for a reply from hermes, then hermes writing, then hermes's reply; the reader then answers both decisions and comments on a highlighted passage](docs/images/loop.gif)
35
+
36
+ ![The comments panel open beside a sample plan page at 1280 pixels wide: the reader's comment on the Rollout section and the agent hermes's reply below it](docs/images/thread.png)
37
+
38
+ ![The decisions table of a sample plan page with both questions answered and saved, one with a note, each naming the reader who answered](docs/images/decisions.png)
39
+
40
+ ![A passage of a sample plan page highlighted with its number 1, and its thread open in the comments panel waiting for a reply from hermes](docs/images/passage.png)
41
+
42
+ The pictures come from a sample page served on loopback by the test fixture;
43
+ [Development](docs/development.md#captures) makes them again.
44
+
45
+ ## Layout
46
+
47
+ ```
48
+ lotuspod/
49
+ ├── src/lotuspod/ # package + minimal CLI (`lotuspod render|publish|manifest|index|serve|answers|backup|restore`)
50
+ │ ├── _templates/artifact.html # artifact template ({{placeholder}} substitution)
51
+ │ ├── _templates/index.html # index-page template
52
+ │ └── _theme/ # tokens.json (colors, fonts, radii) + lotuspod.css
53
+ ├── artifacts/ # rendered output (gitignored)
54
+ └── lotuspod-media/ # markdown pages' images, beside artifacts/ (gitignored)
55
+ ```
56
+
57
+ Beside these, `docs/` holds the reference pages listed below, `deploy/` the
58
+ tunnel config and the systemd units, `tests/` the unit tests and `e2e/` the
59
+ Playwright captures and browser checks.
60
+
61
+ ## Quick start
62
+
63
+ Install the package, write a markdown page, publish it and serve it:
64
+
65
+ ```sh
66
+ pip install -e .
67
+ printf '# Opening the Pond\n\n## The pond\n\nHello from the pond.\n' > pond.md
68
+ lotuspod publish pond.md --local --summary "Why we started Lotuspod."
69
+ lotuspod serve
70
+ ```
71
+
72
+ `--local` publishes on this machine, even when the config names a
73
+ `[publish] host` to send pages to. `publish` writes `artifacts/pond.html`,
74
+ keeps the source beside it as `artifacts/pond.md`, and rebuilds
75
+ `artifacts/index.html`. `serve` prints the address it listens on; open
76
+ `http://127.0.0.1:8000/` for the index. Serve listens on `127.0.0.1`, this
77
+ machine only, unless `--host` names another address, such as
78
+ `--host "$(tailscale ip -4)"` to serve on a tailnet.
79
+
80
+ Comments and answers need the reader's routes, which sit behind Cloudflare
81
+ Access: until the config has an `[access]` section they answer 503, and the
82
+ page itself is served as usual.
83
+
84
+ ## Where to read next
85
+
86
+ - [Publishing pages](docs/publishing.md): for people publishing pages.
87
+ Markdown and HTML pages, images, the manifest and the index, decision
88
+ tables, the template and the theme, and what a page may run.
89
+ - [Reading and answering pages](docs/comments.md): for readers and the people
90
+ answering them. Answers and comments, the comments panel, the popover and
91
+ bottom sheet, and comments on passages.
92
+ - [Agents](docs/agents.md): for agents and their operators. Agent
93
+ credentials, the pull loop and the default responder.
94
+ - [Operating the site](docs/operating.md): for the operator of the site.
95
+ Serve, Cloudflare Access, publishing through the tunnel, deploying from
96
+ `main`, and backups.
97
+ - [Development](docs/development.md): for people changing Lotuspod. The
98
+ tests and the captures.
99
+ - [Architecture](docs/architecture.md): for reviewers and new contributors.
100
+ One diagram of how the parts fit together.
101
+
102
+ ## How it was built
103
+
104
+ Lotuspod is built by [Holophyte](https://github.com/wevial/holophyte), an
105
+ agent factory. Each change starts as a ticket with acceptance criteria and
106
+ verify commands. An agent implements it in an isolated worktree, the verify
107
+ commands run, and a different agent reviews it. A pull request then runs the
108
+ required `unit` check and merges. The
109
+ [merged pull requests](https://github.com/wevial/lotuspod/pulls?q=is%3Apr+is%3Amerged)
110
+ are the record.
111
+
112
+ ## Security
113
+
114
+ Report a vulnerability privately ([SECURITY.md](SECURITY.md)). `src/lotuspod/access.py` checks Cloudflare
115
+ Access's RS256 assertions with the standard library: the package has no runtime dependencies, the check only
116
+ verifies (never signs or makes keys), and RS256 is one modular exponentiation plus a comparison of the whole
117
+ re-encoded PKCS#1 v1.5 block, leaving no parser to trick. It refuses any `alg` but RS256, a key id not in
118
+ the team's key set, keys under 2048 bits, and a wrong issuer, audience, time or email; it never reads the
119
+ plain email header. Pinned by `tests.test_access.VerifierTests.test_header_must_name_rs256_and_a_listed_key`,
120
+ `tests.test_access.VerifierTests.test_signature_block_is_compared_whole`,
121
+ `tests.test_access.VerifierTests.test_keys_under_2048_bits_are_not_listed`,
122
+ `tests.test_access.VerifierTests.test_malformed_tokens_are_invalid` and
123
+ `tests.test_access.WhoamiTests.test_email_header_naming_someone_else_is_not_read`.
@@ -0,0 +1,113 @@
1
+ # Lotuspod
2
+
3
+ Lotuspod publishes pages for a small, signed-in circle of readers, and lets
4
+ those readers answer on them. A page is written in markdown or HTML and
5
+ published in one command, drawn in one shared lotus theme. Readers comment on
6
+ a section or on a passage they highlight, and answer the decision tables a
7
+ plan page asks the maintainer. Agents (a Claude Code or Codex session, or
8
+ the default responder) pull the comments and answers meant for them, reply in
9
+ the thread, and revise the page when asked.
10
+
11
+ Try the demo: [https://lotuspod.kovial.co/](https://lotuspod.kovial.co/) (your comments stay in your browser; replies are scripted)
12
+
13
+ The site runs on one writer host, behind Cloudflare Access, and is reached
14
+ through a Cloudflare Tunnel or over a tailnet.
15
+
16
+ Lotuspod is a personal project, and issues are off.
17
+
18
+ ## See it
19
+
20
+ A reader comments on a section of a plan page, the thread waits for the
21
+ page's agent, `hermes`, and its reply appears; then the reader answers the
22
+ page's decisions and comments on a passage:
23
+
24
+ ![A reader comments on the Rollout section of a sample plan page; the comments panel shows the thread checking for a reply from hermes, then hermes writing, then hermes's reply; the reader then answers both decisions and comments on a highlighted passage](docs/images/loop.gif)
25
+
26
+ ![The comments panel open beside a sample plan page at 1280 pixels wide: the reader's comment on the Rollout section and the agent hermes's reply below it](docs/images/thread.png)
27
+
28
+ ![The decisions table of a sample plan page with both questions answered and saved, one with a note, each naming the reader who answered](docs/images/decisions.png)
29
+
30
+ ![A passage of a sample plan page highlighted with its number 1, and its thread open in the comments panel waiting for a reply from hermes](docs/images/passage.png)
31
+
32
+ The pictures come from a sample page served on loopback by the test fixture;
33
+ [Development](docs/development.md#captures) makes them again.
34
+
35
+ ## Layout
36
+
37
+ ```
38
+ lotuspod/
39
+ ├── src/lotuspod/ # package + minimal CLI (`lotuspod render|publish|manifest|index|serve|answers|backup|restore`)
40
+ │ ├── _templates/artifact.html # artifact template ({{placeholder}} substitution)
41
+ │ ├── _templates/index.html # index-page template
42
+ │ └── _theme/ # tokens.json (colors, fonts, radii) + lotuspod.css
43
+ ├── artifacts/ # rendered output (gitignored)
44
+ └── lotuspod-media/ # markdown pages' images, beside artifacts/ (gitignored)
45
+ ```
46
+
47
+ Beside these, `docs/` holds the reference pages listed below, `deploy/` the
48
+ tunnel config and the systemd units, `tests/` the unit tests and `e2e/` the
49
+ Playwright captures and browser checks.
50
+
51
+ ## Quick start
52
+
53
+ Install the package, write a markdown page, publish it and serve it:
54
+
55
+ ```sh
56
+ pip install -e .
57
+ printf '# Opening the Pond\n\n## The pond\n\nHello from the pond.\n' > pond.md
58
+ lotuspod publish pond.md --local --summary "Why we started Lotuspod."
59
+ lotuspod serve
60
+ ```
61
+
62
+ `--local` publishes on this machine, even when the config names a
63
+ `[publish] host` to send pages to. `publish` writes `artifacts/pond.html`,
64
+ keeps the source beside it as `artifacts/pond.md`, and rebuilds
65
+ `artifacts/index.html`. `serve` prints the address it listens on; open
66
+ `http://127.0.0.1:8000/` for the index. Serve listens on `127.0.0.1`, this
67
+ machine only, unless `--host` names another address, such as
68
+ `--host "$(tailscale ip -4)"` to serve on a tailnet.
69
+
70
+ Comments and answers need the reader's routes, which sit behind Cloudflare
71
+ Access: until the config has an `[access]` section they answer 503, and the
72
+ page itself is served as usual.
73
+
74
+ ## Where to read next
75
+
76
+ - [Publishing pages](docs/publishing.md): for people publishing pages.
77
+ Markdown and HTML pages, images, the manifest and the index, decision
78
+ tables, the template and the theme, and what a page may run.
79
+ - [Reading and answering pages](docs/comments.md): for readers and the people
80
+ answering them. Answers and comments, the comments panel, the popover and
81
+ bottom sheet, and comments on passages.
82
+ - [Agents](docs/agents.md): for agents and their operators. Agent
83
+ credentials, the pull loop and the default responder.
84
+ - [Operating the site](docs/operating.md): for the operator of the site.
85
+ Serve, Cloudflare Access, publishing through the tunnel, deploying from
86
+ `main`, and backups.
87
+ - [Development](docs/development.md): for people changing Lotuspod. The
88
+ tests and the captures.
89
+ - [Architecture](docs/architecture.md): for reviewers and new contributors.
90
+ One diagram of how the parts fit together.
91
+
92
+ ## How it was built
93
+
94
+ Lotuspod is built by [Holophyte](https://github.com/wevial/holophyte), an
95
+ agent factory. Each change starts as a ticket with acceptance criteria and
96
+ verify commands. An agent implements it in an isolated worktree, the verify
97
+ commands run, and a different agent reviews it. A pull request then runs the
98
+ required `unit` check and merges. The
99
+ [merged pull requests](https://github.com/wevial/lotuspod/pulls?q=is%3Apr+is%3Amerged)
100
+ are the record.
101
+
102
+ ## Security
103
+
104
+ Report a vulnerability privately ([SECURITY.md](SECURITY.md)). `src/lotuspod/access.py` checks Cloudflare
105
+ Access's RS256 assertions with the standard library: the package has no runtime dependencies, the check only
106
+ verifies (never signs or makes keys), and RS256 is one modular exponentiation plus a comparison of the whole
107
+ re-encoded PKCS#1 v1.5 block, leaving no parser to trick. It refuses any `alg` but RS256, a key id not in
108
+ the team's key set, keys under 2048 bits, and a wrong issuer, audience, time or email; it never reads the
109
+ plain email header. Pinned by `tests.test_access.VerifierTests.test_header_must_name_rs256_and_a_listed_key`,
110
+ `tests.test_access.VerifierTests.test_signature_block_is_compared_whole`,
111
+ `tests.test_access.VerifierTests.test_keys_under_2048_bits_are_not_listed`,
112
+ `tests.test_access.VerifierTests.test_malformed_tokens_are_invalid` and
113
+ `tests.test_access.WhoamiTests.test_email_header_naming_someone_else_is_not_read`.
@@ -0,0 +1,20 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "lotuspod"
7
+ version = "0.1.0"
8
+ description = "Lotuspod: publish pages people comment on, with decision forms and agents that answer."
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ license = { text = "MIT" }
12
+
13
+ [project.scripts]
14
+ lotuspod = "lotuspod.cli:main"
15
+
16
+ [tool.setuptools.packages.find]
17
+ where = ["src"]
18
+
19
+ [tool.setuptools.package-data]
20
+ lotuspod = ["py.typed", "_templates/*.html", "_theme/*.json", "_theme/*.svg", "_theme/css/*.css", "_theme/js/*.js"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,3 @@
1
+ """Lotuspod: publish pages people comment on, with decision forms and agents that answer."""
2
+
3
+ __version__ = "0.1.0"
@@ -0,0 +1,6 @@
1
+ """Entry point for ``python -m lotuspod`` (mirrors the ``lotuspod`` console script)."""
2
+
3
+ from lotuspod.cli import main
4
+
5
+ if __name__ == "__main__":
6
+ raise SystemExit(main())
@@ -0,0 +1,59 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta http-equiv="Content-Security-Policy" content="{{policy}}">
6
+ <meta name="viewport" content="width=device-width, initial-scale=1">
7
+ <title>{{title}} · Lotuspod</title>
8
+ <meta name="generator" content="lotuspod theme {{theme_name}}">
9
+ <meta name="lotuspod:visible" content="{{visible}}">
10
+ {{#revision}} <meta name="lotuspod:revision" content="{{revision}}">
11
+ {{/revision}}{{#owner}} <meta name="lotuspod:owner" content="{{owner}}">
12
+ {{/owner}} <link rel="icon" type="image/svg+xml" href="favicon.svg">
13
+ <link rel="stylesheet" href="lotuspod.css?v={{theme_hash}}">
14
+ </head>
15
+ <body>
16
+ <main class="artifact{{variant_class}}" id="top">
17
+ <div class="artifact-topbar">
18
+ <a class="artifact-topbar-brand" href="index.html">Lotuspod</a><span class="artifact-topbar-sep">:</span>
19
+ <a class="artifact-topbar-title" href="#top">{{title}}</a>
20
+ </div>
21
+
22
+ <header class="artifact-header">
23
+ {{#kicker}} <p class="artifact-kicker">{{kicker}}</p>
24
+ {{/kicker}} <h1 class="artifact-title">{{title}}</h1>
25
+ <p class="artifact-meta"><time datetime="{{date}}">{{date}}</time></p>
26
+ {{#owner}} <p class="artifact-owner">Published by {{owner}}</p>
27
+ {{/owner}} {{summary_block}}
28
+ </header>
29
+
30
+ <div class="artifact-main">
31
+ {{#outline}}<nav class="artifact-outline" aria-label="On this page">
32
+ <details class="artifact-outline-disclosure">
33
+ <summary class="artifact-outline-summary">On this page</summary>
34
+ <ol class="artifact-outline-list" role="list">
35
+ {{outline_items}}
36
+ </ol>
37
+ </details>
38
+ </nav>{{/outline}}
39
+
40
+ <section class="artifact-body">
41
+ {{body}}
42
+ </section>
43
+ </div>
44
+
45
+ <footer class="artifact-footer">
46
+ <span aria-hidden="true">&#10047;</span> lotuspod
47
+ </footer>
48
+ </main>
49
+ {{#mermaid}} <script type="module">
50
+ import mermaid from "{{mermaid_dir}}dist/mermaid.esm.min.mjs";
51
+ mermaid.initialize({
52
+ startOnLoad: true,
53
+ theme: "base",
54
+ themeVariables: {{mermaid_theme_variables}}
55
+ });
56
+ </script>
57
+ {{/mermaid}}{{#page_script_needed}} <script src="{{page_script}}?v={{theme_hash}}" defer></script>
58
+ {{/page_script_needed}}</body>
59
+ </html>
@@ -0,0 +1,130 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <title>Lotuspod</title>
7
+ <meta name="generator" content="lotuspod theme {{theme_name}}">
8
+ <link rel="icon" type="image/svg+xml" href="favicon.svg">
9
+ <link rel="stylesheet" href="lotuspod.css?v={{theme_hash}}">
10
+ </head>
11
+ <body>
12
+ <main class="index">
13
+ <header class="index-header">
14
+ <h1 class="index-title">Lotuspod <span class="index-updated">updated <time datetime="{{generated}}">{{generated}}</time></span></h1>
15
+ </header>
16
+
17
+ <section class="index-body">
18
+ <div class="index-controls" hidden>
19
+ <label class="index-search-label" for="index-search">Search</label>
20
+ <input id="index-search" class="index-search" type="search" autocomplete="off"
21
+ spellcheck="false" placeholder="Search pages by title, date or summary">
22
+ <p class="index-count" role="status" aria-live="polite"></p>
23
+ </div>
24
+
25
+ {{entries_block}}
26
+
27
+ <p class="index-empty index-no-match" hidden>Nothing matches that search.</p>
28
+ </section>
29
+
30
+ <footer class="artifact-footer">
31
+ <span aria-hidden="true">&#10047;</span> lotuspod
32
+ </footer>
33
+ </main>
34
+
35
+ <script>
36
+ // Progressive enhancement over a listing that is already complete: the table
37
+ // above stands on its own, and this adds sorting and the search filter on
38
+ // top. The search field ships hidden and is revealed here, so a reader
39
+ // without scripting never sees a control that cannot filter anything.
40
+ (() => {
41
+ const table = document.querySelector(".index-table");
42
+ if (!table) return;
43
+
44
+ const body = table.tBodies[0];
45
+ const headers = Array.from(table.tHead.rows[0].cells);
46
+ // Document order is filename order - the listing's stable base. Sorts run
47
+ // from it rather than from the current DOM order, so a sort is repeatable
48
+ // and equal keys keep falling back to filename order.
49
+ const rows = Array.from(body.rows);
50
+
51
+ const cell = (row, column) => row.cells[column].textContent.trim();
52
+
53
+ const sorted = (column, ascending) => {
54
+ const numeric = headers[column].dataset.sortType === "number";
55
+ const direction = ascending ? 1 : -1;
56
+ return rows.slice().sort((a, b) => {
57
+ const left = cell(a, column);
58
+ const right = cell(b, column);
59
+ // Blanks (no episode number, no date) sink in both directions, so the
60
+ // rows that carry the field stay together at the top either way.
61
+ if (!left || !right) return left ? -1 : right ? 1 : 0;
62
+ if (numeric) {
63
+ const [x, y] = [Number(left), Number(right)];
64
+ if (!Number.isNaN(x) && !Number.isNaN(y)) return (x - y) * direction;
65
+ }
66
+ return left.localeCompare(right, undefined, {
67
+ numeric: true,
68
+ sensitivity: "base",
69
+ }) * direction;
70
+ });
71
+ };
72
+
73
+ let sortColumn = -1;
74
+ let ascending = true;
75
+
76
+ const sortBy = (column) => {
77
+ ascending = column === sortColumn ? !ascending : true;
78
+ sortColumn = column;
79
+ for (const row of sorted(column, ascending)) body.append(row);
80
+ headers.forEach((header, index) => {
81
+ header.setAttribute(
82
+ "aria-sort",
83
+ index !== column ? "none" : ascending ? "ascending" : "descending"
84
+ );
85
+ });
86
+ };
87
+
88
+ // The header label becomes a button so the sort is reachable by keyboard;
89
+ // aria-sort carries the state, and the theme draws the arrow from it.
90
+ headers.forEach((header, index) => {
91
+ const button = document.createElement("button");
92
+ button.type = "button";
93
+ button.className = "sort-button";
94
+ button.textContent = header.textContent;
95
+ button.addEventListener("click", () => sortBy(index));
96
+ header.textContent = "";
97
+ header.append(button);
98
+ header.setAttribute("aria-sort", "none");
99
+ });
100
+
101
+ const controls = document.querySelector(".index-controls");
102
+ const search = document.querySelector(".index-search");
103
+ const count = document.querySelector(".index-count");
104
+ const noMatch = document.querySelector(".index-no-match");
105
+
106
+ // One case-insensitive substring test against the whole row, so a query
107
+ // matches any column without the reader having to pick one.
108
+ const filter = () => {
109
+ const query = search.value.trim().toLowerCase();
110
+ let shown = 0;
111
+ for (const row of rows) {
112
+ const match = !query || row.textContent.toLowerCase().includes(query);
113
+ row.hidden = !match;
114
+ if (match) shown += 1;
115
+ }
116
+ const noun = rows.length === 1 ? "page" : "pages";
117
+ count.textContent =
118
+ shown === rows.length
119
+ ? `${rows.length} ${noun}`
120
+ : `${shown} of ${rows.length} ${noun}`;
121
+ noMatch.hidden = shown > 0;
122
+ };
123
+
124
+ search.addEventListener("input", filter);
125
+ controls.hidden = false;
126
+ filter();
127
+ })();
128
+ </script>
129
+ </body>
130
+ </html>
@@ -0,0 +1,126 @@
1
+ /* Images on a comment (attachments.js). In a composer the images attached
2
+ sit under the field's frame, each a thumbnail with its Remove button,
3
+ beside the "Add image" button, and the status line moves below them.
4
+ Under a comment its images are a row of thumbnails, each a link to the
5
+ full size. A thumbnail's width and height attributes are its drawn size,
6
+ so its box is there before its bytes. Every rule here is one class more
7
+ specific than the composer's button rules in comments.css, which come
8
+ later. */
9
+ :is(.artifact-comment-form, .artifact-comment-reply) .artifact-attach {
10
+ align-items: center;
11
+ display: flex;
12
+ flex-wrap: wrap;
13
+ gap: 0.5rem;
14
+ grid-column: 1 / -1;
15
+ grid-row: 2;
16
+ margin: 0.45rem 0.6rem 0;
17
+ }
18
+
19
+ :is(.artifact-comment-form, .artifact-comment-reply) .artifact-comment-actions .artifact-comment-status {
20
+ grid-row: 3;
21
+ }
22
+
23
+ .artifact-attach .artifact-attach-tray,
24
+ .artifact-comment-col .artifact-comment-images {
25
+ display: flex;
26
+ flex-wrap: wrap;
27
+ gap: 0.5rem;
28
+ list-style: none;
29
+ margin: 0;
30
+ padding: 0;
31
+ }
32
+
33
+ .artifact-attach .artifact-attach-tray[hidden] {
34
+ display: none;
35
+ }
36
+
37
+ .artifact-attach .artifact-attach-item,
38
+ .artifact-comment-images > li {
39
+ margin: 0;
40
+ padding: 0;
41
+ }
42
+
43
+ .artifact-attach .artifact-attach-item {
44
+ align-items: flex-start;
45
+ display: flex;
46
+ flex-direction: column;
47
+ gap: 0.2rem;
48
+ }
49
+
50
+ .artifact-attach-thumb {
51
+ background: var(--color-glow);
52
+ border: 1px solid var(--hairline-strong);
53
+ border-radius: var(--radius-block);
54
+ display: block;
55
+ object-fit: cover;
56
+ }
57
+
58
+ .artifact-attach .artifact-attach-holder {
59
+ align-items: center;
60
+ background: var(--color-glow);
61
+ border: 1px dashed var(--hairline-strong);
62
+ border-radius: var(--radius-block);
63
+ color: var(--color-muted-lavender);
64
+ display: flex;
65
+ font-size: 0.78rem;
66
+ height: 4.5rem;
67
+ justify-content: center;
68
+ width: 4.5rem;
69
+ }
70
+
71
+ :is(.artifact-comment-form, .artifact-comment-reply) .artifact-attach :is(.artifact-attach-add, .artifact-attach-remove) {
72
+ appearance: none;
73
+ background: none;
74
+ border: 1px solid var(--hairline-strong);
75
+ border-radius: var(--radius-pill);
76
+ color: var(--color-lavender);
77
+ cursor: pointer;
78
+ font-family: var(--font-body);
79
+ font-size: 0.8rem;
80
+ height: auto;
81
+ line-height: 1.4;
82
+ margin: 0;
83
+ overflow: visible;
84
+ padding: 0.15rem 0.65rem;
85
+ position: static;
86
+ width: auto;
87
+ }
88
+
89
+ :is(.artifact-comment-form, .artifact-comment-reply) .artifact-attach .artifact-attach-remove {
90
+ border-color: transparent;
91
+ color: var(--color-muted-lavender);
92
+ padding: 0 0.3rem;
93
+ }
94
+
95
+ :is(.artifact-comment-form, .artifact-comment-reply) .artifact-attach :is(.artifact-attach-add, .artifact-attach-remove)::before,
96
+ :is(.artifact-comment-form, .artifact-comment-reply) .artifact-attach :is(.artifact-attach-add, .artifact-attach-remove)::after {
97
+ content: none;
98
+ }
99
+
100
+ :is(.artifact-comment-form, .artifact-comment-reply) .artifact-attach :is(.artifact-attach-add, .artifact-attach-remove):hover {
101
+ background: none;
102
+ color: var(--color-pale-lavender);
103
+ }
104
+
105
+ :is(.artifact-comment-form, .artifact-comment-reply) .artifact-attach :is(.artifact-attach-add, .artifact-attach-remove):disabled {
106
+ cursor: default;
107
+ opacity: 0.6;
108
+ }
109
+
110
+ .artifact-comment-col .artifact-comment-images {
111
+ margin-top: 0.35rem;
112
+ }
113
+
114
+ .artifact-comment-item--reader .artifact-comment-images {
115
+ justify-content: flex-end;
116
+ }
117
+
118
+ .artifact-comment-image {
119
+ border-radius: var(--radius-block);
120
+ display: block;
121
+ }
122
+
123
+ .artifact-comment-image:focus-visible {
124
+ outline: 2px solid var(--color-lavender);
125
+ outline-offset: 2px;
126
+ }