@firedrill-tools/trolley 0.1.1

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 (78) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +219 -0
  3. package/firedrill/agent.target.json +16 -0
  4. package/firedrill/baseline.scenario.json +2109 -0
  5. package/firedrill/conformance.suite.json +19 -0
  6. package/firedrill/partner-outage.scenario.json +11 -0
  7. package/firedrill/processing-response-lost.scenario.json +11 -0
  8. package/firedrill/rate-limited.scenario.json +11 -0
  9. package/firedrill/tight-limits.scenario.json +16 -0
  10. package/firedrill/tools/trolley/app/assets/ATTRIBUTION.md +35 -0
  11. package/firedrill/tools/trolley/app/assets/cropped-Site-Icon-512x512-px-1-192x192.png +0 -0
  12. package/firedrill/tools/trolley/app/assets/fonts/OFL.txt +93 -0
  13. package/firedrill/tools/trolley/app/assets/full-logo.svg +1 -0
  14. package/firedrill/tools/trolley/app/assets/logo-black.svg +18 -0
  15. package/firedrill/tools/trolley/app/site/app.js +139 -0
  16. package/firedrill/tools/trolley/app/site/assets/fonts/inter-latin.woff2 +0 -0
  17. package/firedrill/tools/trolley/app/site/assets/trolley-icon.png +0 -0
  18. package/firedrill/tools/trolley/app/site/assets/trolley-logo.svg +1 -0
  19. package/firedrill/tools/trolley/app/site/base.css +99 -0
  20. package/firedrill/tools/trolley/app/site/batch-actions.js +139 -0
  21. package/firedrill/tools/trolley/app/site/components.css +149 -0
  22. package/firedrill/tools/trolley/app/site/icons.js +50 -0
  23. package/firedrill/tools/trolley/app/site/index.html +53 -0
  24. package/firedrill/tools/trolley/app/site/list.js +87 -0
  25. package/firedrill/tools/trolley/app/site/ui.js +181 -0
  26. package/firedrill/tools/trolley/app/site/view-balances.js +30 -0
  27. package/firedrill/tools/trolley/app/site/view-batch.js +86 -0
  28. package/firedrill/tools/trolley/app/site/view-batches.js +78 -0
  29. package/firedrill/tools/trolley/app/site/view-dashboard.js +79 -0
  30. package/firedrill/tools/trolley/app/site/view-methods.js +80 -0
  31. package/firedrill/tools/trolley/app/site/view-recipient.js +124 -0
  32. package/firedrill/tools/trolley/app/site/view-recipients.js +115 -0
  33. package/firedrill/tools/trolley/behavior.mjs +78 -0
  34. package/firedrill/tools/trolley/lib/access.mjs +21 -0
  35. package/firedrill/tools/trolley/lib/ids.mjs +28 -0
  36. package/firedrill/tools/trolley/lib/money.mjs +55 -0
  37. package/firedrill/tools/trolley/lib/paging.mjs +65 -0
  38. package/firedrill/tools/trolley/lib/pricing.mjs +88 -0
  39. package/firedrill/tools/trolley/lib/records.mjs +87 -0
  40. package/firedrill/tools/trolley/lib/serialize.mjs +152 -0
  41. package/firedrill/tools/trolley/lib/store.mjs +61 -0
  42. package/firedrill/tools/trolley/lib/time.mjs +60 -0
  43. package/firedrill/tools/trolley/lib/validate.mjs +111 -0
  44. package/firedrill/tools/trolley/lib/wire.mjs +145 -0
  45. package/firedrill/tools/trolley/ops/accounts-write.mjs +117 -0
  46. package/firedrill/tools/trolley/ops/accounts.mjs +129 -0
  47. package/firedrill/tools/trolley/ops/balances.mjs +23 -0
  48. package/firedrill/tools/trolley/ops/batches.mjs +139 -0
  49. package/firedrill/tools/trolley/ops/payment-build.mjs +79 -0
  50. package/firedrill/tools/trolley/ops/payments.mjs +112 -0
  51. package/firedrill/tools/trolley/ops/processing.mjs +104 -0
  52. package/firedrill/tools/trolley/ops/recipients-list.mjs +58 -0
  53. package/firedrill/tools/trolley/ops/recipients.mjs +127 -0
  54. package/firedrill/tools/trolley/trolley.tool.json +10095 -0
  55. package/firedrill/trolley-accounts.drill.json +205 -0
  56. package/firedrill/trolley-batch-lifecycle.drill.json +522 -0
  57. package/firedrill/trolley-denied.drill.json +74 -0
  58. package/firedrill/trolley-fresh-install.drill.json +68 -0
  59. package/firedrill/trolley-invalid-key.drill.json +743 -0
  60. package/firedrill/trolley-partner-outage.drill.json +97 -0
  61. package/firedrill/trolley-processing-rules.drill.json +138 -0
  62. package/firedrill/trolley-rate-limited.drill.json +118 -0
  63. package/firedrill/trolley-read-only.drill.json +468 -0
  64. package/firedrill/trolley-recipients.drill.json +183 -0
  65. package/firedrill/trolley-response-lost.drill.json +127 -0
  66. package/firedrill/trolley-tight-limits.drill.json +348 -0
  67. package/firedrill/world.json +2539 -0
  68. package/firedrill.json +5 -0
  69. package/package.json +64 -0
  70. package/starter.json +2108 -0
  71. package/test/conformance.mjs +12 -0
  72. package/test/flow-access.mjs +54 -0
  73. package/test/flow-accounts.mjs +63 -0
  74. package/test/flow-batches.mjs +107 -0
  75. package/test/flow-faults.mjs +58 -0
  76. package/test/flow-processing.mjs +41 -0
  77. package/test/flow-recipients.mjs +93 -0
  78. package/test/harness.mjs +87 -0
@@ -0,0 +1,19 @@
1
+ {
2
+ "schemaVersion": 1,
3
+ "id": "conformance",
4
+ "title": "Trolley Tool conformance: every operation, declared error, event and fault",
5
+ "drills": [
6
+ "trolley-recipients",
7
+ "trolley-accounts",
8
+ "trolley-batch-lifecycle",
9
+ "trolley-processing-rules",
10
+ "trolley-read-only",
11
+ "trolley-invalid-key",
12
+ "trolley-fresh-install",
13
+ "trolley-denied",
14
+ "trolley-rate-limited",
15
+ "trolley-partner-outage",
16
+ "trolley-response-lost",
17
+ "trolley-tight-limits"
18
+ ]
19
+ }
@@ -0,0 +1,11 @@
1
+ {
2
+ "schemaVersion": 1,
3
+ "id": "partner-outage",
4
+ "title": "Payout partner outage: adding a payout account and generating a quote answer HTTP 500 partner_integration_error",
5
+ "faults": [
6
+ {
7
+ "packageId": "trolley",
8
+ "faultId": "payout-partner-outage"
9
+ }
10
+ ]
11
+ }
@@ -0,0 +1,11 @@
1
+ {
2
+ "schemaVersion": 1,
3
+ "id": "processing-response-lost",
4
+ "title": "start-processing settles the batch but the caller receives HTTP 500 internal_server_error",
5
+ "faults": [
6
+ {
7
+ "packageId": "trolley",
8
+ "faultId": "processing-response-lost"
9
+ }
10
+ ]
11
+ }
@@ -0,0 +1,11 @@
1
+ {
2
+ "schemaVersion": 1,
3
+ "id": "rate-limited",
4
+ "title": "recipients.list, batches.list and payments.list answer HTTP 429 rate_limit_exceeded",
5
+ "faults": [
6
+ {
7
+ "packageId": "trolley",
8
+ "faultId": "rate-limited"
9
+ }
10
+ ]
11
+ }
@@ -0,0 +1,16 @@
1
+ {
2
+ "schemaVersion": 1,
3
+ "id": "tight-limits",
4
+ "title": "Scan bound lowered to 2 rows (meta/limits) so every bounded scan over more rows answers internal_server_error instead of truncating",
5
+ "state": [
6
+ {
7
+ "action": "upsert",
8
+ "packageId": "trolley",
9
+ "namespace": "meta",
10
+ "rowId": "limits",
11
+ "value": {
12
+ "maxRows": 2
13
+ }
14
+ }
15
+ ]
16
+ }
@@ -0,0 +1,35 @@
1
+ # Third-party assets used by the Trolley Tool app
2
+
3
+ The browser app is served from `../site/`. This directory keeps the attribution record, the font licence and the
4
+ original downloads. The framework serves only html/css/js/json/image/font files, so this Markdown file and the licence
5
+ text cannot live inside the served root. Served copies are byte-identical to the originals here (renamed only).
6
+
7
+ ## Logos (trademarks of Trolley, Inc.)
8
+
9
+ Trolley is not part of the logos.lndev.me library, so the marks were downloaded from the vendor's own website on
10
+ 2026-09-15 and are used unmodified.
11
+
12
+ | original file | served copy | source | brand version |
13
+ |---|---|---|---|
14
+ | `cropped-Site-Icon-512x512-px-1-192x192.png` | `../site/assets/trolley-icon.png` | https://trolley.com/wp-content/uploads/2023/11/cropped-Site-Icon-512x512-px-1-192x192.png (the site's `<link rel="icon">`) | Current Trolley app icon (white lowercase "t" with the blue underline bar on a near-black tile, published 2023-11). Used in the navigation rail tile and as the favicon |
15
+ | `full-logo.svg` | `../site/assets/trolley-logo.svg` | https://trolley.com/wp-content/uploads/2023/08/full-logo.svg (header logo of https://trolley.com/) | Current "trolley" wordmark (ink `#0C0E12`, underline bar `#0092FF`, published 2023-08). Shown when the rail is expanded |
16
+ | `logo-black.svg` | not served | https://trolley.com/wp-content/uploads/2021/11/logo-black.svg | Earlier (2021) export of the same wordmark with ink `#191819`; kept for provenance only, superseded by `full-logo.svg` |
17
+
18
+ Trolley and the Trolley logo are trademarks of their owner. They identify the simulated service inside a test
19
+ environment only; this package is not affiliated with or endorsed by Trolley.
20
+
21
+ ## Font
22
+
23
+ | file | served copy | source | licence |
24
+ |---|---|---|---|
25
+ | — | `../site/assets/fonts/inter-latin.woff2` | https://fonts.gstatic.com/s/inter/v20/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa1ZL7W0Q5nw.woff2 (Inter v20, latin subset, variable weight 400–700, as served by https://fonts.googleapis.com/css2?family=Inter:wght@400..700) | SIL Open Font License 1.1: `fonts/OFL.txt` (copied from https://raw.githubusercontent.com/google/fonts/main/ofl/inter/OFL.txt) |
26
+
27
+ Copyright 2020 The Inter Project Authors (https://github.com/rsms/inter). Trolley's web properties use the
28
+ proprietary Circular Std typeface, which cannot be bundled; Inter is the closest permissively licensed geometric
29
+ sans with tabular figures.
30
+
31
+ ## Interface glyphs
32
+
33
+ The monochrome glyphs in `../site/icons.js` (rail navigation, payout methods, actions) are original SVG paths drawn
34
+ for this package in the solid, rounded style of the dashboard's rail icons. No third-party icon font or brand asset
35
+ is embedded; PayPal and Venmo are shown by name and generic glyphs, not by their logos.
@@ -0,0 +1,93 @@
1
+ Copyright 2020 The Inter Project Authors (https://github.com/rsms/inter)
2
+
3
+ This Font Software is licensed under the SIL Open Font License, Version 1.1.
4
+ This license is copied below, and is also available with a FAQ at:
5
+ https://scripts.sil.org/OFL
6
+
7
+
8
+ -----------------------------------------------------------
9
+ SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
10
+ -----------------------------------------------------------
11
+
12
+ PREAMBLE
13
+ The goals of the Open Font License (OFL) are to stimulate worldwide
14
+ development of collaborative font projects, to support the font creation
15
+ efforts of academic and linguistic communities, and to provide a free and
16
+ open framework in which fonts may be shared and improved in partnership
17
+ with others.
18
+
19
+ The OFL allows the licensed fonts to be used, studied, modified and
20
+ redistributed freely as long as they are not sold by themselves. The
21
+ fonts, including any derivative works, can be bundled, embedded,
22
+ redistributed and/or sold with any software provided that any reserved
23
+ names are not used by derivative works. The fonts and derivatives,
24
+ however, cannot be released under any other type of license. The
25
+ requirement for fonts to remain under this license does not apply
26
+ to any document created using the fonts or their derivatives.
27
+
28
+ DEFINITIONS
29
+ "Font Software" refers to the set of files released by the Copyright
30
+ Holder(s) under this license and clearly marked as such. This may
31
+ include source files, build scripts and documentation.
32
+
33
+ "Reserved Font Name" refers to any names specified as such after the
34
+ copyright statement(s).
35
+
36
+ "Original Version" refers to the collection of Font Software components as
37
+ distributed by the Copyright Holder(s).
38
+
39
+ "Modified Version" refers to any derivative made by adding to, deleting,
40
+ or substituting -- in part or in whole -- any of the components of the
41
+ Original Version, by changing formats or by porting the Font Software to a
42
+ new environment.
43
+
44
+ "Author" refers to any designer, engineer, programmer, technical
45
+ writer or other person who contributed to the Font Software.
46
+
47
+ PERMISSION & CONDITIONS
48
+ Permission is hereby granted, free of charge, to any person obtaining
49
+ a copy of the Font Software, to use, study, copy, merge, embed, modify,
50
+ redistribute, and sell modified and unmodified copies of the Font
51
+ Software, subject to the following conditions:
52
+
53
+ 1) Neither the Font Software nor any of its individual components,
54
+ in Original or Modified Versions, may be sold by itself.
55
+
56
+ 2) Original or Modified Versions of the Font Software may be bundled,
57
+ redistributed and/or sold with any software, provided that each copy
58
+ contains the above copyright notice and this license. These can be
59
+ included either as stand-alone text files, human-readable headers or
60
+ in the appropriate machine-readable metadata fields within text or
61
+ binary files as long as those fields can be easily viewed by the user.
62
+
63
+ 3) No Modified Version of the Font Software may use the Reserved Font
64
+ Name(s) unless explicit written permission is granted by the corresponding
65
+ Copyright Holder. This restriction only applies to the primary font name as
66
+ presented to the users.
67
+
68
+ 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
69
+ Software shall not be used to promote, endorse or advertise any
70
+ Modified Version, except to acknowledge the contribution(s) of the
71
+ Copyright Holder(s) and the Author(s) or with their explicit written
72
+ permission.
73
+
74
+ 5) The Font Software, modified or unmodified, in part or in whole,
75
+ must be distributed entirely under this license, and must not be
76
+ distributed under any other license. The requirement for fonts to
77
+ remain under this license does not apply to any document created
78
+ using the Font Software.
79
+
80
+ TERMINATION
81
+ This license becomes null and void if any of the above conditions are
82
+ not met.
83
+
84
+ DISCLAIMER
85
+ THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
86
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
87
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
88
+ OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
89
+ COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
90
+ INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
91
+ DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
92
+ FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
93
+ OTHER DEALINGS IN THE FONT SOFTWARE.
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="90" height="28" viewBox="0 0 90 28" fill="none"><g id="logo / full logo" clip-path="url(#clip0_1079_23764)"><path id="Vector" d="M13.1331 24.9659H0V28.0433H13.1331V24.9659Z" fill="#0092FF"></path><path id="Vector_2" d="M11.9198 8.62539H8.23554V15.4303C8.23554 17.2074 9.49251 17.2074 11.9198 17.0774V20.8916C6.15505 21.5418 4.03121 19.8514 4.03121 15.387V8.62539H1.21387V4.59443H4.03121V1.25697L8.23554 0V4.55108H11.9198V8.62539Z" fill="#0C0E12"></path><path id="Vector_3" d="M18.5514 7.36842C19.3316 5.28793 21.282 4.24768 23.4926 4.24768V8.92879C22.2356 8.75542 21.0653 9.01548 20.0251 9.70898C19.0282 10.4025 18.508 11.4861 18.508 13.0898V20.8916H14.3037V4.55109H18.508V7.36842H18.5514Z" fill="#0C0E12"></path><path id="Vector_4" d="M32.6811 21.1517C30.2538 21.1517 28.2167 20.3282 26.5263 18.6811C24.8792 17.0341 24.0557 14.9536 24.0557 12.5263C24.0557 10.0991 24.8792 8.10527 26.5263 6.41487C28.1733 4.76781 30.2538 3.90094 32.6811 3.90094C35.1083 3.90094 37.1454 4.76781 38.7925 6.41487C40.4396 8.06193 41.3064 10.0991 41.3064 12.5263C41.3064 14.9536 40.4396 16.9907 38.7925 18.6811C37.1454 20.3715 35.1083 21.1517 32.6811 21.1517ZM32.6811 17.0774C33.938 17.0774 34.9783 16.644 35.8451 15.8204C36.712 14.9536 37.1021 13.9133 37.1021 12.5697C37.1021 11.2694 36.6687 10.1858 35.8451 9.36224C34.9783 8.49537 33.938 8.10527 32.6811 8.10527C31.4241 8.10527 30.3838 8.53871 29.517 9.36224C28.6934 10.2291 28.26 11.2694 28.26 12.5697C28.26 13.87 28.6934 14.9536 29.517 15.8204C30.3405 16.644 31.4241 17.0774 32.6811 17.0774Z" fill="#0C0E12"></path><path id="Vector_5" d="M47.678 0H43.4736V20.8916H47.678V0Z" fill="#0C0E12"></path><path id="Vector_6" d="M54.3098 0H50.1055V20.8916H54.3098V0Z" fill="#0C0E12"></path><path id="Vector_7" d="M60.7248 14.4335C61.2882 16.4706 62.8053 17.5109 65.3192 17.5109C66.9229 17.5109 68.1365 16.9474 68.9601 15.8638L72.3409 17.8143C70.7372 20.1115 68.3533 21.3251 65.2325 21.3251C62.5452 21.3251 60.378 20.5016 58.731 18.8979C57.0839 17.2508 56.3037 15.2136 56.3037 12.7431C56.3037 10.3158 57.1272 8.23531 58.731 6.6316C60.3347 4.98454 62.4152 4.16101 64.9291 4.16101C67.313 4.16101 69.3068 4.98454 70.8238 6.6316C72.3842 8.27866 73.1644 10.3158 73.1644 12.7431C73.1644 13.3065 73.1211 13.87 72.991 14.4768H60.7248V14.4335ZM69.0034 11.1827C68.5266 8.97216 66.8796 7.88857 64.9724 7.88857C62.7186 7.88857 61.2015 9.10219 60.6814 11.1827H69.0034Z" fill="#0C0E12"></path><path id="Vector_8" d="M85.3004 4.55109H89.8081L83.87 20.8483C82.1796 25.5294 79.449 27.6099 75.2446 27.3932V23.4923C77.4985 23.5356 78.7555 22.582 79.5357 20.3715L72.8174 4.55109H77.4118L81.6595 15.517L85.3004 4.55109Z" fill="#0C0E12"></path><path id="Vector_9" d="M88.5073 21.3251C88.334 21.3251 88.1606 21.2817 87.9872 21.2384C87.8138 21.1517 87.6838 21.065 87.5538 20.9783C87.4237 20.8483 87.3371 20.7183 87.2937 20.5449C87.207 20.3715 87.207 20.1981 87.207 20.0248C87.207 19.8514 87.2504 19.678 87.2937 19.5046C87.3804 19.3313 87.4671 19.2012 87.5538 19.0712C87.6838 18.9412 87.8138 18.8545 87.9872 18.8111C88.1606 18.7245 88.334 18.7245 88.5073 18.7245C88.6807 18.7245 88.8541 18.7678 89.0275 18.8111C89.2008 18.8978 89.3309 18.9845 89.4609 19.0712C89.5909 19.2012 89.6776 19.3313 89.721 19.5046C89.8076 19.678 89.8076 19.8514 89.8076 20.0248C89.8076 20.1981 89.7643 20.3715 89.721 20.5449C89.6343 20.7183 89.5476 20.8483 89.4609 20.9783C89.3309 21.1084 89.2008 21.195 89.0275 21.2384C88.8541 21.2817 88.6807 21.3251 88.5073 21.3251ZM88.5073 21.1084C88.6374 21.1084 88.8107 21.065 88.9408 21.0217C89.0708 20.9783 89.2008 20.8916 89.2875 20.805C89.3742 20.7183 89.4609 20.5882 89.5042 20.4582C89.5476 20.3282 89.5909 20.1981 89.5909 20.0248C89.5909 19.8947 89.5476 19.7214 89.5042 19.5913C89.4609 19.4613 89.3742 19.3313 89.2875 19.2446C89.2008 19.1579 89.0708 19.0712 88.9408 19.0279C88.8107 18.9845 88.6807 18.9412 88.5073 18.9412C88.3773 18.9412 88.2039 18.9845 88.0739 19.0279C87.9439 19.0712 87.8138 19.1579 87.7272 19.2446C87.6405 19.3313 87.5538 19.4613 87.5104 19.5913C87.4671 19.7214 87.4237 19.8514 87.4237 20.0248C87.4237 20.1548 87.4671 20.3282 87.5104 20.4582C87.5538 20.5882 87.6405 20.7183 87.7272 20.805C87.8138 20.8916 87.9439 20.9783 88.0739 21.0217C88.2039 21.065 88.334 21.1084 88.5073 21.1084ZM88.5073 20.7183C88.4206 20.7183 88.334 20.7183 88.2473 20.6749C88.1606 20.6316 88.0739 20.5882 88.0306 20.5449C87.9872 20.5015 87.9005 20.4149 87.9005 20.3282C87.8572 20.2415 87.8572 20.1548 87.8572 20.0681C87.8572 19.9814 87.8572 19.8947 87.9005 19.808C87.9439 19.7214 87.9872 19.6347 88.0306 19.5913C88.0739 19.548 88.1606 19.4613 88.2473 19.4613C88.334 19.418 88.4206 19.418 88.5073 19.418C88.6374 19.418 88.7674 19.4613 88.8541 19.5046C88.9408 19.5913 89.0275 19.6347 89.1141 19.7647C89.0708 19.7647 89.0708 19.808 89.0275 19.808C88.9841 19.808 88.9841 19.8514 88.9408 19.8514C88.8974 19.8514 88.8974 19.8947 88.8541 19.8947C88.8107 19.8514 88.7674 19.7647 88.7241 19.7214C88.6374 19.678 88.594 19.6347 88.5073 19.6347C88.464 19.6347 88.3773 19.6347 88.334 19.678C88.2906 19.7214 88.2473 19.7214 88.2039 19.7647C88.1606 19.808 88.1172 19.8514 88.1172 19.8947C88.0739 19.9381 88.0739 20.0248 88.0739 20.0681C88.0739 20.1115 88.0739 20.1981 88.1172 20.2415C88.1606 20.2848 88.1606 20.3282 88.2039 20.3715C88.2473 20.4149 88.2906 20.4582 88.334 20.4582C88.3773 20.5015 88.464 20.5015 88.5073 20.5015C88.594 20.5015 88.6807 20.4582 88.7241 20.4149C88.8107 20.3715 88.8541 20.3282 88.8541 20.2415L89.1141 20.3715C89.0708 20.5015 88.9841 20.5882 88.8541 20.6316C88.7674 20.6749 88.6374 20.7183 88.5073 20.7183Z" fill="#0C0E12"></path></g><defs><clipPath id="clip0_1079_23764"><rect width="89.8081" height="28" fill="white"></rect></clipPath></defs></svg>
@@ -0,0 +1,18 @@
1
+ <svg width="103" height="32" viewBox="0 0 103 32" fill="none" xmlns="http://www.w3.org/2000/svg">
2
+ <g clip-path="url(#clip0_61:874)">
3
+ <path d="M15.0093 28.5327H0V32.0497H15.0093V28.5327Z" fill="#0092FF"/>
4
+ <path d="M13.6223 9.85759H9.41173V17.6347C9.41173 19.6656 10.8483 19.6656 13.6223 19.517V23.8762C7.03402 24.6192 4.60678 22.6873 4.60678 17.5851V9.85759H1.38696V5.25077H4.60678V1.43653L9.41173 0V5.20124H13.6223V9.85759Z" fill="#191819"/>
5
+ <path d="M21.2012 8.42106C22.0929 6.04335 24.322 4.85449 26.8483 4.85449V10.2043C25.4118 10.0062 24.0743 10.3034 22.8854 11.096C21.7461 11.8885 21.1517 13.1269 21.1517 14.9598V23.8762H16.3467V5.20124H21.1517V8.42106H21.2012Z" fill="#191819"/>
6
+ <path d="M37.3498 24.1732C34.5758 24.1732 32.2477 23.232 30.3158 21.3496C28.4334 19.4673 27.4922 17.0896 27.4922 14.3156C27.4922 11.5416 28.4334 9.26296 30.3158 7.33107C32.1981 5.44872 34.5758 4.45801 37.3498 4.45801C40.1238 4.45801 42.452 5.44872 44.3344 7.33107C46.2167 9.21343 47.2074 11.5416 47.2074 14.3156C47.2074 17.0896 46.2167 19.4178 44.3344 21.3496C42.452 23.2815 40.1238 24.1732 37.3498 24.1732ZM37.3498 19.5168C38.7864 19.5168 39.9752 19.0215 40.9659 18.0803C41.9566 17.0896 42.4025 15.9007 42.4025 14.3651C42.4025 12.8791 41.9071 11.6407 40.9659 10.6995C39.9752 9.70878 38.7864 9.26296 37.3498 9.26296C35.9133 9.26296 34.7244 9.75832 33.7337 10.6995C32.7926 11.6902 32.2972 12.8791 32.2972 14.3651C32.2972 15.8512 32.7926 17.0896 33.7337 18.0803C34.6749 19.0215 35.9133 19.5168 37.3498 19.5168Z" fill="#191819"/>
7
+ <path d="M54.4892 0H49.6842V23.8762H54.4892V0Z" fill="#191819"/>
8
+ <path d="M62.0681 0H57.2632V23.8762H62.0681V0Z" fill="#191819"/>
9
+ <path d="M69.3994 16.4953C70.0433 18.8235 71.7771 20.0123 74.6501 20.0123C76.483 20.0123 77.87 19.3684 78.8111 18.13L82.6749 20.3591C80.8421 22.9845 78.1176 24.3715 74.5511 24.3715C71.4799 24.3715 69.0031 23.4303 67.1207 21.5975C65.2384 19.7151 64.3467 17.3869 64.3467 14.5634C64.3467 11.7894 65.2879 9.41172 67.1207 7.5789C68.9536 5.69655 71.3313 4.75537 74.2043 4.75537C76.9288 4.75537 79.2074 5.69655 80.9412 7.5789C82.7245 9.46125 83.6161 11.7894 83.6161 14.5634C83.6161 15.2074 83.5666 15.8513 83.4179 16.5448H69.3994V16.4953ZM78.8607 12.7801C78.3158 10.2538 76.4334 9.01543 74.2539 9.01543C71.678 9.01543 69.9443 10.4024 69.3498 12.7801H78.8607V12.7801Z" fill="#191819"/>
10
+ <path d="M97.4861 5.20117H102.638L95.8514 23.8266C93.9195 29.1764 90.7988 31.5541 85.9938 31.3064V26.8482C88.5697 26.8978 90.0062 25.808 90.8978 23.2817L83.2198 5.20117H88.4706L93.3251 17.7337L97.4861 5.20117Z" fill="#191819"/>
11
+ <path d="M101.152 24.3715C100.954 24.3715 100.755 24.322 100.557 24.2725C100.359 24.1734 100.211 24.0743 100.062 23.9753C99.9133 23.8267 99.8143 23.6781 99.7647 23.4799C99.6656 23.2818 99.6656 23.0836 99.6656 22.8855C99.6656 22.6873 99.7152 22.4892 99.7647 22.2911C99.8638 22.0929 99.9629 21.9443 100.062 21.7957C100.211 21.6471 100.359 21.548 100.557 21.4985C100.755 21.3994 100.954 21.3994 101.152 21.3994C101.35 21.3994 101.548 21.449 101.746 21.4985C101.944 21.5976 102.093 21.6966 102.241 21.7957C102.39 21.9443 102.489 22.0929 102.539 22.2911C102.638 22.4892 102.638 22.6873 102.638 22.8855C102.638 23.0836 102.588 23.2818 102.539 23.4799C102.44 23.6781 102.341 23.8267 102.241 23.9753C102.093 24.1239 101.944 24.2229 101.746 24.2725C101.548 24.322 101.35 24.3715 101.152 24.3715ZM101.152 24.1239C101.3 24.1239 101.498 24.0743 101.647 24.0248C101.796 23.9753 101.944 23.8762 102.043 23.7771C102.142 23.6781 102.241 23.5294 102.291 23.3808C102.341 23.2322 102.39 23.0836 102.39 22.8855C102.39 22.7369 102.341 22.5387 102.291 22.3901C102.241 22.2415 102.142 22.0929 102.043 21.9938C101.944 21.8948 101.796 21.7957 101.647 21.7462C101.498 21.6966 101.35 21.6471 101.152 21.6471C101.003 21.6471 100.805 21.6966 100.656 21.7462C100.508 21.7957 100.359 21.8948 100.26 21.9938C100.161 22.0929 100.062 22.2415 100.012 22.3901C99.9629 22.5387 99.9133 22.6873 99.9133 22.8855C99.9133 23.0341 99.9629 23.2322 100.012 23.3808C100.062 23.5294 100.161 23.6781 100.26 23.7771C100.359 23.8762 100.508 23.9753 100.656 24.0248C100.805 24.0743 100.954 24.1239 101.152 24.1239ZM101.152 23.6781C101.053 23.6781 100.954 23.6781 100.855 23.6285C100.755 23.579 100.656 23.5294 100.607 23.4799C100.557 23.4304 100.458 23.3313 100.458 23.2322C100.409 23.1332 100.409 23.0341 100.409 22.935C100.409 22.8359 100.409 22.7369 100.458 22.6378C100.508 22.5387 100.557 22.4397 100.607 22.3901C100.656 22.3406 100.755 22.2415 100.855 22.2415C100.954 22.192 101.053 22.192 101.152 22.192C101.3 22.192 101.449 22.2415 101.548 22.2911C101.647 22.3901 101.746 22.4397 101.845 22.5883C101.796 22.5883 101.796 22.6378 101.746 22.6378C101.697 22.6378 101.697 22.6873 101.647 22.6873C101.598 22.6873 101.598 22.7369 101.548 22.7369C101.498 22.6873 101.449 22.5883 101.399 22.5387C101.3 22.4892 101.251 22.4397 101.152 22.4397C101.102 22.4397 101.003 22.4397 100.954 22.4892C100.904 22.5387 100.854 22.5387 100.805 22.5883C100.755 22.6378 100.706 22.6873 100.706 22.7369C100.656 22.7864 100.656 22.8855 100.656 22.935C100.656 22.9846 100.656 23.0836 100.706 23.1332C100.755 23.1827 100.755 23.2322 100.805 23.2818C100.854 23.3313 100.904 23.3808 100.954 23.3808C101.003 23.4304 101.102 23.4304 101.152 23.4304C101.251 23.4304 101.35 23.3808 101.399 23.3313C101.498 23.2818 101.548 23.2322 101.548 23.1332L101.845 23.2818C101.796 23.4304 101.697 23.5294 101.548 23.579C101.449 23.6285 101.3 23.6781 101.152 23.6781Z" fill="#191819"/>
12
+ </g>
13
+ <defs>
14
+ <clipPath id="clip0_61:874">
15
+ <rect width="102.638" height="32" fill="white"/>
16
+ </clipPath>
17
+ </defs>
18
+ </svg>
@@ -0,0 +1,139 @@
1
+ // Trolley Tool app shell: rail navigation, top bar, hash routing and world-revision refresh.
2
+ // Every screen reads and writes through the Tool's operations; nothing here is authoritative data.
3
+ import { hydrateIcons } from "./icons.js";
4
+ import { $, call, el, getContext, openModal, toast, watchWorld } from "./ui.js";
5
+ import { renderDashboard } from "./view-dashboard.js";
6
+ import { renderRecipients } from "./view-recipients.js";
7
+ import { renderRecipient } from "./view-recipient.js";
8
+ import { renderBatches } from "./view-batches.js";
9
+ import { renderBatch } from "./view-batch.js";
10
+ import { renderBalances } from "./view-balances.js";
11
+
12
+ export const app = { now: undefined, editing: 0, token: 0 };
13
+
14
+ function parseRoute() {
15
+ const raw = location.hash.replace(/^#\/?/, "");
16
+ const [path, query = ""] = raw.split("?");
17
+ const [page = "", id] = path.split("/").filter(Boolean);
18
+ return { page: page || "dashboard", id: id ? decodeSafe(id) : undefined, params: Object.fromEntries(new URLSearchParams(query)) };
19
+ }
20
+ function decodeSafe(value) {
21
+ try { return decodeURIComponent(value); } catch { return value; }
22
+ }
23
+ export function go(page, id, params = {}) {
24
+ const query = new URLSearchParams(Object.entries(params).filter(([, v]) => v !== undefined && v !== "" && v !== null)).toString();
25
+ const hash = `#/${page === "dashboard" ? "" : page}${id ? `/${encodeURIComponent(id)}` : ""}${query ? `?${query}` : ""}`;
26
+ if (location.hash === hash) void render(); else location.hash = hash;
27
+ }
28
+ /** Replace the query without adding history or re-rendering (list filters). */
29
+ export function setParams(page, params) {
30
+ const query = new URLSearchParams(Object.entries(params).filter(([, v]) => v !== undefined && v !== "" && v !== null)).toString();
31
+ history.replaceState(null, "", `#/${page}${query ? `?${query}` : ""}`);
32
+ }
33
+ export function setTitle(title) {
34
+ $("#page-title").textContent = title;
35
+ document.title = `${title} | Trolley (synthetic)`;
36
+ }
37
+
38
+ const VIEWS = {
39
+ dashboard: [renderDashboard, "dashboard"],
40
+ recipients: [renderRecipients, "recipients"],
41
+ payments: [renderBatches, "payments"],
42
+ balances: [renderBalances, "balances"],
43
+ };
44
+
45
+ export async function refreshNow() {
46
+ try {
47
+ const value = await call("balances.list", {});
48
+ app.now = value.serverTime;
49
+ return value;
50
+ } catch {
51
+ return undefined;
52
+ }
53
+ }
54
+
55
+ async function render() {
56
+ const route = parseRoute();
57
+ const host = $("#page");
58
+ const token = (app.token += 1);
59
+ app.editing = 0;
60
+ let view;
61
+ let nav = route.page;
62
+ if (route.page === "recipients" && route.id) view = renderRecipient;
63
+ else if (route.page === "payments" && route.id) view = renderBatch;
64
+ else if (Object.hasOwn(VIEWS, route.page)) view = VIEWS[route.page][0];
65
+ for (const item of document.querySelectorAll(".rail-item[data-route]")) item.classList.toggle("active", item.dataset.route === nav);
66
+ closeRail();
67
+ if (!view) {
68
+ setTitle("Not found");
69
+ host.replaceChildren(el("div", { class: "card state" }, [el("h3", { text: "Page not found" }), el("a", { text: "Back to Overview", attrs: { href: "#/" } })]));
70
+ return;
71
+ }
72
+ await view(host, route, { isCurrent: () => token === app.token });
73
+ }
74
+
75
+ export function notSimulated(name) {
76
+ openModal({
77
+ title: name,
78
+ body: [
79
+ el("p", { text: `${name} is part of the Trolley dashboard, but it is not simulated by this Firedrill Tool.` }),
80
+ el("p", { text: "This synthetic merchant covers recipients, payout methods, payment batches, payments and balances. No data is shown here so nothing is invented." }),
81
+ ],
82
+ });
83
+ }
84
+
85
+ function closeRail() {
86
+ if (window.matchMedia("(max-width: 760px)").matches) {
87
+ $("#shell").classList.remove("open");
88
+ $("#menu-toggle").setAttribute("aria-expanded", "false");
89
+ }
90
+ }
91
+
92
+ function accountMenu(anchor, actorId) {
93
+ document.querySelector(".menu")?.remove();
94
+ const rect = anchor.getBoundingClientRect();
95
+ const menu = el("div", { class: "menu", attrs: { role: "menu" } }, [
96
+ el("div", { class: "menu-note", text: `Signed in as synthetic actor ${actorId}. Firedrill sandbox merchant: no money moves.` }),
97
+ el("button", { class: "menu-item", text: "Account settings", attrs: { type: "button", role: "menuitem" }, on: { click: () => { menu.remove(); notSimulated("Account settings"); } } }),
98
+ el("button", { class: "menu-item", text: "Switch to live mode", attrs: { type: "button", role: "menuitem" }, on: { click: () => { menu.remove(); notSimulated("Live mode"); } } }),
99
+ ]);
100
+ menu.style.top = `${rect.bottom + window.scrollY + 6}px`;
101
+ menu.style.right = `${Math.max(8, window.innerWidth - rect.right)}px`;
102
+ document.body.append(menu);
103
+ anchor.setAttribute("aria-expanded", "true");
104
+ const dismiss = (event) => {
105
+ if (event.type === "keydown" && event.key !== "Escape") return;
106
+ if (event.type === "mousedown" && menu.contains(event.target)) return;
107
+ menu.remove();
108
+ anchor.setAttribute("aria-expanded", "false");
109
+ document.removeEventListener("mousedown", dismiss);
110
+ document.removeEventListener("keydown", dismiss);
111
+ };
112
+ setTimeout(() => { document.addEventListener("mousedown", dismiss); document.addEventListener("keydown", dismiss); });
113
+ menu.querySelector("button")?.focus();
114
+ }
115
+
116
+ async function boot() {
117
+ hydrateIcons();
118
+ $("#menu-toggle").addEventListener("click", () => {
119
+ const open = $("#shell").classList.toggle("open");
120
+ $("#menu-toggle").setAttribute("aria-expanded", String(open));
121
+ });
122
+ for (const node of document.querySelectorAll("[data-not-simulated]")) node.addEventListener("click", () => notSimulated(node.dataset.notSimulated));
123
+ let actorId = "actor";
124
+ try {
125
+ const context = await getContext();
126
+ actorId = context.actorId;
127
+ } catch (error) {
128
+ toast(error.message, { error: true, timeout: 0 });
129
+ }
130
+ $("#whoami-name").textContent = actorId;
131
+ $("#avatar").textContent = actorId.slice(0, 1).toUpperCase();
132
+ $("#avatar").addEventListener("click", (event) => accountMenu(event.currentTarget, actorId));
133
+ await refreshNow();
134
+ window.addEventListener("hashchange", () => void render());
135
+ await render();
136
+ watchWorld(async () => { await refreshNow(); await render(); }, () => app.editing === 0 && !document.querySelector(".overlay"));
137
+ }
138
+
139
+ void boot();
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="90" height="28" viewBox="0 0 90 28" fill="none"><g id="logo / full logo" clip-path="url(#clip0_1079_23764)"><path id="Vector" d="M13.1331 24.9659H0V28.0433H13.1331V24.9659Z" fill="#0092FF"></path><path id="Vector_2" d="M11.9198 8.62539H8.23554V15.4303C8.23554 17.2074 9.49251 17.2074 11.9198 17.0774V20.8916C6.15505 21.5418 4.03121 19.8514 4.03121 15.387V8.62539H1.21387V4.59443H4.03121V1.25697L8.23554 0V4.55108H11.9198V8.62539Z" fill="#0C0E12"></path><path id="Vector_3" d="M18.5514 7.36842C19.3316 5.28793 21.282 4.24768 23.4926 4.24768V8.92879C22.2356 8.75542 21.0653 9.01548 20.0251 9.70898C19.0282 10.4025 18.508 11.4861 18.508 13.0898V20.8916H14.3037V4.55109H18.508V7.36842H18.5514Z" fill="#0C0E12"></path><path id="Vector_4" d="M32.6811 21.1517C30.2538 21.1517 28.2167 20.3282 26.5263 18.6811C24.8792 17.0341 24.0557 14.9536 24.0557 12.5263C24.0557 10.0991 24.8792 8.10527 26.5263 6.41487C28.1733 4.76781 30.2538 3.90094 32.6811 3.90094C35.1083 3.90094 37.1454 4.76781 38.7925 6.41487C40.4396 8.06193 41.3064 10.0991 41.3064 12.5263C41.3064 14.9536 40.4396 16.9907 38.7925 18.6811C37.1454 20.3715 35.1083 21.1517 32.6811 21.1517ZM32.6811 17.0774C33.938 17.0774 34.9783 16.644 35.8451 15.8204C36.712 14.9536 37.1021 13.9133 37.1021 12.5697C37.1021 11.2694 36.6687 10.1858 35.8451 9.36224C34.9783 8.49537 33.938 8.10527 32.6811 8.10527C31.4241 8.10527 30.3838 8.53871 29.517 9.36224C28.6934 10.2291 28.26 11.2694 28.26 12.5697C28.26 13.87 28.6934 14.9536 29.517 15.8204C30.3405 16.644 31.4241 17.0774 32.6811 17.0774Z" fill="#0C0E12"></path><path id="Vector_5" d="M47.678 0H43.4736V20.8916H47.678V0Z" fill="#0C0E12"></path><path id="Vector_6" d="M54.3098 0H50.1055V20.8916H54.3098V0Z" fill="#0C0E12"></path><path id="Vector_7" d="M60.7248 14.4335C61.2882 16.4706 62.8053 17.5109 65.3192 17.5109C66.9229 17.5109 68.1365 16.9474 68.9601 15.8638L72.3409 17.8143C70.7372 20.1115 68.3533 21.3251 65.2325 21.3251C62.5452 21.3251 60.378 20.5016 58.731 18.8979C57.0839 17.2508 56.3037 15.2136 56.3037 12.7431C56.3037 10.3158 57.1272 8.23531 58.731 6.6316C60.3347 4.98454 62.4152 4.16101 64.9291 4.16101C67.313 4.16101 69.3068 4.98454 70.8238 6.6316C72.3842 8.27866 73.1644 10.3158 73.1644 12.7431C73.1644 13.3065 73.1211 13.87 72.991 14.4768H60.7248V14.4335ZM69.0034 11.1827C68.5266 8.97216 66.8796 7.88857 64.9724 7.88857C62.7186 7.88857 61.2015 9.10219 60.6814 11.1827H69.0034Z" fill="#0C0E12"></path><path id="Vector_8" d="M85.3004 4.55109H89.8081L83.87 20.8483C82.1796 25.5294 79.449 27.6099 75.2446 27.3932V23.4923C77.4985 23.5356 78.7555 22.582 79.5357 20.3715L72.8174 4.55109H77.4118L81.6595 15.517L85.3004 4.55109Z" fill="#0C0E12"></path><path id="Vector_9" d="M88.5073 21.3251C88.334 21.3251 88.1606 21.2817 87.9872 21.2384C87.8138 21.1517 87.6838 21.065 87.5538 20.9783C87.4237 20.8483 87.3371 20.7183 87.2937 20.5449C87.207 20.3715 87.207 20.1981 87.207 20.0248C87.207 19.8514 87.2504 19.678 87.2937 19.5046C87.3804 19.3313 87.4671 19.2012 87.5538 19.0712C87.6838 18.9412 87.8138 18.8545 87.9872 18.8111C88.1606 18.7245 88.334 18.7245 88.5073 18.7245C88.6807 18.7245 88.8541 18.7678 89.0275 18.8111C89.2008 18.8978 89.3309 18.9845 89.4609 19.0712C89.5909 19.2012 89.6776 19.3313 89.721 19.5046C89.8076 19.678 89.8076 19.8514 89.8076 20.0248C89.8076 20.1981 89.7643 20.3715 89.721 20.5449C89.6343 20.7183 89.5476 20.8483 89.4609 20.9783C89.3309 21.1084 89.2008 21.195 89.0275 21.2384C88.8541 21.2817 88.6807 21.3251 88.5073 21.3251ZM88.5073 21.1084C88.6374 21.1084 88.8107 21.065 88.9408 21.0217C89.0708 20.9783 89.2008 20.8916 89.2875 20.805C89.3742 20.7183 89.4609 20.5882 89.5042 20.4582C89.5476 20.3282 89.5909 20.1981 89.5909 20.0248C89.5909 19.8947 89.5476 19.7214 89.5042 19.5913C89.4609 19.4613 89.3742 19.3313 89.2875 19.2446C89.2008 19.1579 89.0708 19.0712 88.9408 19.0279C88.8107 18.9845 88.6807 18.9412 88.5073 18.9412C88.3773 18.9412 88.2039 18.9845 88.0739 19.0279C87.9439 19.0712 87.8138 19.1579 87.7272 19.2446C87.6405 19.3313 87.5538 19.4613 87.5104 19.5913C87.4671 19.7214 87.4237 19.8514 87.4237 20.0248C87.4237 20.1548 87.4671 20.3282 87.5104 20.4582C87.5538 20.5882 87.6405 20.7183 87.7272 20.805C87.8138 20.8916 87.9439 20.9783 88.0739 21.0217C88.2039 21.065 88.334 21.1084 88.5073 21.1084ZM88.5073 20.7183C88.4206 20.7183 88.334 20.7183 88.2473 20.6749C88.1606 20.6316 88.0739 20.5882 88.0306 20.5449C87.9872 20.5015 87.9005 20.4149 87.9005 20.3282C87.8572 20.2415 87.8572 20.1548 87.8572 20.0681C87.8572 19.9814 87.8572 19.8947 87.9005 19.808C87.9439 19.7214 87.9872 19.6347 88.0306 19.5913C88.0739 19.548 88.1606 19.4613 88.2473 19.4613C88.334 19.418 88.4206 19.418 88.5073 19.418C88.6374 19.418 88.7674 19.4613 88.8541 19.5046C88.9408 19.5913 89.0275 19.6347 89.1141 19.7647C89.0708 19.7647 89.0708 19.808 89.0275 19.808C88.9841 19.808 88.9841 19.8514 88.9408 19.8514C88.8974 19.8514 88.8974 19.8947 88.8541 19.8947C88.8107 19.8514 88.7674 19.7647 88.7241 19.7214C88.6374 19.678 88.594 19.6347 88.5073 19.6347C88.464 19.6347 88.3773 19.6347 88.334 19.678C88.2906 19.7214 88.2473 19.7214 88.2039 19.7647C88.1606 19.808 88.1172 19.8514 88.1172 19.8947C88.0739 19.9381 88.0739 20.0248 88.0739 20.0681C88.0739 20.1115 88.0739 20.1981 88.1172 20.2415C88.1606 20.2848 88.1606 20.3282 88.2039 20.3715C88.2473 20.4149 88.2906 20.4582 88.334 20.4582C88.3773 20.5015 88.464 20.5015 88.5073 20.5015C88.594 20.5015 88.6807 20.4582 88.7241 20.4149C88.8107 20.3715 88.8541 20.3282 88.8541 20.2415L89.1141 20.3715C89.0708 20.5015 88.9841 20.5882 88.8541 20.6316C88.7674 20.6749 88.6374 20.7183 88.5073 20.7183Z" fill="#0C0E12"></path></g><defs><clipPath id="clip0_1079_23764"><rect width="89.8081" height="28" fill="white"></rect></clipPath></defs></svg>
@@ -0,0 +1,99 @@
1
+ @font-face {
2
+ font-family: "Inter";
3
+ font-style: normal;
4
+ font-weight: 400 700;
5
+ font-display: swap;
6
+ src: url("./assets/fonts/inter-latin.woff2") format("woff2");
7
+ }
8
+ :root {
9
+ --bg: #f5f5f5;
10
+ --card: #ffffff;
11
+ --ink: #0c0e12;
12
+ --ink-2: #474647;
13
+ --muted: #6d7381;
14
+ --line: #e4e4e4;
15
+ --line-2: #c2c6ce;
16
+ --blue: #0092ff;
17
+ --blue-strong: #0064ff;
18
+ --blue-soft: #e5f4ff;
19
+ --green: #1f8a4c;
20
+ --green-soft: #dcf2e3;
21
+ --red: #d93f3f;
22
+ --red-soft: #fde6e6;
23
+ --amber: #a86400;
24
+ --amber-soft: #fff1d6;
25
+ --grey-soft: #eeeeef;
26
+ --radius: 12px;
27
+ --shadow: 0 1px 2px rgba(12, 14, 18, 0.06);
28
+ --rail: 76px;
29
+ --rail-open: 232px;
30
+ }
31
+ * { box-sizing: border-box; }
32
+ html, body { margin: 0; height: 100%; }
33
+ body {
34
+ font-family: "Inter", -apple-system, "Segoe UI", Helvetica, Arial, sans-serif;
35
+ font-size: 14px;
36
+ color: var(--ink);
37
+ background: var(--bg);
38
+ -webkit-font-smoothing: antialiased;
39
+ }
40
+ button, input, select, textarea { font: inherit; color: inherit; }
41
+ a { color: var(--blue-strong); }
42
+ .skip-link { position: absolute; left: -999px; top: 8px; background: #fff; padding: 8px; z-index: 50; }
43
+ .skip-link:focus { left: 8px; }
44
+ :focus-visible { outline: 2px solid var(--blue); outline-offset: 2px; }
45
+
46
+ .shell { display: flex; height: 100vh; }
47
+ .rail {
48
+ width: var(--rail); flex: none; background: var(--card); border-right: 1px solid var(--line);
49
+ display: flex; flex-direction: column; align-items: stretch; transition: width 0.15s ease; overflow: hidden;
50
+ }
51
+ .shell.open .rail { width: var(--rail-open); }
52
+ .rail-brand { display: flex; align-items: center; gap: 12px; height: 88px; padding: 0 18px; border-bottom: 1px solid var(--line); text-decoration: none; }
53
+ .rail-tile { width: 40px; height: 40px; border-radius: 9px; flex: none; }
54
+ .rail-wordmark { display: none; height: 24px; width: auto; }
55
+ .shell.open .rail-wordmark { display: block; }
56
+ .rail-nav { display: flex; flex-direction: column; gap: 6px; padding: 18px 14px; flex: 1; }
57
+ .rail-bottom { display: flex; flex-direction: column; gap: 6px; padding: 14px; border-top: 1px solid var(--line); }
58
+ .rail-item {
59
+ position: relative; display: flex; align-items: center; gap: 14px; height: 44px; padding: 0 12px;
60
+ border: 0; background: none; border-radius: 10px; color: #7d8290; text-decoration: none; cursor: pointer; white-space: nowrap;
61
+ }
62
+ .rail-item svg { width: 22px; height: 22px; flex: none; }
63
+ .rail-item:hover { background: var(--grey-soft); color: var(--ink); }
64
+ .rail-item.active { background: var(--blue-soft); color: var(--ink); }
65
+ .rail-label { display: none; font-weight: 500; font-size: 14px; }
66
+ .shell.open .rail-label { display: inline; }
67
+ .shell:not(.open) .rail-item:hover::after {
68
+ content: attr(data-tip); position: absolute; left: 56px; top: 50%; transform: translateY(-50%);
69
+ background: var(--ink); color: #fff; font-size: 12px; padding: 5px 8px; border-radius: 6px; z-index: 40; pointer-events: none;
70
+ }
71
+ .shell:not(.open) .rail { overflow: visible; }
72
+
73
+ .main { flex: 1; min-width: 0; display: flex; flex-direction: column; }
74
+ .topbar { height: 88px; flex: none; display: flex; align-items: center; gap: 20px; padding: 0 32px 0 24px; background: var(--bg); }
75
+ .hamburger { border: 0; background: none; color: #9a9ea8; cursor: pointer; padding: 6px; border-radius: 8px; }
76
+ .hamburger svg { width: 28px; height: 28px; }
77
+ .hamburger:hover { background: var(--grey-soft); color: var(--ink); }
78
+ .page-title { font-size: 26px; font-weight: 400; color: var(--ink-2); margin: 0; letter-spacing: -0.2px; }
79
+ .topbar-right { margin-left: auto; display: flex; align-items: center; gap: 18px; }
80
+ .bell { width: 44px; height: 44px; border-radius: 10px; border: 1px solid var(--line); background: var(--card); cursor: pointer; display: grid; place-items: center; box-shadow: var(--shadow); }
81
+ .bell svg { width: 18px; height: 18px; }
82
+ .bell:hover { border-color: var(--line-2); }
83
+ .whoami { display: flex; flex-direction: column; align-items: flex-start; gap: 4px; }
84
+ .whoami-name { font-size: 16px; font-weight: 500; }
85
+ .mode-pill { font-size: 11px; font-weight: 600; letter-spacing: 0.4px; color: var(--amber); background: var(--amber-soft); border-radius: 6px; padding: 3px 8px; }
86
+ .avatar-btn { width: 44px; height: 44px; border-radius: 10px; border: 0; background: #e9e9ea; color: var(--ink-2); font-size: 18px; cursor: pointer; }
87
+ .avatar-btn:hover { background: #dedee0; }
88
+ .page { flex: 1; overflow: auto; padding: 4px 32px 40px; outline: none; }
89
+
90
+ @media (max-width: 760px) {
91
+ .rail { position: fixed; z-index: 30; height: 100vh; width: 0; border: 0; }
92
+ .shell.open .rail { width: var(--rail-open); border-right: 1px solid var(--line); }
93
+ .shell:not(.open) .rail { overflow: hidden; }
94
+ .shell.open .rail { box-shadow: 0 10px 40px rgba(0, 0, 0, 0.2); }
95
+ .topbar { padding: 0 12px; height: 64px; gap: 8px; }
96
+ .page-title { font-size: 20px; }
97
+ .whoami { display: none; }
98
+ .page { padding: 4px 12px 32px; }
99
+ }