Move Memoria off the frozen rastrillo path
Memoria imported rastrillo from github.com/carlosframework/rastrillo, which stops at v0.23.0, so it had missed every rastrillo fix since 2026-08-31, among them tokens.css's 16px text controls on touch screens that stop iOS zooming on tap. - Every import is now amadan.net/rastrillo/rastrillo at 29e4491, the version home/connect already requires, so the build has one amadan rastrillo, not two. - The CLI is a go.mod tool (go tool rastrillo), make migration-check runs it, and make release stamps amadan.net/rastrillo/rastrillo .BuildVersion; the old symbol would have left /api/version on "dev". - One old-path import stays: native.go builds a second sessions handle (same table, cookie and origin) for rastrillo/native's server, which still takes the old type. The app keeps reading sessions through a.sess, the new package. TestNoPreMoveRastrillo forbids every other old-path import in the repository. - go tool rastrillo doctor --fix re-copied tokens.css. It also wrote the old combined shim over rastrillo.js and added datetime.js and calendar.js: writeestilo restored estilo's split shim (TestBusyFilesAreEstilos), and the two new scripts are deleted, with select.js, which no page links either. TestNoUnusedRastrilloScripts- OnDisk fails if any comes back. The design-system gate read select.js as a hook source, and that alone was what passed index.html's class="rst-sr-only"; rst-sr-only is now exempt by name, with why. - estilo pinned at main 530cb93 (the bar title's touch size, door copy nits, the danger menu row); writeestilo re-run. Credits regenerated.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
42 files changed,
+530
−357
AGENTS.md+22 −0Makefile+8 −7cmd/memoria/main.go+3 −3go.mod+2 −2go.sum+2 −2internal/memoria/app.go+4 −4internal/memoria/auth.go+2 −2internal/memoria/chrome.go+1 −1internal/memoria/credits/notices.json+4 −0internal/memoria/custody_test.go+2 −2internal/memoria/errors.go+1 −1internal/memoria/handlers.go+4 −4internal/memoria/home.go+3 −3internal/memoria/home_identity.go+1 −1internal/memoria/home_identity_test.go+2 −2internal/memoria/home_migrations.go+1 −1internal/memoria/home_router_test.go+1 −1internal/memoria/home_security_test.go+1 −1internal/memoria/lag_test.go+1 −1internal/memoria/migrations.go+3 −3internal/memoria/native.go+19 −3internal/memoria/render.go+2 −2internal/memoria/static/chrome.css+8 −0internal/memoria/static/menu.css+4 −0internal/memoria/static/search.js+2 −3internal/memoria/static/select.js+0 −280internal/memoria/static/tokens.css+303 −6internal/memoria/store.go+1 −1internal/memoriatest/admission_test.go+1 −1internal/memoriatest/chrome_client_test.go+1 −1internal/memoriatest/connector_test.go+1 −1internal/memoriatest/footer_model_test.go+1 −1internal/memoriatest/gates_test.go+7 −1internal/memoriatest/harness_test.go+1 −1internal/memoriatest/rastrillo_path_test.go+77 −0internal/memoriatest/signin_keymail_test.go+1 −1internal/memoriatest/signindial_test.go+1 −1internal/memoriatest/vendored_test.go+21 −2internal/memoriatest/zoom_browser_test.go+2 −2tools/drive/main.go+1 −1tools/seed/main.go+2 −2tools/vendorassets/main.go+6 −6
diff --git a/AGENTS.md b/AGENTS.md| index 5d0ef31..5b81084 100644 |
| --- a/AGENTS.md |
| +++ b/AGENTS.md |
| @@ -173,6 +173,28 @@ so; it never renders as a confident zero. |
| **If you add a section to a page, add it to drive too** — the section |
| that is not seeded is the section nobody sees. |
| +**rastrillo** |
| +- **rastrillo is `amadan.net/rastrillo/rastrillo`.** The old path, |
| + `github.com/carlosframework/rastrillo`, stops at v0.23.0: code importing |
| + it gets no fix, and its own checks cannot tell (Memoria zoomed iOS on |
| + every tap for a month that way). `TestNoPreMoveRastrillo` allows one |
| + import of it, native.go's second sessions handle for rastrillo/native's |
| + server, until that server moves; `native_exchange_test.go` proves the |
| + bridge. The app itself reads sessions through `a.sess`, the new |
| + package. The CLI is a go.mod `tool`: `go tool rastrillo …`, never a |
| + `rastrillo` from PATH. |
| +- **After `go tool rastrillo doctor --fix`, run `go run |
| + ./scripts/writeestilo` and delete what it added.** At 29e4491 --fix |
| + overwrites rastrillo.js with the old shim (the old busy rule inside, |
| + which fights estilo's busy.js) and adds select.js, datetime.js and |
| + calendar.js, which no page links. `TestBusyFilesAreEstilos` and |
| + `TestNoUnusedRastrilloScriptsOnDisk` fail until both are done. |
| +- **A field styled with `font: inherit` under a selector heavier than a |
| + bare `input`** loses tokens.css's 16px-on-touch rule, and iOS zooms on |
| + tap. Restate `font-size: max(1rem, 1em)` for it under `(pointer: |
| + coarse)`, as memoria.css does for `[rst-stack]`'s fields. |
| + `TestNoZoomOnTap` (`make browser`) measures every field. |
| + |
| **Auth** |
| - `requireSession` **fails closed**. The test seam is `AppForTest`, named |
| so it cannot be reached by accident, and two gates hold both ends. An |
diff --git a/Makefile b/Makefile| index a3ae7b6..3a14c4a 100644 |
| --- a/Makefile |
| +++ b/Makefile |
| @@ -50,7 +50,7 @@ release: |
| { echo "make release: no version to stamp; commit, or name it: make release VERSION=v0.1.0"; exit 1; } |
| @mkdir -p releases |
| CGO_ENABLED=0 GOOS=$(RELEASE_GOOS) GOARCH=$(RELEASE_GOARCH) go build -trimpath \ |
| - -ldflags "-s -w -X github.com/carlosframework/rastrillo.BuildVersion=$(VERSION)" \ |
| + -ldflags "-s -w -X amadan.net/rastrillo/rastrillo.BuildVersion=$(VERSION)" \ |
| -o $(RELEASE_BIN) ./cmd/$(APP) |
| @echo "built $(RELEASE_BIN) reporting version $(VERSION)" |
| @echo "deploy it with:" |
| @@ -74,13 +74,14 @@ fmt-check: |
| # steps each exec one make target, so migration-check needs its own |
| # name to get its own reported step on a runner with step support. |
| # |
| -# go run, not a bare "rastrillo": go.mod already requires rastrillo, so |
| -# this needs nothing on PATH but the Go toolchain, and works offline |
| -# after go mod download. A bare "rastrillo migration check" failed a |
| -# fresh clone's very first CI run with "make: rastrillo: Command not |
| -# found", and nothing in the scaffold said what to install. |
| +# go tool, not a bare "rastrillo": go.mod pins the CLI as a tool at the |
| +# module's own version, so this needs nothing on PATH but the Go |
| +# toolchain, works offline after go mod download, and always runs the |
| +# migrate the app is built against. A bare "rastrillo migration check" |
| +# failed a fresh clone's very first CI run with "make: rastrillo: |
| +# Command not found", and nothing in the scaffold said what to install. |
| migration-check: |
| - go run github.com/carlosframework/rastrillo/cmd/rastrillo migration check |
| + go tool rastrillo migration check |
| # ci is the one gate: what a runner executes and what you run before |
| # pushing are the same definition. amadan's runner falls back to this |
diff --git a/cmd/memoria/main.go b/cmd/memoria/main.go| index 174c67f..3ed8ce1 100644 |
| --- a/cmd/memoria/main.go |
| +++ b/cmd/memoria/main.go |
| @@ -6,9 +6,9 @@ import ( |
| "log/slog" |
| "os" |
| - "github.com/carlosframework/rastrillo" |
| - "github.com/carlosframework/rastrillo/db" |
| - "github.com/carlosframework/rastrillo/mail" |
| + "amadan.net/rastrillo/rastrillo" |
| + "amadan.net/rastrillo/rastrillo/db" |
| + "amadan.net/rastrillo/rastrillo/mail" |
| memoria "amadan.net/oficina/memoria/internal/memoria" |
| ) |
diff --git a/go.mod b/go.mod| index 304acc0..d8255fc 100644 |
| --- a/go.mod |
| +++ b/go.mod |
| @@ -5,7 +5,7 @@ go 1.26 |
| toolchain go1.26.5 |
| require ( |
| - amadan.net/oficina/estilo.git v0.0.0-20261001084202-0b4d92c3dc16 |
| + amadan.net/oficina/estilo.git v0.0.0-20261001093750-530cb932e768 |
| amadan.net/oficina/home/connect v0.0.0-20261001093757-2bec942ddd0c |
| amadan.net/oficina/home/handoff v0.0.0-20260908102137-481d2d57f489 |
| amadan.net/oficina/suite v0.0.0-20260905215529-60b01a8a0d6d |
| @@ -47,4 +47,4 @@ require ( |
| modernc.org/memory v1.11.0 // indirect |
| ) |
| -tool github.com/carlosframework/rastrillo/cmd/rastrillo |
| +tool amadan.net/rastrillo/rastrillo/cmd/rastrillo |
diff --git a/go.sum b/go.sum| index d120a6e..281d8a3 100644 |
| --- a/go.sum |
| +++ b/go.sum |
| @@ -1,5 +1,5 @@ |
| -amadan.net/oficina/estilo.git v0.0.0-20261001084202-0b4d92c3dc16 h1:F5v3x25dEj1rJNnBm71G7shN+fM+T2actUlaGPvoeH8= |
| -amadan.net/oficina/estilo.git v0.0.0-20261001084202-0b4d92c3dc16/go.mod h1:4u0jNBtIdMLOsJyrScfqoEuYDJF7BBM3qM9YWbzt6aw= |
| +amadan.net/oficina/estilo.git v0.0.0-20261001093750-530cb932e768 h1:bVscoa9fH5LdLDjlyecZhrX7JzWPT0blb57MH0FIm2U= |
| +amadan.net/oficina/estilo.git v0.0.0-20261001093750-530cb932e768/go.mod h1:4u0jNBtIdMLOsJyrScfqoEuYDJF7BBM3qM9YWbzt6aw= |
| amadan.net/oficina/home/connect v0.0.0-20261001093757-2bec942ddd0c h1:3yxO2D8BxDLMGfFV2o6Sp9Z91yLnjDCIuRKHVWORa8A= |
| amadan.net/oficina/home/connect v0.0.0-20261001093757-2bec942ddd0c/go.mod h1:YroavXe43Wietec+po8mtSk4a9Bir2h31Q3iAf4MWxI= |
| amadan.net/oficina/home/handoff v0.0.0-20260908102137-481d2d57f489 h1:RPKtyry/71i06o8L+gjAaUyexbmG56kO1YPs601URAM= |
diff --git a/internal/memoria/app.go b/internal/memoria/app.go| index c848a57..319b93b 100644 |
| --- a/internal/memoria/app.go |
| +++ b/internal/memoria/app.go |
| @@ -14,10 +14,10 @@ import ( |
| "github.com/go-chi/chi/v5" |
| "amadan.net/oficina/home/connect" |
| - "github.com/carlosframework/rastrillo/auth" |
| - "github.com/carlosframework/rastrillo/db" |
| - "github.com/carlosframework/rastrillo/migrate" |
| - "github.com/carlosframework/rastrillo/sessions" |
| + "amadan.net/rastrillo/rastrillo/auth" |
| + "amadan.net/rastrillo/rastrillo/db" |
| + "amadan.net/rastrillo/rastrillo/migrate" |
| + "amadan.net/rastrillo/rastrillo/sessions" |
| "amadan.net/oficina/memoria/internal/memoria/index" |
| "amadan.net/oficina/memoria/internal/memoria/seal" |
diff --git a/internal/memoria/auth.go b/internal/memoria/auth.go| index b09f2eb..194242c 100644 |
| --- a/internal/memoria/auth.go |
| +++ b/internal/memoria/auth.go |
| @@ -10,8 +10,8 @@ import ( |
| "amadan.net/oficina/estilo.git/chrome" |
| "amadan.net/oficina/estilo.git/sky" |
| "amadan.net/oficina/home/connect" |
| - "github.com/carlosframework/rastrillo/auth" |
| - "github.com/carlosframework/rastrillo/mail" |
| + "amadan.net/rastrillo/rastrillo/auth" |
| + "amadan.net/rastrillo/rastrillo/mail" |
| ) |
| // signinPath is where every sign-in outcome lands. It must match the |
diff --git a/internal/memoria/chrome.go b/internal/memoria/chrome.go| index adfb1cd..cdfaef2 100644 |
| --- a/internal/memoria/chrome.go |
| +++ b/internal/memoria/chrome.go |
| @@ -24,7 +24,7 @@ import ( |
| estilomenu "amadan.net/oficina/estilo.git/menu" |
| "amadan.net/oficina/home/connect" |
| "amadan.net/oficina/home/connect/linkstore" |
| - "github.com/carlosframework/rastrillo/sessions" |
| + "amadan.net/rastrillo/rastrillo/sessions" |
| ) |
| // appName is the brand and the last part of every <title>. |
diff --git a/internal/memoria/credits/notices.json b/internal/memoria/credits/notices.json| index 78702c2..751ad94 100644 |
| --- a/internal/memoria/credits/notices.json |
| +++ b/internal/memoria/credits/notices.json |
| @@ -41,6 +41,10 @@ |
| { |
| "Name": "LICENSE", |
| "Text": "Mozilla Public License Version 2.0\n==================================\n\n1. Definitions\n--------------\n\n1.1. \"Contributor\"\n means each individual or legal entity that creates, contributes to\n the creation of, or owns Covered Software.\n\n1.2. \"Contributor Version\"\n means the combination of the Contributions of others (if any) used\n by a Contributor and that particular Contributor's Contribution.\n\n1.3. \"Contribution\"\n means Covered Software of a particular Contributor.\n\n1.4. \"Covered Software\"\n means Source Code Form to which the initial Contributor has attached\n the notice in Exhibit A, the Executable Form of such Source Code\n Form, and Modifications of such Source Code Form, in each case\n including portions thereof.\n\n1.5. \"Incompatible With Secondary Licenses\"\n means\n\n (a) that the initial Contributor has attached the notice described\n in Exhibit B to the Covered Software; or\n\n (b) that the Covered Software was made available under the terms of\n version 1.1 or earlier of the License, but not also under the\n terms of a Secondary License.\n\n1.6. \"Executable Form\"\n means any form of the work other than Source Code Form.\n\n1.7. \"Larger Work\"\n means a work that combines Covered Software with other material, in\n a separate file or files, that is not Covered Software.\n\n1.8. \"License\"\n means this document.\n\n1.9. \"Licensable\"\n means having the right to grant, to the maximum extent possible,\n whether at the time of the initial grant or subsequently, any and\n all of the rights conveyed by this License.\n\n1.10. \"Modifications\"\n means any of the following:\n\n (a) any file in Source Code Form that results from an addition to,\n deletion from, or modification of the contents of Covered\n Software; or\n\n (b) any new file in Source Code Form that contains any Covered\n Software.\n\n1.11. \"Patent Claims\" of a Contributor\n means any patent claim(s), including without limitation, method,\n process, and apparatus claims, in any patent Licensable by such\n Contributor that would be infringed, but for the grant of the\n License, by the making, using, selling, offering for sale, having\n made, import, or transfer of either its Contributions or its\n Contributor Version.\n\n1.12. \"Secondary License\"\n means either the GNU General Public License, Version 2.0, the GNU\n Lesser General Public License, Version 2.1, the GNU Affero General\n Public License, Version 3.0, or any later versions of those\n licenses.\n\n1.13. \"Source Code Form\"\n means the form of the work preferred for making modifications.\n\n1.14. \"You\" (or \"Your\")\n means an individual or a legal entity exercising rights under this\n License. For legal entities, \"You\" includes any entity that\n controls, is controlled by, or is under common control with You. For\n purposes of this definition, \"control\" means (a) the power, direct\n or indirect, to cause the direction or management of such entity,\n whether by contract or otherwise, or (b) ownership of more than\n fifty percent (50%) of the outstanding shares or beneficial\n ownership of such entity.\n\n2. License Grants and Conditions\n--------------------------------\n\n2.1. Grants\n\nEach Contributor hereby grants You a world-wide, royalty-free,\nnon-exclusive license:\n\n(a) under intellectual property rights (other than patent or trademark)\n Licensable by such Contributor to use, reproduce, make available,\n modify, display, perform, distribute, and otherwise exploit its\n Contributions, either on an unmodified basis, with Modifications, or\n as part of a Larger Work; and\n\n(b) under Patent Claims of such Contributor to make, use, sell, offer\n for sale, have made, import, and otherwise transfer either its\n Contributions or its Contributor Version.\n\n2.2. Effective Date\n\nThe licenses granted in Section 2.1 with respect to any Contribution\nbecome effective for each Contribution on the date the Contributor first\ndistributes such Contribution.\n\n2.3. Limitations on Grant Scope\n\nThe licenses granted in this Section 2 are the only rights granted under\nthis License. No additional rights or licenses will be implied from the\ndistribution or licensing of Covered Software under this License.\nNotwithstanding Section 2.1(b) above, no patent license is granted by a\nContributor:\n\n(a) for any code that a Contributor has removed from Covered Software;\n or\n\n(b) for infringements caused by: (i) Your and any other third party's\n modifications of Covered Software, or (ii) the combination of its\n Contributions with other software (except as part of its Contributor\n Version); or\n\n(c) under Patent Claims infringed by Covered Software in the absence of\n its Contributions.\n\nThis License does not grant any rights in the trademarks, service marks,\nor logos of any Contributor (except as may be necessary to comply with\nthe notice requirements in Section 3.4).\n\n2.4. Subsequent Licenses\n\nNo Contributor makes additional grants as a result of Your choice to\ndistribute the Covered Software under a subsequent version of this\nLicense (see Section 10.2) or under the terms of a Secondary License (if\npermitted under the terms of Section 3.3).\n\n2.5. Representation\n\nEach Contributor represents that the Contributor believes its\nContributions are its original creation(s) or it has sufficient rights\nto grant the rights to its Contributions conveyed by this License.\n\n2.6. Fair Use\n\nThis License is not intended to limit any rights You have under\napplicable copyright doctrines of fair use, fair dealing, or other\nequivalents.\n\n2.7. Conditions\n\nSections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted\nin Section 2.1.\n\n3. Responsibilities\n-------------------\n\n3.1. Distribution of Source Form\n\nAll distribution of Covered Software in Source Code Form, including any\nModifications that You create or to which You contribute, must be under\nthe terms of this License. You must inform recipients that the Source\nCode Form of the Covered Software is governed by the terms of this\nLicense, and how they can obtain a copy of this License. You may not\nattempt to alter or restrict the recipients' rights in the Source Code\nForm.\n\n3.2. Distribution of Executable Form\n\nIf You distribute Covered Software in Executable Form then:\n\n(a) such Covered Software must also be made available in Source Code\n Form, as described in Section 3.1, and You must inform recipients of\n the Executable Form how they can obtain a copy of such Source Code\n Form by reasonable means in a timely manner, at a charge no more\n than the cost of distribution to the recipient; and\n\n(b) You may distribute such Executable Form under the terms of this\n License, or sublicense it under different terms, provided that the\n license for the Executable Form does not attempt to limit or alter\n the recipients' rights in the Source Code Form under this License.\n\n3.3. Distribution of a Larger Work\n\nYou may create and distribute a Larger Work under terms of Your choice,\nprovided that You also comply with the requirements of this License for\nthe Covered Software. If the Larger Work is a combination of Covered\nSoftware with a work governed by one or more Secondary Licenses, and the\nCovered Software is not Incompatible With Secondary Licenses, this\nLicense permits You to additionally distribute such Covered Software\nunder the terms of such Secondary License(s), so that the recipient of\nthe Larger Work may, at their option, further distribute the Covered\nSoftware under the terms of either this License or such Secondary\nLicense(s).\n\n3.4. Notices\n\nYou may not remove or alter the substance of any license notices\n(including copyright notices, patent notices, disclaimers of warranty,\nor limitations of liability) contained within the Source Code Form of\nthe Covered Software, except that You may alter any license notices to\nthe extent required to remedy known factual inaccuracies.\n\n3.5. Application of Additional Terms\n\nYou may choose to offer, and to charge a fee for, warranty, support,\nindemnity or liability obligations to one or more recipients of Covered\nSoftware. However, You may do so only on Your own behalf, and not on\nbehalf of any Contributor. You must make it absolutely clear that any\nsuch warranty, support, indemnity, or liability obligation is offered by\nYou alone, and You hereby agree to indemnify every Contributor for any\nliability incurred by such Contributor as a result of warranty, support,\nindemnity or liability terms You offer. You may include additional\ndisclaimers of warranty and limitations of liability specific to any\njurisdiction.\n\n4. Inability to Comply Due to Statute or Regulation\n---------------------------------------------------\n\nIf it is impossible for You to comply with any of the terms of this\nLicense with respect to some or all of the Covered Software due to\nstatute, judicial order, or regulation then You must: (a) comply with\nthe terms of this License to the maximum extent possible; and (b)\ndescribe the limitations and the code they affect. Such description must\nbe placed in a text file included with all distributions of the Covered\nSoftware under this License. Except to the extent prohibited by statute\nor regulation, such description must be sufficiently detailed for a\nrecipient of ordinary skill to be able to understand it.\n\n5. Termination\n--------------\n\n5.1. The rights granted under this License will terminate automatically\nif You fail to comply with any of its terms. However, if You become\ncompliant, then the rights granted under this License from a particular\nContributor are reinstated (a) provisionally, unless and until such\nContributor explicitly and finally terminates Your grants, and (b) on an\nongoing basis, if such Contributor fails to notify You of the\nnon-compliance by some reasonable means prior to 60 days after You have\ncome back into compliance. Moreover, Your grants from a particular\nContributor are reinstated on an ongoing basis if such Contributor\nnotifies You of the non-compliance by some reasonable means, this is the\nfirst time You have received notice of non-compliance with this License\nfrom such Contributor, and You become compliant prior to 30 days after\nYour receipt of the notice.\n\n5.2. If You initiate litigation against any entity by asserting a patent\ninfringement claim (excluding declaratory judgment actions,\ncounter-claims, and cross-claims) alleging that a Contributor Version\ndirectly or indirectly infringes any patent, then the rights granted to\nYou by any and all Contributors for the Covered Software under Section\n2.1 of this License shall terminate.\n\n5.3. In the event of termination under Sections 5.1 or 5.2 above, all\nend user license agreements (excluding distributors and resellers) which\nhave been validly granted by You or Your distributors under this License\nprior to termination shall survive termination.\n\n************************************************************************\n* *\n* 6. Disclaimer of Warranty *\n* ------------------------- *\n* *\n* Covered Software is provided under this License on an \"as is\" *\n* basis, without warranty of any kind, either expressed, implied, or *\n* statutory, including, without limitation, warranties that the *\n* Covered Software is free of defects, merchantable, fit for a *\n* particular purpose or non-infringing. The entire risk as to the *\n* quality and performance of the Covered Software is with You. *\n* Should any Covered Software prove defective in any respect, You *\n* (not any Contributor) assume the cost of any necessary servicing, *\n* repair, or correction. This disclaimer of warranty constitutes an *\n* essential part of this License. No use of any Covered Software is *\n* authorized under this License except under this disclaimer. *\n* *\n************************************************************************\n\n************************************************************************\n* *\n* 7. Limitation of Liability *\n* -------------------------- *\n* *\n* Under no circumstances and under no legal theory, whether tort *\n* (including negligence), contract, or otherwise, shall any *\n* Contributor, or anyone who distributes Covered Software as *\n* permitted above, be liable to You for any direct, indirect, *\n* special, incidental, or consequential damages of any character *\n* including, without limitation, damages for lost profits, loss of *\n* goodwill, work stoppage, computer failure or malfunction, or any *\n* and all other commercial damages or losses, even if such party *\n* shall have been informed of the possibility of such damages. This *\n* limitation of liability shall not apply to liability for death or *\n* personal injury resulting from such party's negligence to the *\n* extent applicable law prohibits such limitation. Some *\n* jurisdictions do not allow the exclusion or limitation of *\n* incidental or consequential damages, so this exclusion and *\n* limitation may not apply to You. *\n* *\n************************************************************************\n\n8. Litigation\n-------------\n\nAny litigation relating to this License may be brought only in the\ncourts of a jurisdiction where the defendant maintains its principal\nplace of business and such litigation shall be governed by laws of that\njurisdiction, without reference to its conflict-of-law provisions.\nNothing in this Section shall prevent a party's ability to bring\ncross-claims or counter-claims.\n\n9. Miscellaneous\n----------------\n\nThis License represents the complete agreement concerning the subject\nmatter hereof. If any provision of this License is held to be\nunenforceable, such provision shall be reformed only to the extent\nnecessary to make it enforceable. Any law or regulation which provides\nthat the language of a contract shall be construed against the drafter\nshall not be used to construe this License against a Contributor.\n\n10. Versions of the License\n---------------------------\n\n10.1. New Versions\n\nMozilla Foundation is the license steward. Except as provided in Section\n10.3, no one other than the license steward has the right to modify or\npublish new versions of this License. Each version will be given a\ndistinguishing version number.\n\n10.2. Effect of New Versions\n\nYou may distribute the Covered Software under the terms of the version\nof the License under which You originally received the Covered Software,\nor under the terms of any subsequent version published by the license\nsteward.\n\n10.3. Modified Versions\n\nIf you create software not governed by this License, and you want to\ncreate a new license for such software, you may create and use a\nmodified version of this License if you rename the license and remove\nany references to the name of the license steward (except to note that\nsuch modified license differs from this License).\n\n10.4. Distributing Source Code Form that is Incompatible With Secondary\nLicenses\n\nIf You choose to distribute Source Code Form that is Incompatible With\nSecondary Licenses under the terms of this version of the License, the\nnotice described in Exhibit B of this License must be attached.\n\nExhibit A - Source Code Form License Notice\n-------------------------------------------\n\n This Source Code Form is subject to the terms of the Mozilla Public\n License, v. 2.0. If a copy of the MPL was not distributed with this\n file, You can obtain one at https://mozilla.org/MPL/2.0/.\n\nIf it is not possible or desirable to put the notice in a particular\nfile, then You may include the notice in a location (such as a LICENSE\nfile in a relevant directory) where a recipient would be likely to look\nfor such a notice.\n\nYou may add additional accurate notices of copyright ownership.\n\nExhibit B - \"Incompatible With Secondary Licenses\" Notice\n---------------------------------------------------------\n\n This Source Code Form is \"Incompatible With Secondary Licenses\", as\n defined by the Mozilla Public License, v. 2.0.\n" |
| + }, |
| + { |
| + "Name": "gormlite/LICENSE", |
| + "Text": "The MIT License (MIT)\n\nCopyright (c) 2013-NOW Jinzhu \u003cwosmvp@gmail.com\u003e\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in\nall copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\nTHE SOFTWARE.\n" |
| } |
| ] |
| }, |
diff --git a/internal/memoria/custody_test.go b/internal/memoria/custody_test.go| index 9b35870..591aad9 100644 |
| --- a/internal/memoria/custody_test.go |
| +++ b/internal/memoria/custody_test.go |
| @@ -10,8 +10,8 @@ import ( |
| "strings" |
| "testing" |
| - "github.com/carlosframework/rastrillo/db" |
| - "github.com/carlosframework/rastrillo/migrate" |
| + "amadan.net/rastrillo/rastrillo/db" |
| + "amadan.net/rastrillo/rastrillo/migrate" |
| "amadan.net/oficina/suite" |
diff --git a/internal/memoria/errors.go b/internal/memoria/errors.go| index c3fda8e..9209552 100644 |
| --- a/internal/memoria/errors.go |
| +++ b/internal/memoria/errors.go |
| @@ -25,7 +25,7 @@ import ( |
| "strings" |
| "amadan.net/oficina/estilo.git/chrome" |
| - "github.com/carlosframework/rastrillo" |
| + "amadan.net/rastrillo/rastrillo" |
| "github.com/go-chi/chi/v5" |
| ) |
diff --git a/internal/memoria/handlers.go b/internal/memoria/handlers.go| index b34bcdb..45ca150 100644 |
| --- a/internal/memoria/handlers.go |
| +++ b/internal/memoria/handlers.go |
| @@ -16,12 +16,12 @@ import ( |
| "amadan.net/oficina/estilo.git/chrome" |
| "amadan.net/oficina/estilo.git/sky" |
| - "github.com/carlosframework/rastrillo" |
| - "github.com/carlosframework/rastrillo/auth" |
| + "amadan.net/rastrillo/rastrillo" |
| + "amadan.net/rastrillo/rastrillo/auth" |
| "amadan.net/oficina/suite" |
| - "github.com/carlosframework/rastrillo/db" |
| - "github.com/carlosframework/rastrillo/sessions" |
| + "amadan.net/rastrillo/rastrillo/db" |
| + "amadan.net/rastrillo/rastrillo/sessions" |
| "github.com/go-chi/chi/v5" |
| "amadan.net/oficina/memoria/internal/memoria/query" |
diff --git a/internal/memoria/home.go b/internal/memoria/home.go| index 78e76fd..b7d5afc 100644 |
| --- a/internal/memoria/home.go |
| +++ b/internal/memoria/home.go |
| @@ -8,9 +8,9 @@ import ( |
| "amadan.net/oficina/home/connect" |
| "amadan.net/oficina/home/connect/linkstore" |
| "amadan.net/oficina/home/handoff" |
| - "github.com/carlosframework/rastrillo/csrf" |
| - "github.com/carlosframework/rastrillo/migrate" |
| - "github.com/carlosframework/rastrillo/sessions" |
| + "amadan.net/rastrillo/rastrillo/csrf" |
| + "amadan.net/rastrillo/rastrillo/migrate" |
| + "amadan.net/rastrillo/rastrillo/sessions" |
| ) |
| type homeIntegration struct { |
diff --git a/internal/memoria/home_identity.go b/internal/memoria/home_identity.go| index 48fa111..6de6942 100644 |
| --- a/internal/memoria/home_identity.go |
| +++ b/internal/memoria/home_identity.go |
| @@ -6,7 +6,7 @@ import ( |
| "errors" |
| "fmt" |
| - "github.com/carlosframework/rastrillo/sessions" |
| + "amadan.net/rastrillo/rastrillo/sessions" |
| ) |
| var errHomeIdentity = errors.New("memoria: Home identity unavailable") |
diff --git a/internal/memoria/home_identity_test.go b/internal/memoria/home_identity_test.go| index cac7675..43ae693 100644 |
| --- a/internal/memoria/home_identity_test.go |
| +++ b/internal/memoria/home_identity_test.go |
| @@ -6,8 +6,8 @@ import ( |
| "errors" |
| "testing" |
| - "github.com/carlosframework/rastrillo/migrate" |
| - "github.com/carlosframework/rastrillo/sessions" |
| + "amadan.net/rastrillo/rastrillo/migrate" |
| + "amadan.net/rastrillo/rastrillo/sessions" |
| ) |
| func homeIdentityFixture(t *testing.T) *app { |
diff --git a/internal/memoria/home_migrations.go b/internal/memoria/home_migrations.go| index 75d6b11..8c27a49 100644 |
| --- a/internal/memoria/home_migrations.go |
| +++ b/internal/memoria/home_migrations.go |
| @@ -4,7 +4,7 @@ import ( |
| "embed" |
| "io/fs" |
| - "github.com/carlosframework/rastrillo/migrate" |
| + "amadan.net/rastrillo/rastrillo/migrate" |
| ) |
| //go:embed home_migrations/migrations/*.sql |
diff --git a/internal/memoria/home_router_test.go b/internal/memoria/home_router_test.go| index 9fdcfbf..cce199c 100644 |
| --- a/internal/memoria/home_router_test.go |
| +++ b/internal/memoria/home_router_test.go |
| @@ -20,7 +20,7 @@ import ( |
| "amadan.net/oficina/suite" |
| "amadan.net/rastrillo/rastrillo/assertion" |
| assertioncrypto "amadan.net/rastrillo/rastrillo/crypto" |
| - "github.com/carlosframework/rastrillo/db" |
| + "amadan.net/rastrillo/rastrillo/db" |
| ) |
| const homeTestOrigin = "https://memoria.example.test" |
diff --git a/internal/memoria/home_security_test.go b/internal/memoria/home_security_test.go| index fe8ea15..abada07 100644 |
| --- a/internal/memoria/home_security_test.go |
| +++ b/internal/memoria/home_security_test.go |
| @@ -6,7 +6,7 @@ import ( |
| "strings" |
| "testing" |
| - "github.com/carlosframework/rastrillo" |
| + "amadan.net/rastrillo/rastrillo" |
| ) |
| func TestHomeCSPPreservesPinnedFrameworkDirectives(t *testing.T) { |
diff --git a/internal/memoria/lag_test.go b/internal/memoria/lag_test.go| index 64faee5..2c07765 100644 |
| --- a/internal/memoria/lag_test.go |
| +++ b/internal/memoria/lag_test.go |
| @@ -22,7 +22,7 @@ import ( |
| "strings" |
| "testing" |
| - "github.com/carlosframework/rastrillo/db" |
| + "amadan.net/rastrillo/rastrillo/db" |
| "amadan.net/oficina/memoria/internal/memoria/seal" |
| ) |
diff --git a/internal/memoria/migrations.go b/internal/memoria/migrations.go| index 8ad5334..aa155aa 100644 |
| --- a/internal/memoria/migrations.go |
| +++ b/internal/memoria/migrations.go |
| @@ -3,9 +3,9 @@ package memoria |
| import ( |
| "embed" |
| - "github.com/carlosframework/rastrillo/auth" |
| - "github.com/carlosframework/rastrillo/migrate" |
| - "github.com/carlosframework/rastrillo/sessions" |
| + "amadan.net/rastrillo/rastrillo/auth" |
| + "amadan.net/rastrillo/rastrillo/migrate" |
| + "amadan.net/rastrillo/rastrillo/sessions" |
| ) |
| //go:embed migrations/*.sql |
diff --git a/internal/memoria/native.go b/internal/memoria/native.go| index 67fc244..04ff662 100644 |
| --- a/internal/memoria/native.go |
| +++ b/internal/memoria/native.go |
| @@ -10,16 +10,32 @@ import ( |
| "amadan.net/oficina/suite" |
| nativeauth "amadan.net/rastrillo/native/server" |
| - "github.com/carlosframework/rastrillo/sessions" |
| + "amadan.net/rastrillo/rastrillo/sessions" |
| + |
| + // The one import left on rastrillo's pre-move path, on purpose: |
| + // rastrillo/native's server (2a5d07b, and its main) still takes a |
| + // github.com/carlosframework/rastrillo *sessions.Sessions. Both |
| + // sessions packages read and write the same sessions table, so a |
| + // session native sign-in mints is an ordinary session to the app |
| + // (native_exchange_test.go). Move this to |
| + // amadan.net/rastrillo/rastrillo/sessions when native/server moves. |
| + presessions "github.com/carlosframework/rastrillo/sessions" |
| ) |
| func (a *app) wireNative(origin string) error { |
| - s, e := sessions.New(sessions.Config{DB: a.db.Writer(), Origin: origin, SigninPath: "/signin", Logger: a.logger}) |
| + cfg := sessions.Config{DB: a.db.Writer(), Origin: origin, SigninPath: "/signin", Logger: a.logger} |
| + s, e := sessions.New(cfg) |
| if e != nil { |
| return e |
| } |
| a.sess = s |
| - a.native = nativeauth.New(s, origin, "Memoria", a.requireSession) |
| + // The same table, cookie and origin, through the type native/server |
| + // takes. The app itself reads sessions through a.sess only. |
| + pre, e := presessions.New(presessions.Config{DB: cfg.DB, Origin: cfg.Origin, SigninPath: cfg.SigninPath, Logger: cfg.Logger}) |
| + if e != nil { |
| + return e |
| + } |
| + a.native = nativeauth.New(pre, origin, "Memoria", a.requireSession) |
| return nil |
| } |
| func nativeJSON(w http.ResponseWriter, v any) { |
diff --git a/internal/memoria/render.go b/internal/memoria/render.go| index 099c494..37e026c 100644 |
| --- a/internal/memoria/render.go |
| +++ b/internal/memoria/render.go |
| @@ -11,8 +11,8 @@ import ( |
| "amadan.net/oficina/estilo.git/credits" |
| estilomenu "amadan.net/oficina/estilo.git/menu" |
| "amadan.net/oficina/home/connect" |
| - "github.com/carlosframework/rastrillo" |
| - "github.com/carlosframework/rastrillo/ui" |
| + "amadan.net/rastrillo/rastrillo" |
| + "amadan.net/rastrillo/rastrillo/ui" |
| "amadan.net/oficina/memoria/internal/memoria/icons" |
| ) |
diff --git a/internal/memoria/static/chrome.css b/internal/memoria/static/chrome.css| index 6bda384..74bd3d7 100644 |
| --- a/internal/memoria/static/chrome.css |
| +++ b/internal/memoria/static/chrome.css |
| @@ -135,6 +135,14 @@ |
| } |
| [estilo-bar-title-input]:hover { border-color: var(--rst-line); } |
| [estilo-bar-title-input]:focus { border-color: var(--rst-accent); outline: none; background: var(--rst-surface); } |
| +/* iOS zooms the page into a text field under 16px. tokens.css lifts fields |
| + to 16px on a coarse pointer, but with a bare `input` selector, which |
| + font: inherit above outweighs. Same rule, restated at this weight. */ |
| +@media (pointer: coarse) { |
| + [estilo-bar-title-input] { |
| + font-size: max(1rem, 1em); |
| + } |
| +} |
| [estilo-bar-title-submit] { position: absolute; clip-path: inset(50%); inline-size: 1px; block-size: 1px; overflow: hidden; white-space: nowrap; } |
| [estilo-bar-title]:focus-within [estilo-bar-title-submit] { position: static; clip-path: none; inline-size: auto; block-size: 2rem; overflow: visible; flex: none; } |
| /* Below the shell breakpoint the row has no room for the button beside the |
diff --git a/internal/memoria/static/menu.css b/internal/memoria/static/menu.css| index 2ddf029..9070652 100644 |
| --- a/internal/memoria/static/menu.css |
| +++ b/internal/memoria/static/menu.css |
| @@ -63,6 +63,10 @@ |
| [estilo-menu-item]:hover, |
| [estilo-menu-item]:focus-visible { background: var(--rst-accent-soft); } |
| [estilo-menu-item]:focus-visible { outline: 2px solid var(--rst-accent); outline-offset: -2px; } |
| +/* A destructive row: the negative tone, and its own tint on hover. The |
| + label says what it does; the tone says it is the one to read twice. */ |
| +[estilo-menu-item~="danger"] { color: var(--rst-tone-negative-fg); } |
| +[estilo-menu-item~="danger"]:hover, [estilo-menu-item~="danger"]:focus-visible { background: var(--rst-tone-negative-bg); } |
| [estilo-menu-item][aria-disabled="true"], |
| [estilo-menu-item]:disabled { |
| color: var(--rst-text-faint); |
diff --git a/internal/memoria/static/search.js b/internal/memoria/static/search.js| index 90010d3..249ae03 100644 |
| --- a/internal/memoria/static/search.js |
| +++ b/internal/memoria/static/search.js |
| @@ -1,8 +1,7 @@ |
| /* search.js — live search, keyboard navigation, and Enter. |
| * |
| - * App-owned, not vendored: rastrillo.js and select.js come from the |
| - * library and are pinned byte-identical by a gate; this file and drop.js |
| - * are memoria's. |
| + * App-owned, not vendored: rastrillo.js comes from estilo and is pinned |
| + * byte-identical by a gate; this file and drop.js are memoria's. |
| * |
| * Same house rules as its neighbours: first-party, dependency-free, and |
| * an enhancement rather than a mechanism. Delete this file and the |
diff --git a/internal/memoria/static/select.js b/internal/memoria/static/select.js| deleted file mode 100644 |
| index 127b501..0000000 |
| --- a/internal/memoria/static/select.js |
| +++ /dev/null |
| @@ -1,280 +0,0 @@ |
| -/* select.js — the searchable-select enhancement. A sibling of |
| - rastrillo.js, following exactly the same rules: first-party, |
| - dependency-free, and inert by default. Only a <select> that opts in |
| - with data-rst-select gets behavior, and what it enhances works with |
| - scripts disabled, because the thing it enhances is a real <select> |
| - that never leaves the page. |
| - |
| - It is a separate file rather than more of rastrillo.js so that both |
| - stay small enough for an app owner to read in one sitting — the shim |
| - holds itself to 8KB and a full ARIA combobox would have doubled it; |
| - this one holds itself to 12KB. |
| - Both are app-owned from the moment they are scaffolded. |
| - |
| - Vocabulary: |
| - data-rst-select on a <select>: mirror a filterable |
| - ARIA 1.2 combobox onto it |
| - data-rst-select="false" never; stay a native select at any |
| - size |
| - data-rst-select-filter placeholder for the filter input |
| - data-rst-select-results live-region text, {n} substituted |
| - data-rst-select-result-one live-region text for exactly one |
| - |
| - ui/partials/field-select.html emits all four past ten options. The |
| - strings come from the framework's base catalog (rastrillo.ui.select_*) |
| - and ride out on attributes, because this markup is built in the |
| - browser where the catalog is out of reach. */ |
| -(function () { |
| - "use strict"; |
| - |
| -// A <select> with too many options to scan by eye becomes a filterable |
| -// combobox — but the <select> itself never leaves the DOM. It is |
| -// visually hidden and mirrored, so form submission, required |
| -// validation, reset and browser autofill all keep working on the |
| -// element they always did. Nothing here invents state: the select is |
| -// the single source of truth, and if this function never runs the user |
| -// gets a perfectly ordinary native select. |
| -// |
| -// Strings arrive on data attributes because this markup is built in |
| -// the browser, after the catalog is out of reach. See |
| -// rastrillo.ui.select_* in the framework's base catalog. |
| -function combo(native) { |
| - if (native.dataset.rstEnhanced) return; // idempotent: safe to re-scan |
| - native.dataset.rstEnhanced = "true"; |
| - |
| - // The list mirrors the select's own shape rather than flattening it: |
| - // an <optgroup> the author wrote to make a long list readable becomes |
| - // a group in the popup, and loose <option> children sit at the top |
| - // level as a group with no label. native.options would have been |
| - // shorter and would have silently thrown the headings away. |
| - var groups = []; |
| - Array.prototype.forEach.call(native.children, function (el) { |
| - if (el.tagName === "OPTGROUP") { |
| - groups.push({ label: el.label, options: Array.prototype.slice.call(el.querySelectorAll("option")) }); |
| - } else if (el.tagName === "OPTION") { |
| - groups.push({ label: "", options: [el] }); |
| - } |
| - }); |
| - var id = native.id; |
| - var listId = id + "-listbox"; |
| - var filterLabel = native.getAttribute("data-rst-select-filter") || "Type to filter"; |
| - var manyFmt = native.getAttribute("data-rst-select-results") || "{n} results"; |
| - var oneFmt = native.getAttribute("data-rst-select-result-one") || "1 result"; |
| - |
| - var wrap = document.createElement("div"); |
| - wrap.setAttribute("rst-combo", ""); |
| - |
| - var input = document.createElement("input"); |
| - input.type = "text"; |
| - // rst-input and nothing else: rst-combo-input never had a rule, so |
| - // the flip dropped it rather than renaming a name into the |
| - // vocabulary that styles nothing. |
| - input.setAttribute("rst-input", ""); |
| - input.id = id + "-combo"; |
| - input.autocomplete = "off"; |
| - input.setAttribute("role", "combobox"); |
| - input.setAttribute("aria-expanded", "false"); |
| - input.setAttribute("aria-controls", listId); |
| - input.setAttribute("aria-autocomplete", "list"); |
| - input.setAttribute("placeholder", filterLabel); |
| - |
| - var list = document.createElement("ul"); |
| - list.setAttribute("rst-combo-list", ""); |
| - list.id = listId; |
| - list.setAttribute("role", "listbox"); |
| - list.hidden = true; |
| - |
| - var status = document.createElement("p"); |
| - status.className = "rst-sr-only"; |
| - status.setAttribute("role", "status"); |
| - status.setAttribute("aria-live", "polite"); |
| - |
| - native.parentNode.insertBefore(wrap, native); |
| - wrap.appendChild(input); |
| - wrap.appendChild(list); |
| - wrap.appendChild(status); |
| - |
| - // The label named the select; it now names the control the user |
| - // actually types into. Without this the combobox has no accessible |
| - // name at all. |
| - var label = native.id && document.querySelector('label[for="' + native.id + '"]'); |
| - if (label) label.setAttribute("for", input.id); |
| - // Drop [rst-input] too: both are single class selectors, so its |
| - // width:100% wins on source order and would leave the hidden select a |
| - // full-width box held out of sight by clip-path alone. |
| - native.removeAttribute("rst-input"); |
| - native.classList.add("rst-sr-only"); |
| - native.setAttribute("tabindex", "-1"); |
| - native.setAttribute("aria-hidden", "true"); |
| - |
| - var shown = []; |
| - var active = -1; |
| - |
| - function chosenLabel() { |
| - var o = native.options[native.selectedIndex]; |
| - return o && o.value !== "" ? o.text : ""; |
| - } |
| - input.value = chosenLabel(); |
| - |
| - function close() { |
| - list.hidden = true; |
| - input.setAttribute("aria-expanded", "false"); |
| - input.removeAttribute("aria-activedescendant"); |
| - active = -1; |
| - } |
| - |
| - function commit(option) { |
| - native.value = option.value; |
| - input.value = option.text; |
| - // Mirror onto the real control, so a listener the app attached to |
| - // the select still fires. |
| - native.dispatchEvent(new Event("change", { bubbles: true })); |
| - close(); |
| - } |
| - |
| - // One pickable row. Its index is its position in `shown`, which is |
| - // the keyboard order — headings never enter it, so arrowing down |
| - // steps option to option straight across a group boundary. |
| - function row(o) { |
| - var li = document.createElement("li"); |
| - li.id = listId + "-" + shown.length; |
| - li.setAttribute("rst-combo-option", ""); |
| - li.setAttribute("role", "option"); |
| - li.setAttribute("aria-selected", o.value === native.value ? "true" : "false"); |
| - li.textContent = o.text; |
| - li.addEventListener("mousedown", function (e) { |
| - e.preventDefault(); // keep focus; blur would undo the pick |
| - commit(o); |
| - }); |
| - shown.push(o); |
| - return li; |
| - } |
| - |
| - function draw(filter) { |
| - var needle = filter.trim().toLowerCase(); |
| - shown = []; |
| - list.innerHTML = ""; |
| - groups.forEach(function (g) { |
| - var matched = g.options.filter(function (o) { |
| - return o.value !== "" && o.text.toLowerCase().indexOf(needle) !== -1; |
| - }); |
| - // A group filtered down to nothing takes its heading with it: a |
| - // heading over no rows is a lie about what is there. |
| - if (!matched.length) return; |
| - var into = list; |
| - if (g.label) { |
| - // The group's name lives on aria-label, so the heading itself is |
| - // furniture — hidden from the accessibility tree, out of the |
| - // keyboard order, announced once as the group it names. |
| - var box = document.createElement("li"); |
| - box.setAttribute("role", "group"); |
| - box.setAttribute("aria-label", g.label); |
| - // <li> cannot hold <li>, so the rows nest in a list of their |
| - // own; role=none keeps it out of the accessibility tree, which |
| - // leaves the options owned by the group. |
| - into = document.createElement("ul"); |
| - into.setAttribute("rst-combo-rows", ""); |
| - into.setAttribute("role", "none"); |
| - var head = document.createElement("li"); |
| - head.setAttribute("rst-select-group", ""); |
| - head.setAttribute("aria-hidden", "true"); |
| - head.textContent = g.label; |
| - into.appendChild(head); |
| - box.appendChild(into); |
| - list.appendChild(box); |
| - } |
| - matched.forEach(function (o) { into.appendChild(row(o)); }); |
| - }); |
| - list.hidden = shown.length === 0; |
| - input.setAttribute("aria-expanded", shown.length ? "true" : "false"); |
| - status.textContent = shown.length === 1 |
| - ? oneFmt |
| - : manyFmt.replace("{n}", String(shown.length)); |
| - active = -1; |
| - input.removeAttribute("aria-activedescendant"); |
| - } |
| - |
| - function move(delta) { |
| - if (list.hidden) draw(input.value); |
| - if (!shown.length) return; |
| - active = (active + delta + shown.length) % shown.length; |
| - // The rows, not list.children: a grouped list's children are the |
| - // groups, and the headings inside them are not stops. |
| - list.querySelectorAll('[role="option"]').forEach(function (li, i) { |
| - li.classList.toggle("is-active", i === active); |
| - }); |
| - input.setAttribute("aria-activedescendant", listId + "-" + active); |
| - } |
| - |
| - input.addEventListener("input", function () { draw(input.value); }); |
| - input.addEventListener("focus", function () { |
| - // Select the committed label so the first keystroke replaces it. |
| - // Otherwise typing into a box holding "Option 1" filters for |
| - // "Option 1Option 12" and silently matches nothing. |
| - input.select(); |
| - draw(""); |
| - }); |
| - input.addEventListener("blur", function () { |
| - // A half-typed filter is not a value: restore what is committed. |
| - window.setTimeout(function () { input.value = chosenLabel(); close(); }, 0); |
| - }); |
| - input.addEventListener("keydown", function (e) { |
| - switch (e.key) { |
| - case "ArrowDown": e.preventDefault(); move(1); break; |
| - case "ArrowUp": e.preventDefault(); move(-1); break; |
| - case "Home": if (!list.hidden) { e.preventDefault(); active = -1; move(1); } break; |
| - case "End": if (!list.hidden) { e.preventDefault(); active = shown.length - 1; move(0); } break; |
| - case "Enter": |
| - // Enter acts on the filter, never on the form: submitting |
| - // mid-filter discards the edit and silently keeps the previous |
| - // selection. Take the highlight, or the only match, or close. |
| - e.preventDefault(); |
| - if (active >= 0) { |
| - commit(shown[active]); |
| - } else if (shown.length === 1) { |
| - commit(shown[0]); |
| - } else { |
| - close(); |
| - } |
| - break; |
| - case "Escape": e.preventDefault(); close(); input.value = chosenLabel(); break; |
| - } |
| - }); |
| - |
| - // The same guard datetime.js carries, where the long version of this |
| - // note lives. Enter belongs to the combobox, never to the form: a |
| - // commit that also submits posts whatever the select held before the |
| - // user chose. On a key a person presses, the keydown handler's |
| - // preventDefault settles it — the engine derives the keypress from |
| - // the keydown it just cancelled. On a SYNTHESISED key it does not: |
| - // CDP, and every browser drive built on it, can deliver the keydown |
| - // and the character as two independent events, so the second arrives |
| - // with no memory of the first one's refusal, carrying the \r that |
| - // implicit form submission listens for. Measured, not guessed (issue |
| - // #86): without this line the select drive fired a submit on every |
| - // run and passed only by outrunning the navigation. |
| - input.addEventListener("keypress", function (e) { |
| - if (e.key === "Enter") e.preventDefault(); |
| - }); |
| -} |
| - |
| -// Idempotent, so re-scanning is safe if a future change re-scans after |
| -// the shim swaps in a polled fragment. Today nothing does: a select |
| -// arriving inside a replacement stays native — correct, not enhanced. |
| -function scan() { |
| - document.querySelectorAll("select[data-rst-select]").forEach(function (el) { |
| - // data-rst-select="false" is the markup-side opt-out: a select that |
| - // says no stays native at any size. It has to be checked rather |
| - // than selected against, because to CSS the attribute is simply |
| - // present. field-select opts out the other way, by never emitting |
| - // the attribute at all; this is for markup written by hand. |
| - if (el.dataset.rstSelect !== "false") combo(el); |
| - }); |
| -} |
| - |
| -if (document.readyState === "loading") { |
| - document.addEventListener("DOMContentLoaded", scan); |
| -} else { |
| - scan(); |
| -} |
| -})(); |
diff --git a/internal/memoria/static/tokens.css b/internal/memoria/static/tokens.css| index 16a8809..10f6da5 100644 |
| --- a/internal/memoria/static/tokens.css |
| +++ b/internal/memoria/static/tokens.css |
| @@ -62,8 +62,10 @@ |
| Padding values are load-bearing for SC 2.5.8 Target Size (AA, new in |
| WCAG 2.2): pagination chips measure about 30px, row action pills |
| - about 27px and buttons about 34px on their smaller axis, all clear of |
| - the 24px minimum. Do not shrink them without re-measuring. |
| + about 27px, and the button sizes about 28px (sm), 34px (default) and |
| + 44px (lg) on their smaller axis, all clear of the 24px minimum. Do |
| + not shrink them without re-measuring; the button block shows the |
| + arithmetic. |
| Type sizes are rem, not px, so the whole scale tracks a reader who has |
| raised their browser's default font size. Spacing is rem for the same |
| @@ -73,6 +75,7 @@ |
| /* ── Scale: type and spacing. Theme-independent. Radius is not here — |
| it is part of the theme's shape axis; see themes/. ────────────── */ |
| :root { |
| + --rst-fs-lg: 1.0625rem; /* 17px — the one step above base */ |
| --rst-fs-base: 0.875rem; /* 14px at the browser default */ |
| --rst-fs-sm: 0.78125rem; /* 12.5px */ |
| --rst-fs-xs: 0.71875rem; /* 11.5px */ |
| @@ -244,6 +247,56 @@ body { |
| filter: brightness(0.9); |
| } |
| +/* ── Button sizes ───────────────────────────────────────────────────── |
| + Three steps, and they are a size scale rather than a set of variants: |
| + size composes with primary/ghost/danger, so `rst-btn="primary lg"` is |
| + the ordinary spelling of a form's submit. |
| + |
| + The default step is the one the page-header action and the row pill |
| + want — small, dense, a control beside other controls. It was the ONLY |
| + step for a long time, and that is how a form's submit ended up |
| + wearing it: a 34px, 12.5px-labelled button stretched across a 44rem |
| + column, which reads as a skinny bar rather than the thing the whole |
| + screen is asking you to press. A CTA is not a toolbar button; give it |
| + `lg`. |
| + |
| + Measured on the smaller axis, against SC 2.5.8's 24px floor — padding |
| + both sides, plus the label's line box, plus the 1px border top and |
| + bottom (the border is why the default step is 34px and not 32px). The |
| + header comment carries these too; keep the two in step: |
| + sm 0.3rem × 2 + 11.5px × 1.4 + 2 = 9.6 + 16.1 + 2 ≈ 28px |
| + default 0.45rem × 2 + 12.5px × 1.4 + 2 = 14.4 + 17.5 + 2 ≈ 34px |
| + lg 0.7rem × 2 + 14px × 1.4 + 2 = 22.4 + 19.6 + 2 ≈ 44px |
| + sm is the tightest thing in the file that is still a button. It is |
| + for a dense row of them, not for making an ordinary button smaller, |
| + and there is deliberately nothing below it. */ |
| +.rst-btn--sm, [rst-btn~="sm"] { |
| + font-size: var(--rst-fs-xs); |
| + padding: 0.3rem 0.55rem; |
| +} |
| +.rst-btn--lg, [rst-btn~="lg"] { |
| + font-size: var(--rst-fs-base); |
| + padding: 0.7rem 1.15rem; |
| +} |
| + |
| +/* block — a button that fills its container, for the one-action form |
| + (sign in, sign up, a confirm page) where the submit IS the screen. |
| + It centres its own label: .rst-btn sets no justify-content, so a |
| + stretched button's text sits at the row's flex-start and reads as a |
| + mistake rather than a choice. |
| + |
| + Pair it with lg. A block button at the default size is precisely the |
| + skinny bar described above — the width comes from the container and |
| + the height does not follow, so the ratio goes wrong in proportion to |
| + how wide the column is. Nothing here enforces that pairing, because a |
| + narrow container is a legitimate place for a block button at the |
| + default size; the wide ones are where it fails. */ |
| +.rst-btn--block, [rst-btn~="block"] { |
| + display: flex; |
| + justify-content: center; |
| + width: 100%; |
| +} |
| + |
| /* busy — the loading state rastrillo.js gives every submit button on |
| its way out, and the one state of a button that is not a variant: |
| any .rst-btn can wear it. The spinner is a real child element |
| @@ -656,7 +709,7 @@ body { |
| document outline, not a paragraph dressed up as one. */ |
| .rst-empty__title, [rst-empty-title] { |
| color: var(--rst-text); |
| - font-size: 1.0625rem; |
| + font-size: var(--rst-fs-lg); |
| font-weight: 600; |
| margin: 0 0 var(--rst-sp-2); |
| } |
| @@ -733,12 +786,132 @@ body { |
| .rst-badge--negative, [rst-badge~="negative"] { background: var(--rst-tone-negative-bg); border-color: var(--rst-tone-negative-fg); color: var(--rst-tone-negative-fg); } |
| .rst-badge--neutral, [rst-badge~="neutral"] { background: var(--rst-tone-neutral-bg); border-color: var(--rst-tone-neutral-fg); color: var(--rst-tone-neutral-fg); } |
| -/* meter — 4px capacity bar; the number rides beside it as text. */ |
| +/* meter — 4px capacity bar; the number rides beside it as text. |
| + |
| + The bar is a native <meter> (spec §6-v2.4), and styling one is the |
| + one place in this file where the same declaration has to be written |
| + three times: once on the element, once for the WebKit/Blink |
| + pseudo-elements, once for the Firefox one. They cannot share a rule. |
| + A selector list containing a pseudo-element an engine does not know |
| + is invalid AS A WHOLE in that engine, so ::-webkit-meter-bar and |
| + ::-moz-meter-bar in one comma-separated rule would style nothing |
| + anywhere. That is why the repetition below is not tidied up. |
| + |
| + The two engines also disagree about which pseudo-element is which. |
| + In WebKit/Blink ::-webkit-meter-bar is the TRACK and |
| + ::-webkit-meter-optimum-value is the FILL; in Firefox the element |
| + itself is the track and ::-moz-meter-bar is the FILL. Read the three |
| + blocks with that in mind or they look contradictory. |
| + |
| + HONESTLY FLAGGED: the -moz- rule is unverified. This project's |
| + browser drive is Chromium, so the WebKit/Blink path below is measured |
| + and the Firefox path is written from the spec and reasoned about. It |
| + was shipped that way knowingly (Paul's call). If a Firefox drive ever |
| + lands, this is the first rule to point it at. |
| + |
| + The old markup was a <span> with an <i> in it sized by |
| + --rst-meter-fill. That custom property is gone; an app that set it is |
| + setting nothing now, which is why it is called out in the changelog |
| + rather than only here. */ |
| .rst-meter, [rst-meter] { align-items: center; color: var(--rst-text-muted); display: inline-flex; font-size: var(--rst-fs-xs); gap: var(--rst-sp-2); } |
| -.rst-meter__bar, [rst-meter-bar] { background: var(--rst-accent-soft); border-radius: 2px; flex: 1; height: 4px; min-width: 34px; overflow: hidden; } |
| -.rst-meter__bar i, [rst-meter-bar] i { background: var(--rst-accent); border-radius: 2px; display: block; height: 100%; width: var(--rst-meter-fill, 0%); } |
| +.rst-meter__bar, [rst-meter-bar] { appearance: none; background: var(--rst-accent-soft); block-size: 4px; border: 0; border-radius: 2px; flex: 1; min-inline-size: 34px; } |
| +.rst-meter__bar::-webkit-meter-inner-element, [rst-meter-bar]::-webkit-meter-inner-element { block-size: 100%; } |
| +.rst-meter__bar::-webkit-meter-bar, [rst-meter-bar]::-webkit-meter-bar { background: var(--rst-accent-soft); block-size: 100%; border: 0; border-radius: 2px; } |
| +.rst-meter__bar::-webkit-meter-optimum-value, [rst-meter-bar]::-webkit-meter-optimum-value { background: var(--rst-accent); border-radius: 2px; } |
| +.rst-meter__bar::-moz-meter-bar, [rst-meter-bar]::-moz-meter-bar { background: var(--rst-accent); border-radius: 2px; } |
| .rst-meter__num, [rst-meter-num] { font-variant-numeric: tabular-nums; white-space: nowrap; } |
| +/* stats — the instrument strip a dashboard or a report opens with: a |
| + lead reading at headline size, companion readings beside it, divided |
| + by hairlines inside one card. |
| + |
| + One band, one cell, one lead variant. The headline stat and the |
| + counts next to it are the same component at two sizes (spec |
| + §6-v2.4), so there is no second kind to keep in step with this one. |
| + |
| + flex-wrap and a flex-basis rather than a grid with a column count. |
| + Any number of cells has to work — three on one dashboard, five on |
| + the next — and a grid would need the caller to say how many. The |
| + basis is what decides when the strip becomes two rows, and the lead |
| + cell's is larger so it keeps the line to itself when the others |
| + wrap under it. |
| + |
| + The hairline is border-inline-start on every cell but the first, |
| + which is what makes the strip read as ONE instrument rather than as |
| + a row of cards. Logical, so it lands on the right in Arabic. A |
| + wrapped cell keeps its line, and that is the one visual compromise |
| + here: at two rows the first cell of the second row draws a leading |
| + hairline against the card's edge. It is a hairline, and the |
| + alternative — :nth-child arithmetic against a column count nobody |
| + declared — is the grid this deliberately is not. */ |
| +.rst-stats, [rst-stats] { |
| + background: var(--rst-surface); |
| + border: 1px solid var(--rst-line); |
| + border-radius: var(--rst-radius); |
| + display: flex; |
| + flex-wrap: wrap; |
| +} |
| +.rst-stat, [rst-stat] { |
| + border-inline-start: 1px solid var(--rst-line); |
| + display: flex; |
| + flex: 1 1 9rem; |
| + flex-direction: column; |
| + gap: 0.15rem; |
| + min-inline-size: 0; |
| + padding: var(--rst-sp-4); |
| +} |
| +.rst-stat:first-child, [rst-stat]:first-child { |
| + border-inline-start: 0; |
| +} |
| +.rst-stat--lead, [rst-stat~="lead"] { |
| + flex-basis: 14rem; |
| +} |
| +/* The eyebrow. Uppercase and tracked, the same grammar every label in |
| + this system uses above a value, so a band of cells reads as one row |
| + of labels rather than as four headings. */ |
| +.rst-stat__label, [rst-stat-label] { |
| + color: var(--rst-text-muted); |
| + font-size: var(--rst-fs-xs); |
| + font-weight: 650; |
| + letter-spacing: 0.06em; |
| + text-transform: uppercase; |
| +} |
| +/* tabular-nums so a column of readings lines up on its digits, and so |
| + a number that changes on a poll does not jitter its neighbours. */ |
| +.rst-stat__num, [rst-stat-num] { |
| + font-size: 1.5rem; |
| + font-variant-numeric: tabular-nums; |
| + font-weight: 650; |
| + line-height: 1.1; |
| +} |
| +.rst-stat--lead .rst-stat__num, [rst-stat~="lead"] [rst-stat-num] { |
| + font-size: 2.1rem; |
| +} |
| +/* The delta is TEXT in a colour, not a colour. Its sign is written by |
| + the caller and is the signal a reader who cannot tell the two hues |
| + apart is reading; the colour is the second one (WCAG 2.2 AA 1.4.1). |
| + Both hues are held at 4.5:1 against this card by the contrast gate |
| + in the same commit that added them — they are text, so the floor |
| + applies whether or not the colour carries meaning of its own. */ |
| +.rst-stat__delta, [rst-stat-delta] { |
| + font-size: var(--rst-fs-sm); |
| + font-variant-numeric: tabular-nums; |
| + font-weight: 600; |
| +} |
| +.rst-stat__delta[data-tone="positive"], [rst-stat-delta][rst-tone~="positive"] { |
| + color: var(--rst-tone-positive-fg); |
| +} |
| +.rst-stat__delta[data-tone="negative"], [rst-stat-delta][rst-tone~="negative"] { |
| + color: var(--rst-tone-negative-fg); |
| +} |
| +.rst-stat__delta[data-tone="neutral"], [rst-stat-delta][rst-tone~="neutral"] { |
| + color: var(--rst-text-muted); |
| +} |
| +.rst-stat__note, [rst-stat-note] { |
| + color: var(--rst-text-faint); |
| + font-size: var(--rst-fs-xs); |
| +} |
| + |
| /* person — avatar + name + email; avatar is decoration, text is identity. */ |
| .rst-person, [rst-person] { align-items: center; color: inherit; display: inline-flex; gap: 0.7rem; min-width: 0; text-decoration: none; } |
| .rst-person__av, [rst-person-av] { align-items: center; background: var(--rst-accent); border-radius: 50%; color: var(--rst-on-accent); display: flex; flex: none; font-size: 0.6875rem; font-weight: 600; height: 28px; justify-content: center; width: 28px; } |
| @@ -1039,6 +1212,17 @@ details[open] > summary > .rst-caret, details[open] > summary > [rst-caret] { tr |
| and it is wide enough for the postcodes and codes it is for. The |
| max keeps it from overflowing a column narrower than itself. */ |
| .rst-input--short, [rst-input~="short"] { inline-size: 8rem; max-inline-size: 100%; } |
| +/* primary is the form's headline field — the title or name the record is |
| + known by — and the size is the whole signal, because the label above it |
| + stays the same quiet line every other field gets. font-size has to be |
| + restated rather than inherited: .rst-input sets `font: inherit`, so the |
| + shorthand would otherwise reset it back to the body's 14px. 550 is the |
| + weight .rst-nm and .rst-person__name already give a name; 600 reads as a |
| + heading, which this is not. The padding grows with the type so the box |
| + keeps its proportions instead of looking like a squeezed plain input. |
| + 17px is also where iOS Safari stops zooming a focused input, which the |
| + 14px base does not clear — a real bonus, not the reason. */ |
| +.rst-input--primary, [rst-input~="primary"] { font-size: var(--rst-fs-lg); font-weight: 550; padding: 0.6rem 0.7rem; } |
| textarea.rst-input, textarea[rst-input] { min-height: 5rem; resize: vertical; } |
| /* field-text/field-textarea (the interim pair the manifest generator |
| emits) mark a required label with * and put the textarea on its own |
| @@ -1288,6 +1472,19 @@ dialog.rst-modal-panel, dialog[rst-modal-panel] { block-size: auto; color: inher |
| animation: rst-spin 0.8s linear infinite; |
| } |
| @keyframes rst-spin { to { transform: rotate(1turn); } } |
| + |
| +/* A running job's determinate bar: a native <progress>, styled the same |
| + way and for the same reasons as [rst-meter-bar] — see that block for |
| + why the three engines need three rules that cannot be merged, and for |
| + the same honest flag about the -moz- one being unverified here. |
| + <progress>'s pseudo-elements are its own: ::-webkit-progress-bar is |
| + the track, ::-webkit-progress-value the fill, ::-moz-progress-bar the |
| + fill. It is a block on its own line under the sentence, because it |
| + measures the job the sentence is about. */ |
| +.rst-job__bar, [rst-job-bar] { appearance: none; background: var(--rst-accent-soft); block-size: 4px; border: 0; border-radius: 2px; display: block; inline-size: 100%; margin-block-start: var(--rst-sp-2); } |
| +.rst-job__bar::-webkit-progress-bar, [rst-job-bar]::-webkit-progress-bar { background: var(--rst-accent-soft); border-radius: 2px; } |
| +.rst-job__bar::-webkit-progress-value, [rst-job-bar]::-webkit-progress-value { background: var(--rst-accent); border-radius: 2px; } |
| +.rst-job__bar::-moz-progress-bar, [rst-job-bar]::-moz-progress-bar { background: var(--rst-accent); border-radius: 2px; } |
| @media (prefers-reduced-motion: reduce) { |
| .rst-spin, [rst-spin] { animation: none; opacity: 0.5; } |
| } |
| @@ -1343,6 +1540,72 @@ dialog.rst-modal-panel, dialog[rst-modal-panel] { block-size: auto; color: inher |
| .rst-dtp__set, [rst-dtp-set] { flex: none; font-size: var(--rst-fs-xs); color: var(--rst-muted); } |
| .rst-dtp__hint, [rst-dtp-hint] { color: var(--rst-muted); font-size: var(--rst-fs-xs); padding: 0.35rem 0.65rem 0.15rem; } |
| +/* The popup opens above the field where there is no room below it — |
| + datetime.js measures and sets .is-above, because a panel hanging off |
| + the bottom of the window is a panel whose last week nobody can click. |
| + One rule for both popups: only ever one of them is open. */ |
| +.rst-dtp.is-above .rst-dtp__list, [rst-dtp].is-above [rst-dtp-list], .rst-dtp.is-above .rst-cal, [rst-dtp].is-above [rst-cal] { inset-block-end: 100%; margin-block-end: 4px; margin-block-start: 0; top: auto; } |
| +/* A completion offered rather than chosen — the other months a |
| + half-typed word could still have meant. Lighter than the reading |
| + above it, because it is a second-best by construction. */ |
| +.rst-dtp__row--alt .rst-dtp__label, [rst-dtp-row~="alt"] [rst-dtp-label] { color: var(--rst-text-muted); } |
| + |
| +/* ── The calendar overlay (ui/calendar.js): the month grid a date |
| + field's button opens. A real <table> with real column headers, |
| + because that is what a calendar is, with role="grid" over the top |
| + and a roving tabindex so exactly one day sits in the tab order. |
| + |
| + Sized in rem so the grid keeps its shape when a page is zoomed: |
| + seven columns of text, not seven columns of pixels. A fixed inline |
| + size rather than the field's, because a calendar is a calendar |
| + whatever it hangs under — a month stretched across a wide input is |
| + a grid nobody can aim at. |
| + |
| + Everything here is logical (inline/block, never left/right), so a |
| + right-to-left page mirrors the panel and the grid together. The |
| + chevrons are the one thing that needs saying twice, below. |
| + ───────────────────────────────────────────────────────────────── */ |
| +.rst-cal, [rst-cal] { background: var(--rst-surface); border: 1px solid var(--rst-line); border-radius: 9px; box-shadow: var(--rst-shadow-pop); inline-size: 18rem; inset-inline-start: 0; margin-block-start: 4px; padding: var(--rst-sp-2); position: absolute; top: 100%; z-index: 40; } |
| +.rst-cal__head, [rst-cal-head] { align-items: center; display: flex; gap: var(--rst-sp-1); justify-content: space-between; padding-block-end: var(--rst-sp-2); } |
| +.rst-cal__title, [rst-cal-title] { flex: 1; font-size: var(--rst-fs-sm); font-weight: 600; text-align: center; } |
| +.rst-cal__nav, [rst-cal-nav] { align-items: center; background: none; block-size: 1.75rem; border: 0; border-radius: var(--rst-radius-sm); color: var(--rst-muted); cursor: pointer; display: flex; inline-size: 1.75rem; justify-content: center; padding: 0; } |
| +.rst-cal__nav:hover:not(:disabled), [rst-cal-nav]:hover:not(:disabled) { background: var(--rst-accent-soft); color: var(--rst-text); } |
| +/* A month with nothing pickable in it — past the field's min, or past |
| + its max. Dimmed and inert rather than removed, so the header keeps |
| + its shape and the title stays where the eye left it. */ |
| +.rst-cal__nav:disabled, [rst-cal-nav]:disabled { cursor: default; opacity: 0.35; } |
| +.rst-cal__nav svg, [rst-cal-nav] svg { block-size: 1.1rem; inline-size: 1.1rem; } |
| +/* One chevron, drawn once and turned round for the other direction, so |
| + "back" always points at the start of the line — which is the other |
| + end of the box in a right-to-left page, and handled there by the |
| + panel's own direction rather than a second rule. */ |
| +[rst-cal-nav~="next"] svg, .rst-cal__nav--next svg { transform: scaleX(-1); } |
| +.rst-cal__grid, [rst-cal-grid] { border-collapse: collapse; inline-size: 100%; table-layout: fixed; } |
| +.rst-cal__grid th, [rst-cal-grid] th { color: var(--rst-muted); font-size: var(--rst-fs-xs); font-weight: 500; padding-block: var(--rst-sp-1); text-align: center; } |
| +.rst-cal__day, [rst-cal-day] { border-radius: var(--rst-radius-sm); cursor: pointer; font-size: var(--rst-fs-sm); font-variant-numeric: tabular-nums; line-height: 2.15; text-align: center; } |
| +.rst-cal__day:hover, [rst-cal-day]:hover { background: var(--rst-accent-soft); } |
| +/* The days either side of the month. Still pickable — reaching for the |
| + 1st from the row above it is how people use a calendar — but plainly |
| + not part of the month on screen. */ |
| +.rst-cal__day.is-outside, [rst-cal-day].is-outside { color: var(--rst-text-faint); } |
| +/* Today is a ring rather than a fill, so it can still be read when the |
| + selection is sitting on top of it. */ |
| +.rst-cal__day.is-today, [rst-cal-day].is-today { box-shadow: inset 0 0 0 1px var(--rst-line-strong); font-weight: 600; } |
| +/* What the text in the box currently reads as: an outline that follows |
| + the words as they are typed. Deliberately quieter than the selection |
| + — it is a preview, and nothing has been chosen yet. */ |
| +.rst-cal__day.is-preview, [rst-cal-day].is-preview { box-shadow: inset 0 0 0 2px var(--rst-accent); } |
| +/* The committed value. aria-selected says so in the accessibility tree; |
| + this is the matching visual, never the only signal. */ |
| +.rst-cal__day.is-selected, [rst-cal-day].is-selected { background: var(--rst-accent); color: var(--rst-on-accent); font-weight: 600; } |
| +.rst-cal__day.is-selected.is-outside, [rst-cal-day].is-selected.is-outside { color: var(--rst-on-accent); } |
| +/* Outside the field's own min/max. aria-disabled carries it to a screen |
| + reader and calendar.js refuses the pick; this says the same to the |
| + eye. */ |
| +.rst-cal__day.is-disabled, [rst-cal-day].is-disabled { color: var(--rst-text-faint); cursor: default; text-decoration: line-through; } |
| +.rst-cal__day.is-disabled:hover, [rst-cal-day].is-disabled:hover { background: none; } |
| +.rst-cal__day:focus-visible, [rst-cal-day]:focus-visible { outline: 2px solid var(--rst-accent); outline-offset: -2px; } |
| + |
| /* ── Shells: the page frames an app's layout template wraps .rst-page |
| in — topbar and sidebar. Written in logical properties throughout, |
| so a right-to-left locale gets a mirrored shell for free: the rail |
| @@ -1618,6 +1881,40 @@ dialog.rst-modal-panel, dialog[rst-modal-panel] { block-size: auto; color: inher |
| .rst-error__cta, [rst-error-cta] { display: flex; flex-wrap: wrap; gap: var(--rst-sp-2); justify-content: center; margin-block: var(--rst-sp-5) 0; } |
| .rst-error__ref, [rst-error-ref] { color: var(--rst-text-faint); margin-block: 2rem 0; } |
| +/* --------------------------------------------------------------------- |
| + Touch: the two zooms nobody asked for. |
| + |
| + Mobile Safari zooms the whole page when it focuses a form control |
| + whose text is under 16px, and this file's base type is 14px on |
| + purpose. So every text-entry control comes back up to 16px where the |
| + pointer is coarse, and stays 14px under a mouse. max() rather than a |
| + flat 1rem, so a control deliberately set larger keeps its size; rem |
| + rather than px, so a reader who raised the browser default keeps the |
| + increase. This is the fix. The other one — user-scalable=no in the |
| + viewport tag — buys the same quiet by taking pinch-zoom away from |
| + everyone, which is a WCAG 1.4.4 failure and not on offer here. |
| + |
| + Double-tap is the second zoom: tap a button or a row twice in quick |
| + succession and the second tap lands as a zoom gesture rather than a |
| + click. touch-action: manipulation drops that gesture on interactive |
| + elements only, so panning and pinch-zoom are untouched, and so is |
| + double-tapping prose or an image, which is a real way to read a |
| + narrow column. :where() weighs nothing, so any rule can override it. |
| + --------------------------------------------------------------------- */ |
| + |
| +@media (pointer: coarse) { |
| + input, select, textarea, |
| + .rst-input, [rst-input], |
| + .rst-textarea, [rst-textarea], |
| + .rst-search input[type="search"], [rst-search] input[type="search"] { |
| + font-size: max(1rem, 1em); |
| + } |
| +} |
| + |
| +:where(a[href], button, input, select, textarea, label, summary, [role="button"], [role="tab"], [role="option"]) { |
| + touch-action: manipulation; |
| +} |
| + |
| /* --------------------------------------------------------------------- |
| Utilities, last on purpose. |
diff --git a/internal/memoria/store.go b/internal/memoria/store.go| index 012b8d2..3b29cb2 100644 |
| --- a/internal/memoria/store.go |
| +++ b/internal/memoria/store.go |
| @@ -9,7 +9,7 @@ import ( |
| "strings" |
| "time" |
| - "github.com/carlosframework/rastrillo/db" |
| + "amadan.net/rastrillo/rastrillo/db" |
| "amadan.net/oficina/suite" |
diff --git a/internal/memoriatest/admission_test.go b/internal/memoriatest/admission_test.go| index b238692..db54944 100644 |
| --- a/internal/memoriatest/admission_test.go |
| +++ b/internal/memoriatest/admission_test.go |
| @@ -14,7 +14,7 @@ import ( |
| "sync/atomic" |
| "testing" |
| - "github.com/carlosframework/rastrillo/db" |
| + "amadan.net/rastrillo/rastrillo/db" |
| memoria "amadan.net/oficina/memoria/internal/memoria" |
| ) |
diff --git a/internal/memoriatest/chrome_client_test.go b/internal/memoriatest/chrome_client_test.go| index 394c53b..d58edbb 100644 |
| --- a/internal/memoriatest/chrome_client_test.go |
| +++ b/internal/memoriatest/chrome_client_test.go |
| @@ -10,7 +10,7 @@ import ( |
| "strings" |
| "testing" |
| - "github.com/carlosframework/rastrillo/db" |
| + "amadan.net/rastrillo/rastrillo/db" |
| memoria "amadan.net/oficina/memoria/internal/memoria" |
| ) |
diff --git a/internal/memoriatest/connector_test.go b/internal/memoriatest/connector_test.go| index 11d394a..348cf0a 100644 |
| --- a/internal/memoriatest/connector_test.go |
| +++ b/internal/memoriatest/connector_test.go |
| @@ -11,7 +11,7 @@ import ( |
| "testing" |
| "time" |
| - "github.com/carlosframework/rastrillo/db" |
| + "amadan.net/rastrillo/rastrillo/db" |
| memoria "amadan.net/oficina/memoria/internal/memoria" |
| "amadan.net/oficina/memoria/internal/memoria/notion" |
diff --git a/internal/memoriatest/footer_model_test.go b/internal/memoriatest/footer_model_test.go| index d2ad6f1..652a3b7 100644 |
| --- a/internal/memoriatest/footer_model_test.go |
| +++ b/internal/memoriatest/footer_model_test.go |
| @@ -10,7 +10,7 @@ import ( |
| "strings" |
| "testing" |
| - "github.com/carlosframework/rastrillo/db" |
| + "amadan.net/rastrillo/rastrillo/db" |
| "amadan.net/oficina/memoria/internal/memoria" |
| "amadan.net/oficina/memoria/internal/memoria/provider" |
diff --git a/internal/memoriatest/gates_test.go b/internal/memoriatest/gates_test.go| index 2925b41..3519071 100644 |
| --- a/internal/memoriatest/gates_test.go |
| +++ b/internal/memoriatest/gates_test.go |
| @@ -869,7 +869,7 @@ func TestEveryTemplateAttributeIsInTheDesignSystem(t *testing.T) { |
| // anything one of the vendored scripts reads is a hook by definition, |
| // and pattern-matching that is the honest version of an exemption. |
| js := "" |
| - for _, name := range []string{"rastrillo.js", "search.js", "drop.js", "select.js"} { |
| + for _, name := range []string{"rastrillo.js", "search.js", "drop.js"} { |
| raw, err := os.ReadFile(filepath.Join("..", "memoria", "static", name)) |
| if err != nil { |
| t.Fatalf("read %s: %v", name, err) |
| @@ -924,6 +924,12 @@ func TestEveryTemplateAttributeIsInTheDesignSystem(t *testing.T) { |
| } |
| exempt := map[string]bool{ |
| "rst-withheld": true, // a test hook for §6.5's notice block |
| + // A class-only utility, written class="rst-sr-only" (tokens.css's |
| + // .rst-sr-only; utilities keep the class spelling). This gate reads |
| + // rst- words in class values too, and select.js's mention of it |
| + // used to satisfy it; select.js went when the app moved off |
| + // rastrillo's frozen path (no page links it). |
| + "rst-sr-only": true, |
| } |
| attr := regexp.MustCompile(`rst-[a-z0-9-]+`) |
diff --git a/internal/memoriatest/harness_test.go b/internal/memoriatest/harness_test.go| index ecabc58..f712fa8 100644 |
| --- a/internal/memoriatest/harness_test.go |
| +++ b/internal/memoriatest/harness_test.go |
| @@ -26,7 +26,7 @@ import ( |
| "strings" |
| "testing" |
| - "github.com/carlosframework/rastrillo/db" |
| + "amadan.net/rastrillo/rastrillo/db" |
| memoria "amadan.net/oficina/memoria/internal/memoria" |
| "amadan.net/oficina/memoria/internal/memoria/seal" |
diff --git a/internal/memoriatest/rastrillo_path_test.go b/internal/memoriatest/rastrillo_path_test.go| new file mode 100644 |
| index 0000000..e27ce24 |
| --- /dev/null |
| +++ b/internal/memoriatest/rastrillo_path_test.go |
| @@ -0,0 +1,77 @@ |
| +package memoriatest |
| + |
| +import ( |
| + "go/parser" |
| + "go/token" |
| + "io/fs" |
| + "os" |
| + "path/filepath" |
| + "strings" |
| + "testing" |
| +) |
| + |
| +// preMoveRastrillo is rastrillo's module path before it moved to |
| +// amadan.net/rastrillo/rastrillo (v0.25.0). Nothing after v0.23.0 was |
| +// released under it, so code importing it gets no rastrillo fix, and |
| +// its own vendored-asset check cannot tell. That is how Memoria kept |
| +// zooming iOS on every tap for a month after rastrillo fixed it (polish |
| +// audit r3, item 1). |
| +const preMoveRastrillo = "github.com/carlosframework/rastrillo" |
| + |
| +// preMoveAllowed is the one file that may still import it, and why: |
| +// rastrillo/native's server takes the old path's *sessions.Sessions. |
| +// Delete the entry when native/server moves. |
| +var preMoveAllowed = map[string]string{ |
| + "internal/memoria/native.go": preMoveRastrillo + "/sessions", |
| +} |
| + |
| +// TestNoPreMoveRastrillo fails on any import of rastrillo's old module |
| +// path outside preMoveAllowed, anywhere in the repository's Go. |
| +func TestNoPreMoveRastrillo(t *testing.T) { |
| + root, err := filepath.Abs(filepath.Join("..", "..")) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + fset := token.NewFileSet() |
| + seen := 0 |
| + err = filepath.WalkDir(root, func(path string, d fs.DirEntry, err error) error { |
| + if err != nil { |
| + return err |
| + } |
| + if d.IsDir() { |
| + switch d.Name() { |
| + case ".git", "node_modules", "native", "releases": |
| + return filepath.SkipDir |
| + } |
| + return nil |
| + } |
| + if !strings.HasSuffix(path, ".go") { |
| + return nil |
| + } |
| + src, err := os.ReadFile(path) |
| + if err != nil { |
| + return err |
| + } |
| + f, err := parser.ParseFile(fset, path, src, parser.ImportsOnly) |
| + if err != nil { |
| + return nil // a file that does not parse is the compiler's problem |
| + } |
| + seen++ |
| + rel, _ := filepath.Rel(root, path) |
| + rel = filepath.ToSlash(rel) |
| + for _, im := range f.Imports { |
| + imp := strings.Trim(im.Path.Value, `"`) |
| + if (imp == preMoveRastrillo || strings.HasPrefix(imp, preMoveRastrillo+"/")) && preMoveAllowed[rel] != imp { |
| + t.Errorf("%s: imports %q, rastrillo's frozen pre-move path; use amadan.net/rastrillo/rastrillo", rel, imp) |
| + } |
| + } |
| + return nil |
| + }) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + // The walk found the app, or it would pass by reading nothing. |
| + if seen < 50 { |
| + t.Fatalf("walked only %d Go files from %s; the walk is not reaching the repository", seen, root) |
| + } |
| +} |
diff --git a/internal/memoriatest/signin_keymail_test.go b/internal/memoriatest/signin_keymail_test.go| index ebf25ee..9e3e943 100644 |
| --- a/internal/memoriatest/signin_keymail_test.go |
| +++ b/internal/memoriatest/signin_keymail_test.go |
| @@ -14,7 +14,7 @@ import ( |
| stdsync "sync" |
| "testing" |
| - "github.com/carlosframework/rastrillo/db" |
| + "amadan.net/rastrillo/rastrillo/db" |
| "amadan.net/oficina/memoria/internal/memoria" |
| ) |
diff --git a/internal/memoriatest/signindial_test.go b/internal/memoriatest/signindial_test.go| index e2194bc..8b79c9e 100644 |
| --- a/internal/memoriatest/signindial_test.go |
| +++ b/internal/memoriatest/signindial_test.go |
| @@ -14,7 +14,7 @@ import ( |
| "sync/atomic" |
| "testing" |
| - "github.com/carlosframework/rastrillo/db" |
| + "amadan.net/rastrillo/rastrillo/db" |
| memoria "amadan.net/oficina/memoria/internal/memoria" |
| ) |
diff --git a/internal/memoriatest/vendored_test.go b/internal/memoriatest/vendored_test.go| index 2b13ba1..aef4e7b 100644 |
| --- a/internal/memoriatest/vendored_test.go |
| +++ b/internal/memoriatest/vendored_test.go |
| @@ -12,7 +12,7 @@ import ( |
| "testing" |
| "amadan.net/oficina/estilo.git/theme" |
| - "github.com/carlosframework/rastrillo/ui" |
| + "amadan.net/rastrillo/rastrillo/ui" |
| ) |
| // The scaffold delivered these files once; they are app-owned from |
| @@ -21,10 +21,13 @@ import ( |
| // ~56KB of assets as app diff, and a framework upgrade that forgets |
| // to re-copy is caught instead of drifting silently. If you edit one |
| // DELIBERATELY, delete its line below — the file is yours. |
| +// |
| +// tokens.css is the only one. select.js, datetime.js and calendar.js |
| +// are rastrillo's too, but no page here links them, so they are not on |
| +// disk (TestNoUnusedRastrilloScriptsOnDisk); rastrillo.js is estilo's. |
| func TestVendoredAssetsMatchTheLibrary(t *testing.T) { |
| for name, lib := range map[string][]byte{ |
| "tokens.css": ui.TokensCSS(), |
| - "select.js": ui.SelectJS(), |
| } { |
| vendored, err := os.ReadFile(filepath.Join("..", "memoria", "static", name)) |
| if err != nil { |
| @@ -126,3 +129,19 @@ func TestDoorStylesAreEstilos(t *testing.T) { |
| } |
| } |
| } |
| + |
| +// rastrillo doctor --fix re-copies what the library ships and the app |
| +// lacks, and at 29e4491 that includes the old combined shim (with the |
| +// old busy rule inside) as rastrillo.js, plus select.js, datetime.js and |
| +// calendar.js. Memoria links none of the three, and its rastrillo.js is |
| +// estilo's split shim (TestBusyFilesAreEstilos): the old shim loaded |
| +// beside busy.js makes the two busy rules fight. After a --fix, run |
| +// go run ./scripts/writeestilo and delete what it added; this fails |
| +// until both are done. |
| +func TestNoUnusedRastrilloScriptsOnDisk(t *testing.T) { |
| + for _, name := range []string{"select.js", "datetime.js", "calendar.js"} { |
| + if _, err := os.Stat(filepath.Join("..", "memoria", "static", name)); err == nil { |
| + t.Errorf("static/%s is on disk; no page links it — delete it (rastrillo doctor --fix adds it)", name) |
| + } |
| + } |
| +} |
diff --git a/internal/memoriatest/zoom_browser_test.go b/internal/memoriatest/zoom_browser_test.go| index e176197..cc93e02 100644 |
| --- a/internal/memoriatest/zoom_browser_test.go |
| +++ b/internal/memoriatest/zoom_browser_test.go |
| @@ -24,8 +24,8 @@ import ( |
| "testing" |
| "time" |
| - "github.com/carlosframework/rastrillo/db" |
| - "github.com/carlosframework/rastrillo/harness" |
| + "amadan.net/rastrillo/rastrillo/db" |
| + "amadan.net/rastrillo/rastrillo/harness" |
| "github.com/chromedp/cdproto/emulation" |
| "github.com/chromedp/chromedp" |
diff --git a/tools/drive/main.go b/tools/drive/main.go| index 0eccbbc..b1e3e65 100644 |
| --- a/tools/drive/main.go |
| +++ b/tools/drive/main.go |
| @@ -27,7 +27,7 @@ import ( |
| "time" |
| - "github.com/carlosframework/rastrillo/db" |
| + "amadan.net/rastrillo/rastrillo/db" |
| memoria "amadan.net/oficina/memoria/internal/memoria" |
| "amadan.net/oficina/memoria/internal/memoria/notion" |
diff --git a/tools/seed/main.go b/tools/seed/main.go| index 93f5df5..2c49450 100644 |
| --- a/tools/seed/main.go |
| +++ b/tools/seed/main.go |
| @@ -19,8 +19,8 @@ import ( |
| "path/filepath" |
| "strings" |
| - "github.com/carlosframework/rastrillo/db" |
| - "github.com/carlosframework/rastrillo/migrate" |
| + "amadan.net/rastrillo/rastrillo/db" |
| + "amadan.net/rastrillo/rastrillo/migrate" |
| memoria "amadan.net/oficina/memoria/internal/memoria" |
| "amadan.net/oficina/memoria/internal/memoria/index" |
diff --git a/tools/vendorassets/main.go b/tools/vendorassets/main.go| index ca17873..98d0aa2 100644 |
| --- a/tools/vendorassets/main.go |
| +++ b/tools/vendorassets/main.go |
| @@ -1,7 +1,8 @@ |
| -// Emits rastrillo's vendored front-end assets into internal/memoria/static. |
| -// This is what `rastrillo doctor --fix` does; the CLI installed on this |
| -// machine predates v0.22.0 and has no doctor, so we do it from the |
| -// linked library instead — which is the same source the pin test reads. |
| +// Emits rastrillo's vendored front-end assets into internal/memoria/static: |
| +// tokens.css, the one memoria uses. `go tool rastrillo doctor --fix` does |
| +// the same and more — it also adds select.js, datetime.js and calendar.js, |
| +// which no page links, and overwrites rastrillo.js with the old shim; this |
| +// writes only what the pin test reads. |
| // |
| // Not rastrillo.js: that file comes from estilo's busy package, via |
| // scripts/writeestilo, without the old busy rule. The one in this library |
| @@ -13,14 +14,13 @@ import ( |
| "os" |
| "path/filepath" |
| - "github.com/carlosframework/rastrillo/ui" |
| + "amadan.net/rastrillo/rastrillo/ui" |
| ) |
| func main() { |
| dir := filepath.Join("internal", "memoria", "static") |
| for name, body := range map[string][]byte{ |
| "tokens.css": ui.TokensCSS(), |
| - "select.js": ui.SelectJS(), |
| } { |
| if err := os.WriteFile(filepath.Join(dir, name), body, 0o644); err != nil { |
| log.Fatalf("write %s: %v", name, err) |