Move Calendar off the frozen rastrillo path
Calendar 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's sessions handle for rastrillo/native's server, which still takes the old type. Both packages share the sessions table. TestNoPreMoveRastrillo forbids every other old-path import, and TestNeverSessionsUserID watches both. - go tool rastrillo doctor --fix re-copied tokens.css and datetime.js. select.js was already current. calendar.js is recorded as ours in vendoredIsMine: it is Calendar's working-hours scroll, and rastrillo now vendors a month-grid overlay under the same name, which doctor would otherwise have written over it. rastrillo.js stays estilo's shim (vendoredIsMine, TestBusyFilesAreEstilos). - estilo pinned at main 530cb93 (the bar title's touch size, door copy nits, the danger menu row); writeestilo re-run. - credits regenerated (make credits).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
34 files changed,
+871
−118
AGENTS.md+29 −2Makefile+8 −7cmd/calendar/main.go+2 −2go.mod+3 −3go.sum+2 −2internal/calendar/app.go+4 −4internal/calendar/auth.go+2 −2internal/calendar/chrome_calendar.go+1 −1internal/calendar/config.go+1 −1internal/calendar/credits/notices.json+4 −0internal/calendar/handlers.go+2 −2internal/calendar/handlers_jobs.go+1 −1internal/calendar/home.go+2 −2internal/calendar/home_security_test.go+1 −1internal/calendar/mail_bounded.go+1 −1internal/calendar/migrations.go+2 −2internal/calendar/native.go+10 −2internal/calendar/person.go+1 −1internal/calendar/render.go+3 −3internal/calendar/routes.go+2 −2internal/calendar/schedule_control.go+1 −1internal/calendar/schedule_restore.go+1 −1internal/calendar/scheduled_work.go+1 −1internal/calendar/scheduled_work_test.go+1 −1internal/calendar/static/chrome.css+8 −0internal/calendar/static/datetime.js+420 −60internal/calendar/static/menu.css+4 −0internal/calendar/static/tokens.css+303 −6internal/calendar/viewer.go+2 −2internal/calendartest/browser_test.go+1 −1internal/calendartest/schedule_delivery_test.go+1 −1internal/calendartest/vendored_test.go+9 −1internal/calendartest/zoom_browser_test.go+1 −1internal/guardstest/guards_test.go+37 −1
diff --git a/AGENTS.md b/AGENTS.md| index 491bb16..37c2eeb 100644 |
| --- a/AGENTS.md |
| +++ b/AGENTS.md |
| @@ -216,8 +216,35 @@ relying on remembering one, make it a test instead. |
| everybody runs must not need a toolchain the app does not use. |
| - It runs the feature twice, the second time with **JavaScript |
| disabled**. That pass is the only proof the scriptless floor is real. |
| -- chromedp does not drive in this environment (it parks at |
| - `about:blank`); `TestBrowserWalk` is still dead. Do not assume it ran. |
| +- `make browser` also runs `TestNoZoomOnTap`, through chromedp: every |
| + text control on a touch screen is 16px or more, or iOS zooms the page |
| + on tap. chromedp drives when `RASTRILLO_CHROME` names a Chromium (the |
| + Makefile finds the Playwright cache's on a Mac); it parked at |
| + `about:blank` without one. `TestBrowserWalk` drives now, but fails |
| + at its first sign-in (no link is mailed after its form submit), and |
| + is not in `make browser`; do not assume it ran. |
| +- **A control styled with `font: inherit` under a selector heavier than |
| + a bare `input`** loses tokens.css's 16px-on-touch rule. Restate |
| + `font-size: max(1rem, 1em)` for it under `(pointer: coarse)`, as |
| + calendar.css does for the quick-add box. |
| + |
| +**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 (Calendar |
| + zoomed iOS on every tap for a month that way). `TestNoPreMoveRastrillo` |
| + allows one import of it, native.go's bridge to rastrillo/native's |
| + server, until that server moves; `native_exchange_test.go` proves the |
| + bridge. The CLI is pinned as a `tool` in go.mod: `go tool rastrillo …`, |
| + never a `rastrillo` from PATH. |
| +- **Run `go tool rastrillo doctor` on every rastrillo upgrade**, and |
| + `--fix` to re-copy what drifted (tokens.css, select.js, datetime.js). |
| + It leaves `vendoredIsMine` alone: rastrillo.js (estilo's shim, without |
| + the old busy rule) and theme.css come from estilo through |
| + `go run ./scripts/writeestilo`, and calendar.js is Calendar's OWN |
| + script, not rastrillo's month-grid overlay of the same name. If |
| + `--fix` ever writes rastrillo.js, re-run writeestilo: the old shim |
| + must never load beside busy.js (TestBusyFilesAreEstilos). |
| **Logs** |
| - **No title, place, note or address in any log line, error string or |
diff --git a/Makefile b/Makefile| index 915854c..877d7dd 100644 |
| --- a/Makefile |
| +++ b/Makefile |
| @@ -46,7 +46,7 @@ build: |
| release: version-check |
| @mkdir -p releases |
| CGO_ENABLED=0 GOOS=$(RELEASE_GOOS) GOARCH=$(RELEASE_GOARCH) \ |
| - go build -ldflags="-s -w -X 'github.com/carlosframework/rastrillo.BuildVersion=$(VERSION)'" \ |
| + go build -ldflags="-s -w -X 'amadan.net/rastrillo/rastrillo.BuildVersion=$(VERSION)'" \ |
| -o $(RELEASE_BIN) ./cmd/$(APP) |
| @echo |
| @echo "built $(RELEASE_BIN) reporting version $(VERSION)" |
| @@ -97,13 +97,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/calendar/main.go b/cmd/calendar/main.go| index bc2aa78..a19e6e8 100644 |
| --- a/cmd/calendar/main.go |
| +++ b/cmd/calendar/main.go |
| @@ -18,8 +18,8 @@ import ( |
| "os" |
| "strings" |
| - "github.com/carlosframework/rastrillo" |
| - "github.com/carlosframework/rastrillo/mail" |
| + "amadan.net/rastrillo/rastrillo" |
| + "amadan.net/rastrillo/rastrillo/mail" |
| calendar "amadan.net/oficina/calendar/internal/calendar" |
| ) |
diff --git a/go.mod b/go.mod| index 3bcbec9..f90a106 100644 |
| --- a/go.mod |
| +++ b/go.mod |
| @@ -14,7 +14,7 @@ require ( |
| gorm.io/gorm v1.31.2 |
| ) |
| -require amadan.net/oficina/estilo.git v0.0.0-20261001084202-0b4d92c3dc16 |
| +require amadan.net/oficina/estilo.git v0.0.0-20261001093750-530cb932e768 |
| require ( |
| github.com/BurntSushi/toml v1.6.0 // indirect |
| @@ -42,6 +42,6 @@ require ( |
| modernc.org/sqlite v1.55.0 // indirect |
| ) |
| -tool github.com/carlosframework/rastrillo/cmd/rastrillo |
| - |
| require amadan.net/rastrillo/native/server v0.0.0-20260923173546-2a5d07b2c8d3 |
| + |
| +tool amadan.net/rastrillo/rastrillo/cmd/rastrillo |
diff --git a/go.sum b/go.sum| index 9c8d9d9..2ab2ecd 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/calendar/app.go b/internal/calendar/app.go| index 6fcdc9f..728b30b 100644 |
| --- a/internal/calendar/app.go |
| +++ b/internal/calendar/app.go |
| @@ -12,10 +12,10 @@ import ( |
| "sync" |
| "time" |
| - rauth "github.com/carlosframework/rastrillo/auth" |
| - "github.com/carlosframework/rastrillo/db" |
| - "github.com/carlosframework/rastrillo/migrate" |
| - "github.com/carlosframework/rastrillo/sessions" |
| + rauth "amadan.net/rastrillo/rastrillo/auth" |
| + "amadan.net/rastrillo/rastrillo/db" |
| + "amadan.net/rastrillo/rastrillo/migrate" |
| + "amadan.net/rastrillo/rastrillo/sessions" |
| "amadan.net/oficina/calendar/internal/calendar/seal" |
| ) |
diff --git a/internal/calendar/auth.go b/internal/calendar/auth.go| index 1188633..baf95fd 100644 |
| --- a/internal/calendar/auth.go |
| +++ b/internal/calendar/auth.go |
| @@ -16,8 +16,8 @@ import ( |
| "sync" |
| "sync/atomic" |
| - rauth "github.com/carlosframework/rastrillo/auth" |
| - "github.com/carlosframework/rastrillo/mail" |
| + rauth "amadan.net/rastrillo/rastrillo/auth" |
| + "amadan.net/rastrillo/rastrillo/mail" |
| ) |
| // authSchema aliases rauth.Schema so BootSchema (migrations.go) reads |
diff --git a/internal/calendar/chrome_calendar.go b/internal/calendar/chrome_calendar.go| index 0216ccd..b231000 100644 |
| --- a/internal/calendar/chrome_calendar.go |
| +++ b/internal/calendar/chrome_calendar.go |
| @@ -7,7 +7,7 @@ import ( |
| "amadan.net/oficina/estilo.git/chrome" |
| estilomenu "amadan.net/oficina/estilo.git/menu" |
| - rauth "github.com/carlosframework/rastrillo/auth" |
| + rauth "amadan.net/rastrillo/rastrillo/auth" |
| ) |
| func (h *handlers) viewer(r *http.Request) (rauth.Identity, bool) { |
diff --git a/internal/calendar/config.go b/internal/calendar/config.go| index 4897675..76333cf 100644 |
| --- a/internal/calendar/config.go |
| +++ b/internal/calendar/config.go |
| @@ -8,7 +8,7 @@ import ( |
| "net/url" |
| "strings" |
| - "github.com/carlosframework/rastrillo/mail" |
| + "amadan.net/rastrillo/rastrillo/mail" |
| ) |
| // DefaultOrigin is what an Env == "development" instance falls back to |
diff --git a/internal/calendar/credits/notices.json b/internal/calendar/credits/notices.json| index 78702c2..751ad94 100644 |
| --- a/internal/calendar/credits/notices.json |
| +++ b/internal/calendar/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/calendar/handlers.go b/internal/calendar/handlers.go| index 7758479..31602b0 100644 |
| --- a/internal/calendar/handlers.go |
| +++ b/internal/calendar/handlers.go |
| @@ -4,8 +4,8 @@ import ( |
| "log/slog" |
| "net/http" |
| - rauth "github.com/carlosframework/rastrillo/auth" |
| - "github.com/carlosframework/rastrillo/mail" |
| + rauth "amadan.net/rastrillo/rastrillo/auth" |
| + "amadan.net/rastrillo/rastrillo/mail" |
| ) |
| // handlers holds what every handler needs. One value, built in New. |
diff --git a/internal/calendar/handlers_jobs.go b/internal/calendar/handlers_jobs.go| index 0c22617..4dd0c33 100644 |
| --- a/internal/calendar/handlers_jobs.go |
| +++ b/internal/calendar/handlers_jobs.go |
| @@ -7,7 +7,7 @@ import ( |
| "net/http" |
| "time" |
| - "github.com/carlosframework/rastrillo/carlos" |
| + "amadan.net/rastrillo/rastrillo/carlos" |
| ) |
| // drainReminders is the platform's scheduled-work handler: POST |
diff --git a/internal/calendar/home.go b/internal/calendar/home.go| index 4f9b8d7..32d72cf 100644 |
| --- a/internal/calendar/home.go |
| +++ b/internal/calendar/home.go |
| @@ -9,8 +9,8 @@ import ( |
| "amadan.net/oficina/home/connect" |
| "amadan.net/oficina/home/connect/linkstore" |
| - "github.com/carlosframework/rastrillo/csrf" |
| - "github.com/carlosframework/rastrillo/sessions" |
| + "amadan.net/rastrillo/rastrillo/csrf" |
| + "amadan.net/rastrillo/rastrillo/sessions" |
| ) |
| type homeConnection struct { |
diff --git a/internal/calendar/home_security_test.go b/internal/calendar/home_security_test.go| index db21388..4ef6307 100644 |
| --- a/internal/calendar/home_security_test.go |
| +++ b/internal/calendar/home_security_test.go |
| @@ -1,7 +1,7 @@ |
| package calendar |
| import ( |
| - "github.com/carlosframework/rastrillo" |
| + "amadan.net/rastrillo/rastrillo" |
| "net/http" |
| "net/http/httptest" |
| "strings" |
diff --git a/internal/calendar/mail_bounded.go b/internal/calendar/mail_bounded.go| index 3e4bced..b2930eb 100644 |
| --- a/internal/calendar/mail_bounded.go |
| +++ b/internal/calendar/mail_bounded.go |
| @@ -5,7 +5,7 @@ import ( |
| "errors" |
| "time" |
| - "github.com/carlosframework/rastrillo/mail" |
| + "amadan.net/rastrillo/rastrillo/mail" |
| ) |
| // ErrMailBusy is what a send returns when every in-flight slot of the |
diff --git a/internal/calendar/migrations.go b/internal/calendar/migrations.go| index f07e315..a72e45d 100644 |
| --- a/internal/calendar/migrations.go |
| +++ b/internal/calendar/migrations.go |
| @@ -3,8 +3,8 @@ package calendar |
| import ( |
| "embed" |
| - "github.com/carlosframework/rastrillo/migrate" |
| - "github.com/carlosframework/rastrillo/sessions" |
| + "amadan.net/rastrillo/rastrillo/migrate" |
| + "amadan.net/rastrillo/rastrillo/sessions" |
| ) |
| //go:embed migrations/*.sql |
diff --git a/internal/calendar/native.go b/internal/calendar/native.go| index 2606d8d..6d892ef 100644 |
| --- a/internal/calendar/native.go |
| +++ b/internal/calendar/native.go |
| @@ -8,12 +8,20 @@ import ( |
| "time" |
| nativeauth "amadan.net/rastrillo/native/server" |
| - "github.com/carlosframework/rastrillo/sessions" |
| "github.com/go-chi/chi/v5" |
| + |
| + // 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() error { |
| - s, err := sessions.New(sessions.Config{DB: a.db.Writer(), Origin: a.cfg.Origin, SigninPath: "/signin", Logger: a.logger}) |
| + s, err := presessions.New(presessions.Config{DB: a.db.Writer(), Origin: a.cfg.Origin, SigninPath: "/signin", Logger: a.logger}) |
| if err != nil { |
| return err |
| } |
diff --git a/internal/calendar/person.go b/internal/calendar/person.go| index b35370c..32d0b19 100644 |
| --- a/internal/calendar/person.go |
| +++ b/internal/calendar/person.go |
| @@ -2,8 +2,8 @@ package calendar |
| import ( |
| "amadan.net/oficina/home/connect" |
| + "amadan.net/rastrillo/rastrillo/sessions" |
| "context" |
| - "github.com/carlosframework/rastrillo/sessions" |
| "net/http" |
| ) |
diff --git a/internal/calendar/render.go b/internal/calendar/render.go| index da8593d..5af6b8a 100644 |
| --- a/internal/calendar/render.go |
| +++ b/internal/calendar/render.go |
| @@ -10,9 +10,9 @@ import ( |
| "html/template" |
| "net/http" |
| - "github.com/carlosframework/rastrillo" |
| - "github.com/carlosframework/rastrillo/sessions" |
| - "github.com/carlosframework/rastrillo/ui" |
| + "amadan.net/rastrillo/rastrillo" |
| + "amadan.net/rastrillo/rastrillo/sessions" |
| + "amadan.net/rastrillo/rastrillo/ui" |
| "amadan.net/oficina/calendar/internal/calendar/icons" |
| ) |
diff --git a/internal/calendar/routes.go b/internal/calendar/routes.go| index a5162ee..421decf 100644 |
| --- a/internal/calendar/routes.go |
| +++ b/internal/calendar/routes.go |
| @@ -10,8 +10,8 @@ import ( |
| "github.com/go-chi/chi/v5" |
| - rauth "github.com/carlosframework/rastrillo/auth" |
| - "github.com/carlosframework/rastrillo/csrf" |
| + rauth "amadan.net/rastrillo/rastrillo/auth" |
| + "amadan.net/rastrillo/rastrillo/csrf" |
| ) |
| // routes builds the whole router. |
diff --git a/internal/calendar/schedule_control.go b/internal/calendar/schedule_control.go| index 62882c5..f49aa4c 100644 |
| --- a/internal/calendar/schedule_control.go |
| +++ b/internal/calendar/schedule_control.go |
| @@ -9,7 +9,7 @@ import ( |
| "net/http" |
| "os" |
| - "github.com/carlosframework/rastrillo/carlos" |
| + "amadan.net/rastrillo/rastrillo/carlos" |
| ) |
| const scheduleUnsupportedMessage = "calendar: this platform does not support safe scheduled work; update the platform before starting Calendar" |
diff --git a/internal/calendar/schedule_restore.go b/internal/calendar/schedule_restore.go| index aaeee1b..4f82dfe 100644 |
| --- a/internal/calendar/schedule_restore.go |
| +++ b/internal/calendar/schedule_restore.go |
| @@ -5,7 +5,7 @@ import ( |
| "errors" |
| "time" |
| - "github.com/carlosframework/rastrillo/carlos" |
| + "amadan.net/rastrillo/rastrillo/carlos" |
| ) |
| func (s *scheduledWork) matches(remote remoteSchedule) bool { |
diff --git a/internal/calendar/scheduled_work.go b/internal/calendar/scheduled_work.go| index a8c6556..2a1bb17 100644 |
| --- a/internal/calendar/scheduled_work.go |
| +++ b/internal/calendar/scheduled_work.go |
| @@ -7,7 +7,7 @@ import ( |
| "sync" |
| "time" |
| - "github.com/carlosframework/rastrillo/carlos" |
| + "amadan.net/rastrillo/rastrillo/carlos" |
| ) |
| const scheduledWorkName = "calendar-next-due" |
diff --git a/internal/calendar/scheduled_work_test.go b/internal/calendar/scheduled_work_test.go| index 96ae657..2fb1aa2 100644 |
| --- a/internal/calendar/scheduled_work_test.go |
| +++ b/internal/calendar/scheduled_work_test.go |
| @@ -10,7 +10,7 @@ import ( |
| "testing" |
| "time" |
| - "github.com/carlosframework/rastrillo/carlos" |
| + "amadan.net/rastrillo/rastrillo/carlos" |
| ) |
| type scheduleRecorder struct { |
diff --git a/internal/calendar/static/chrome.css b/internal/calendar/static/chrome.css| index 6bda384..74bd3d7 100644 |
| --- a/internal/calendar/static/chrome.css |
| +++ b/internal/calendar/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/calendar/static/datetime.js b/internal/calendar/static/datetime.js| index c95890a..285d8ea 100644 |
| --- a/internal/calendar/static/datetime.js |
| +++ b/internal/calendar/static/datetime.js |
| @@ -24,7 +24,12 @@ |
| data-rst-date-hint guidance under the list, {example} |
| substituted here with a date this |
| locale's own formatter wrote |
| - data-rst-date-pick label for the native-picker button |
| + data-rst-date-pick label for the calendar button |
| + data-rst-date-pick-time label for a TIME field's button, which |
| + opens a clock rather than a calendar |
| + data-rst-date-calendar the calendar overlay's own name |
| + data-rst-date-prev-month label for the overlay's back chevron |
| + data-rst-date-next-month label for the overlay's forward one |
| data-rst-date-results live-region text, {n} substituted |
| data-rst-date-result-one live-region text for exactly one |
| data-rst-date-quick-* the seven quick-pick labels: today, |
| @@ -58,7 +63,22 @@ |
| The pure half is exported for Node at the bottom of the file, behind |
| a guard a browser never takes; ui/datetime_node.mjs drives it |
| - against ui/testdata/datetime/*.json. */ |
| + against ui/testdata/datetime/*.json. |
| + |
| + Two things live NEXT to the strict grammar rather than inside it. |
| + parseAll offers a reading where the grammar refuses one, by finishing |
| + a half-typed word into the names this locale actually has: "5j" is |
| + not a date, and 5 January, 5 June and 5 July are three, nearest |
| + first. It never overrules the grammar — an exact reading wins |
| + outright — and every completion is shown with its date written out |
| + before anyone commits it. |
| + |
| + And the calendar itself is ui/calendar.js, a sibling file. This one |
| + reads and writes dates as TEXT; that one draws six weeks of them. |
| + The seam is window.rastrilloCalendar, looked up at enhance time, so |
| + an app that ships this file without that one still has a working |
| + field — the button falls back to the browser's own picker, the same |
| + way everything here falls back when an attribute is missing. */ |
| (function () { |
| "use strict"; |
| @@ -399,6 +419,20 @@ |
| return list; |
| } |
| + // The index for this table, built once and kept on it. Cached |
| + // against the vocabulary itself, not just the locale: a page could |
| + // in principle carry two, and a stale index would parse the second |
| + // field with the first field's words. |
| + function indexFor(vocab, tbl) { |
| + var key = "", name; |
| + for (name in vocab) if (Object.prototype.hasOwnProperty.call(vocab, name)) key += name + "=" + vocab[name] + "\u0000"; |
| + if (tbl.indexKey !== key) { |
| + tbl.index = index(vocab, tbl); |
| + tbl.indexKey = key; |
| + } |
| + return tbl.index; |
| + } |
| + |
| // A separator is never a letter, whatever block it lives in. The |
| // Arabic comma sits at \u060c, inside the same range as the letters |
| // either side of it, so a class drawn by range alone reads |
| @@ -444,8 +478,15 @@ |
| // being counted ("3 months"). A fact about the typing, not about |
| // any language, which is why it can be recorded without knowing |
| // which language was typed. |
| - function push(tok) { |
| + // |
| + // Where the token was read is recorded alongside it, because |
| + // completion below rewrites exactly one token and hands the |
| + // sentence back to the grammar unchanged around it. A scanner that |
| + // forgets where it read a word cannot offer to finish it. |
| + function push(tok, from, to) { |
| tok.glued = glued; |
| + tok.from = from; |
| + tok.to = to; |
| toks.push(tok); |
| glued = true; |
| } |
| @@ -453,15 +494,15 @@ |
| if (BREAKS.test(s.charAt(pos))) { pos++; glued = false; continue; } |
| m = /^(\d{1,2}):(\d{2})/.exec(s.slice(pos)); |
| if (m) { |
| - push({ t: "clock", h: +m[1], mi: +m[2] }); |
| + push({ t: "clock", h: +m[1], mi: +m[2] }, pos, pos + m[0].length); |
| pos += m[0].length; |
| continue; |
| } |
| hit = matchAt(s, pos, idx); |
| - if (hit) { push(hit.tok); pos += hit.len; continue; } |
| + if (hit) { push(hit.tok, pos, pos + hit.len); pos += hit.len; continue; } |
| m = /^\d+/.exec(s.slice(pos)); |
| if (m) { |
| - push({ t: "num", n: +m[0], digits: m[0].length }); |
| + push({ t: "num", n: +m[0], digits: m[0].length }, pos, pos + m[0].length); |
| pos += m[0].length; |
| continue; |
| } |
| @@ -471,7 +512,7 @@ |
| pos++; |
| } |
| if (pos === start) pos++; |
| - push({ t: "other", raw: s.slice(start, pos) }); |
| + push({ t: "other", raw: s.slice(start, pos) }, start, pos); |
| } |
| return toks; |
| } |
| @@ -491,16 +532,7 @@ |
| ctx = ctx || {}; |
| var raw = tbl.fold(text); |
| if (!raw) return null; |
| - // Cached against the vocabulary itself, not just the locale: a |
| - // page could in principle carry two, and a stale index would parse |
| - // the second field with the first field's words. |
| - var key = ""; |
| - for (var name in vocab) if (Object.prototype.hasOwnProperty.call(vocab, name)) key += name + "=" + vocab[name] + "\u0000"; |
| - if (tbl.indexKey !== key) { |
| - tbl.index = index(vocab, tbl); |
| - tbl.indexKey = key; |
| - } |
| - var idx = tbl.index; |
| + var idx = indexFor(vocab, tbl); |
| var prev = ctx.prev || null; |
| var base = ctx.base || null; |
| @@ -916,6 +948,92 @@ |
| return out ? { date: out, hasDate: hasDate, hasTime: hasTime } : null; |
| } |
| + /* ── completion ────────────────────────────────────────────────── |
| + The grammar above is exact, and says so: one word it does not know |
| + makes the whole reading null. That refusal is the promise which |
| + keeps this field from ever committing a date nobody typed, and it |
| + is not up for negotiation. |
| + |
| + Completion is the other half of that bargain. It never overrules a |
| + reading — an exact one wins outright and nothing below runs — it |
| + only offers one where the grammar had none. "5j" is not a date. |
| + "5 January", "5 June" and "5 July" are three, and putting all |
| + three on screen with their dates written out in full is precisely |
| + how a guess stops being a guess: the person reads the answer |
| + before they accept it. |
| + |
| + Nearest first, which is the whole of "the fifth of the next month |
| + starting with j": inferYear already rolls a bare day-and-month |
| + forward to its next occurrence, so ordering the readings by date |
| + IS that rule. Nothing here knows what a month is called. */ |
| + |
| + // How many finishings one unfinished word may offer. A single letter |
| + // is the front of three months in English, and of more than that |
| + // once the weekdays and the vocabulary are in; a list nobody can |
| + // read at a glance has stopped being a suggestion. |
| + var MAX_COMPLETIONS = 6; |
| + |
| + // Every word in this locale's index that the run could still become. |
| + // A prefix, never an infix: people type the front of a word, so "ju" |
| + // finishes to June and July and "une" finishes to nothing. |
| + // |
| + // Two kinds are skipped. A literal is punctuation Intl threads |
| + // between numbers, which nobody is halfway through typing; and "at" |
| + // is optional everywhere it appears, so finishing a word INTO it |
| + // changes no reading and only spends a row. |
| + function completionsFor(run, idx) { |
| + var out = [], seen = {}, i, e; |
| + if (!run) return out; |
| + for (i = 0; i < idx.length; i++) { |
| + e = idx[i]; |
| + if (e.role === "word" && (e.kind === "ignore" || e.kind === "at")) continue; |
| + if (e.text.length <= run.length) continue; |
| + if (e.text.slice(0, run.length) !== run) continue; |
| + if (seen[e.text]) continue; |
| + seen[e.text] = true; |
| + out.push(e.text); |
| + } |
| + return out; |
| + } |
| + |
| + // parseAll(text, vocab, tbl, now, ctx) -> [reading, ...], best first, |
| + // and empty where the grammar and the completions both come back |
| + // with nothing. This is what the field itself reads with; parse |
| + // stays exactly as strict as it was, and the read-back gate still |
| + // holds it to reading its own writing with no help from here. |
| + function parseAll(text, vocab, tbl, now, ctx) { |
| + var exact = parse(text, vocab, tbl, now, ctx); |
| + if (exact) return [exact]; |
| + var raw = tbl.fold(text); |
| + if (!raw) return []; |
| + var idx = indexFor(vocab, tbl); |
| + var toks = scan(raw, idx), at = -1, i; |
| + for (i = 0; i < toks.length; i++) { |
| + if (toks[i].t !== "other") continue; |
| + // Two unfinished words is not a typo anybody is mid-way through, |
| + // it is prose — and finishing both would multiply out into a |
| + // list of dates nobody asked for. |
| + if (at >= 0) return []; |
| + at = i; |
| + } |
| + if (at < 0) return []; |
| + var names = completionsFor(toks[at].raw, idx), out = [], seen = {}, read, key; |
| + for (i = 0; i < names.length; i++) { |
| + // The sentence handed back to the grammar with that one run |
| + // finished, spaced off on both sides so a finished word cannot |
| + // glue itself onto its neighbour and mean something else. |
| + read = parse(raw.slice(0, toks[at].from) + " " + names[i] + " " + raw.slice(toks[at].to), |
| + vocab, tbl, now, ctx); |
| + if (!read) continue; |
| + key = String(read.date.getTime()) + (read.hasTime ? "t" : ""); |
| + if (seen[key]) continue; |
| + seen[key] = true; |
| + out.push(read); |
| + } |
| + out.sort(function (a, b) { return a.date - b.date; }); |
| + return out.slice(0, MAX_COMPLETIONS); |
| + } |
| + |
| /* ── the page ──────────────────────────────────────────────────── |
| Everything below is behind the document guard, so Node loads the |
| parser above and nothing else. */ |
| @@ -926,6 +1044,12 @@ |
| 'stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false">' + |
| '<rect x="3" y="4" width="18" height="18" rx="2"/><path d="M16 2v4M8 2v4M3 10h18"/></svg>'; |
| + // A time field's button opens a clock, not a calendar, and says so |
| + // with the one glyph everybody reads the same way. |
| + var CLOCK_SVG = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" ' + |
| + 'stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false">' + |
| + '<circle cx="12" cy="12" r="9"/><path d="M12 7v5l3 2"/></svg>'; |
| + |
| var seq = 0; |
| function toWire(kind, d) { |
| @@ -998,6 +1122,10 @@ |
| var setLabel = attr(native, "data-rst-date-set", "Set"); |
| var hintText = attr(native, "data-rst-date-hint", ""); |
| var pickLabel = attr(native, "data-rst-date-pick", "Open the calendar"); |
| + var clockLabel = attr(native, "data-rst-date-pick-time", "Open the clock"); |
| + var calLabel = attr(native, "data-rst-date-calendar", "Calendar"); |
| + var prevLabel = attr(native, "data-rst-date-prev-month", "Previous month"); |
| + var nextLabel = attr(native, "data-rst-date-next-month", "Next month"); |
| var manyFmt = attr(native, "data-rst-date-results", "{n} suggestions"); |
| var oneFmt = attr(native, "data-rst-date-result-one", "1 suggestion"); |
| var quick = { |
| @@ -1057,21 +1185,12 @@ |
| native.setAttribute("tabindex", "-1"); |
| native.setAttribute("aria-hidden", "true"); |
| - var pick = null; |
| - if (canPick(native)) { |
| - pick = document.createElement("button"); |
| - pick.type = "button"; |
| - pick.setAttribute("rst-dtp-pick", ""); |
| - pick.setAttribute("aria-label", pickLabel); |
| - pick.innerHTML = CAL_SVG; |
| - wrap.insertBefore(pick, list); |
| - } |
| - |
| var rows = []; // [{li, date, hasTime}] — selectable rows only |
| var active = -1; |
| var baseOf = null; // range end: () => Date|null, the start's value |
| var onCommit = null; |
| var isEnd = false; |
| + var mine = false; // true while WE are the ones writing to the carrier |
| function current() { return fromWire(kind, native.value, new Date()); } |
| function show(d) { return fmt ? fmt.format(d) : toWire(kind, d); } |
| @@ -1087,10 +1206,10 @@ |
| else input.removeAttribute("aria-activedescendant"); |
| } |
| - // extra is the row's variant ("set") or a second attribute |
| - // ("rst-dtp-quick"): the vocabulary spells a variant in the |
| - // value slot and a kind as an attribute of its own, so one |
| - // parameter cannot be both, and the caller says which. |
| + // extra is the row's variant ("set", "alt") or a second attribute |
| + // ("rst-dtp-quick"): the vocabulary spells a variant in the value |
| + // slot and a kind as an attribute of its own, so one parameter |
| + // cannot be both, and the caller says which. |
| function addRow(label, meta, date, hasTime, variant, extra) { |
| var li = document.createElement("li"); |
| li.setAttribute("rst-dtp-row", variant || ""); |
| @@ -1155,11 +1274,22 @@ |
| list.textContent = ""; |
| rows = []; |
| var text = String(query || "").trim(); |
| - var read = text ? parse(text, vocab, tbl, now, ctx()) : null; |
| - if (read) addRow(show(read.date), setLabel, read.date, read.hasTime, "set"); |
| - var quickPicks = picks(now), i, skip = read ? toWire(kind, read.date) : ""; |
| + var reads = text ? parseAll(text, vocab, tbl, now, ctx()) : []; |
| + var i, skip = {}, wire; |
| + for (i = 0; i < reads.length; i++) { |
| + // The first reading is the one Enter takes, and it is |
| + // labelled as such. The rest are the other months a |
| + // half-typed word could still have meant — offered, never |
| + // chosen, each with its date written out in full, which is |
| + // what keeps a completion from being a guess. |
| + addRow(show(reads[i].date), i === 0 ? setLabel : "", reads[i].date, reads[i].hasTime, |
| + i === 0 ? "set" : "alt"); |
| + skip[toWire(kind, reads[i].date)] = true; |
| + } |
| + var quickPicks = picks(now); |
| for (i = 0; i < quickPicks.length; i++) { |
| - if (toWire(kind, quickPicks[i].date) === skip) continue; |
| + wire = toWire(kind, quickPicks[i].date); |
| + if (skip[wire]) continue; |
| addRow(quickPicks[i].label, show(quickPicks[i].date), quickPicks[i].date, kind !== "date", "", "rst-dtp-quick"); |
| } |
| if (hintText) { |
| @@ -1170,30 +1300,196 @@ |
| list.appendChild(hint); |
| } |
| status.textContent = rows.length === 1 ? oneFmt : manyFmt.replace("{n}", String(rows.length)); |
| - // Text that failed to parse leaves NOTHING armed: a quick pick |
| + // Text that read as nothing leaves NOTHING armed: a quick pick |
| // sitting under the cursor would make Enter on a date the |
| // grammar did not understand commit today instead of refusing. |
| - setActive(rows.length && (read || !text) ? 0 : -1); |
| + setActive(rows.length && (reads.length || !text) ? 0 : -1); |
| + } |
| + |
| + // What the carrier itself allows, plus — for a range's end — the |
| + // start it must not come before. Asked fresh on every question |
| + // rather than captured once, because the start moves. |
| + function bounds() { |
| + var when = new Date(); |
| + var lo = fromWire(kind, native.getAttribute("min"), when); |
| + var hi = fromWire(kind, native.getAttribute("max"), when); |
| + var start = baseOf ? baseOf() : null; |
| + if (isEnd && start && (!lo || start > lo)) lo = midnight(start); |
| + return { min: lo, max: hi }; |
| + } |
| + |
| + // What the text in the box currently reads as, committing |
| + // nothing. Null where it reads as nothing at all. |
| + function previewOf(text) { |
| + var t = String(text || "").trim(); |
| + if (!t) return null; |
| + var reads = parseAll(t, vocab, tbl, new Date(), ctx()); |
| + return reads.length ? reads[0].date : null; |
| + } |
| + |
| + // The calendar overlay, when calendar.js is on the page. Looked |
| + // up here rather than at load time so the two files may arrive |
| + // in either order: both are deferred and this runs on |
| + // DOMContentLoaded, by which point both have executed. |
| + // |
| + // Absent, the button falls back to the browser's own panel — |
| + // the same posture this file takes towards a missing attribute. |
| + // A page that links one script and not the other gets the old |
| + // behaviour rather than a dead control. |
| + var makeCal = (typeof window !== "undefined" && window.rastrilloCalendar) || null; |
| + var cal = null; |
| + |
| + if (makeCal && kind !== "time") { |
| + cal = makeCal({ |
| + id: id, |
| + locale: locale, |
| + labels: { calendar: calLabel, prev: prevLabel, next: nextLabel }, |
| + value: current, |
| + bounds: bounds, |
| + onPick: function (day) { |
| + // A day chosen on the grid keeps the clock the field |
| + // already holds: picking the 5th for a meeting says |
| + // nothing about what time it starts, and throwing the time |
| + // away would be a second edit nobody asked for. |
| + var had = current(); |
| + commit(new Date(day.getFullYear(), day.getMonth(), day.getDate(), |
| + had ? had.getHours() : 9, had ? had.getMinutes() : 0), kind !== "date"); |
| + }, |
| + onDismiss: function () { |
| + closeAll(); |
| + input.focus(); |
| + } |
| + }); |
| + cal.el.id = id + "-cal"; |
| + wrap.appendChild(cal.el); |
| + } |
| + |
| + var pick = null; |
| + if (cal || kind === "time" || canPick(native)) { |
| + pick = document.createElement("button"); |
| + pick.type = "button"; |
| + pick.setAttribute("rst-dtp-pick", ""); |
| + pick.setAttribute("aria-label", kind === "time" ? clockLabel : pickLabel); |
| + pick.setAttribute("aria-expanded", "false"); |
| + pick.setAttribute("aria-haspopup", cal ? "dialog" : "listbox"); |
| + pick.innerHTML = kind === "time" ? CLOCK_SVG : CAL_SVG; |
| + wrap.insertBefore(pick, list); |
| + } |
| + |
| + /* ── which popup is open ───────────────────────────────────── |
| + One region under the field, in one of three states, so there |
| + is only ever one thing to close, one thing the arrow keys |
| + drive, and one Escape: |
| + |
| + "list" the suggestions — readings, quick picks, the hint |
| + "cal" the month grid, on a date or datetime field |
| + "slots" every half hour, on a time field |
| + |
| + The calendar REPLACES the suggestions rather than sitting |
| + beside them, because two popups under one field is two things |
| + the same keystroke could mean. The words keep working while it |
| + is open: typing moves the grid, and Enter still commits what |
| + the text reads as. */ |
| + |
| + var mode = ""; |
| + |
| + function closeAll() { |
| + mode = ""; |
| + list.hidden = true; |
| + active = -1; |
| + input.setAttribute("aria-expanded", "false"); |
| + input.setAttribute("aria-controls", listId); |
| + input.removeAttribute("aria-activedescendant"); |
| + if (cal) cal.close(); |
| + if (pick) pick.setAttribute("aria-expanded", "false"); |
| + wrap.classList.remove("is-above"); |
| } |
| - function open() { |
| + // Which side of the field the popup opens on. Below by default, |
| + // above where there is no room below and there is room above: a |
| + // panel hanging off the bottom of the window is a panel whose |
| + // last week nobody can click. |
| + function place() { |
| + var panel = mode === "cal" && cal ? cal.el : list; |
| + wrap.classList.remove("is-above"); |
| + if (!panel || panel.hidden) return; |
| + var box = wrap.getBoundingClientRect(); |
| + var need = panel.getBoundingClientRect().height; |
| + if (need > window.innerHeight - box.bottom && box.top > need) { |
| + wrap.classList.add("is-above"); |
| + } |
| + } |
| + |
| + function openList() { |
| + if (cal) cal.close(); |
| + mode = "list"; |
| list.hidden = false; |
| input.setAttribute("aria-expanded", "true"); |
| + input.setAttribute("aria-controls", listId); |
| + if (pick) pick.setAttribute("aria-expanded", "false"); |
| rebuild(input.value); |
| + place(); |
| } |
| - function close() { |
| + function openCal() { |
| list.hidden = true; |
| - input.setAttribute("aria-expanded", "false"); |
| - input.removeAttribute("aria-activedescendant"); |
| active = -1; |
| + input.removeAttribute("aria-activedescendant"); |
| + mode = "cal"; |
| + // Opened on what the text already says, so somebody who typed |
| + // "next march" and then reached for the calendar arrives in |
| + // March rather than in this month. |
| + cal.open(previewOf(input.value)); |
| + input.setAttribute("aria-expanded", "true"); |
| + input.setAttribute("aria-controls", cal.el.id); |
| + if (pick) pick.setAttribute("aria-expanded", "true"); |
| + place(); |
| + } |
| + |
| + // A time field has no calendar, so its button opens the clock: |
| + // every half hour of its own day, written in the field's own |
| + // locale and scrolled to the one it is already showing. The same |
| + // list element and the same keys as the suggestions — one popup, |
| + // one Escape, one set of arrows. |
| + function openSlots() { |
| + var base = current() || new Date(); |
| + var day = midnight(base); |
| + var want = base.getHours() * 60 + base.getMinutes(); |
| + var i, at, best = -1, near = 0, gap; |
| + mode = "slots"; |
| + list.hidden = false; |
| + list.textContent = ""; |
| + rows = []; |
| + for (i = 0; i < 48; i++) { |
| + at = new Date(day.getFullYear(), day.getMonth(), day.getDate(), |
| + Math.floor(i / 2), (i % 2) * 30); |
| + gap = Math.abs(at.getHours() * 60 + at.getMinutes() - want); |
| + if (best < 0 || gap < best) { best = gap; near = rows.length; } |
| + addRow(show(at), "", at, true, "", "rst-dtp-quick"); |
| + } |
| + input.setAttribute("aria-expanded", "true"); |
| + input.setAttribute("aria-controls", listId); |
| + if (pick) pick.setAttribute("aria-expanded", "true"); |
| + status.textContent = manyFmt.replace("{n}", String(rows.length)); |
| + setActive(near); |
| + place(); |
| + // The nearest half hour put where the eye lands. The list's own |
| + // scrollTop rather than scrollIntoView, which would also scroll |
| + // the page out from under the field. |
| + if (rows[near]) { |
| + list.scrollTop = rows[near].li.offsetTop - Math.round(list.clientHeight / 2); |
| + } |
| } |
| function commit(date, hasTime) { |
| + // Ours, so the change listener below leaves the repaint alone: |
| + // this path has already painted. |
| + mine = true; |
| native.value = toWire(kind, date); |
| native.dispatchEvent(new Event("change", { bubbles: true })); |
| + mine = false; |
| input.value = show(date); |
| - close(); |
| + closeAll(); |
| if (onCommit) onCommit(date, hasTime); |
| } |
| @@ -1205,43 +1501,58 @@ |
| // would with no script at all. |
| input.value = ""; |
| if (native.value) { |
| + mine = true; |
| native.value = ""; |
| native.dispatchEvent(new Event("change", { bubbles: true })); |
| + mine = false; |
| } |
| return; |
| } |
| - var read = parse(text, vocab, tbl, new Date(), ctx()); |
| - if (read) commit(read.date, read.hasTime); |
| + var reads = parseAll(text, vocab, tbl, new Date(), ctx()); |
| + if (reads.length) commit(reads[0].date, reads[0].hasTime); |
| else paint(); // unreadable text is not a value: put the old one back |
| } |
| input.addEventListener("focus", function () { |
| input.select(); |
| - open(); |
| + if (!mode) openList(); |
| + }); |
| + input.addEventListener("click", function () { |
| + if (!mode) openList(); |
| + }); |
| + input.addEventListener("input", function () { |
| + // The live half of the brief: while the grid is up, the words |
| + // drive it. Every keystroke re-reads the text and moves the |
| + // calendar to what it now says, committing nothing and taking |
| + // focus out of nothing. |
| + if (mode === "cal" && cal) return cal.show(previewOf(input.value)); |
| + openList(); |
| }); |
| - input.addEventListener("click", open); |
| - input.addEventListener("input", function () { open(); }); |
| input.addEventListener("keydown", function (e) { |
| switch (e.key) { |
| case "ArrowDown": |
| e.preventDefault(); |
| - if (list.hidden) return open(); |
| + // Down out of the box is down into whatever is open: the |
| + // grid takes focus, and the arrow keys become days. |
| + if (mode === "cal" && cal) { cal.focusGrid(); break; } |
| + if (!mode) { openList(); break; } |
| if (rows.length) setActive(Math.min(active + 1, rows.length - 1)); |
| break; |
| case "ArrowUp": |
| e.preventDefault(); |
| - if (list.hidden) return open(); |
| + if (mode === "cal" && cal) { cal.focusGrid(); break; } |
| + if (!mode) { openList(); break; } |
| if (rows.length) setActive(Math.max(active - 1, 0)); |
| break; |
| case "Enter": |
| e.preventDefault(); |
| - if (!list.hidden && active >= 0) commit(rows[active].date, rows[active].hasTime); |
| - else { commitText(); close(); } |
| + if (mode !== "cal" && !list.hidden && active >= 0) commit(rows[active].date, rows[active].hasTime); |
| + else { commitText(); closeAll(); } |
| break; |
| case "Escape": |
| e.preventDefault(); |
| paint(); |
| - close(); |
| + closeAll(); |
| break; |
| } |
| }); |
| @@ -1260,15 +1571,46 @@ |
| if (e.key === "Enter") e.preventDefault(); |
| }); |
| - wrap.addEventListener("focusout", function (e) { |
| - if (wrap.contains(e.relatedTarget)) return; |
| - commitText(); |
| - close(); |
| + // Focus left the field, so commit what is in the box and shut |
| + // the popup — but only if it really left. |
| + // |
| + // The check is deferred by a tick, and that tick is the whole |
| + // point. Paging the calendar REBUILDS the grid, which removes the |
| + // very cell that had focus; the browser fires focusout with a |
| + // null relatedTarget on the way out and calendar.js puts focus on |
| + // the new cell immediately afterwards. Reading relatedTarget |
| + // alone, every arrow key inside the grid looked like a person |
| + // walking away: the field committed whatever the box happened to |
| + // say and slammed the panel shut under the cursor. A tick later |
| + // the answer is not in doubt — document.activeElement is either |
| + // back inside this field or it is not. |
| + wrap.addEventListener("focusout", function () { |
| + setTimeout(function () { |
| + if (wrap.contains(document.activeElement)) return; |
| + commitText(); |
| + closeAll(); |
| + }, 0); |
| }); |
| if (pick) { |
| + // The press must not blur the box: a blur commits the text, |
| + // and not committing it yet is the whole reason the button is |
| + // being pressed. |
| + pick.addEventListener("mousedown", function (e) { e.preventDefault(); }); |
| pick.addEventListener("click", function () { |
| - close(); |
| + if (mode === "cal" || mode === "slots") { |
| + closeAll(); |
| + input.focus(); |
| + return; |
| + } |
| + input.focus(); |
| + if (cal) return openCal(); |
| + if (kind === "time") return openSlots(); |
| + // Neither a calendar nor a clock, which means calendar.js is |
| + // not on the page. The browser's own panel is better than a |
| + // button that does nothing, and this is the only line in |
| + // this file that opens it. |
| + closeAll(); |
| try { |
| native.showPicker(); |
| } catch (err) { |
| @@ -1276,9 +1618,22 @@ |
| } |
| }); |
| } |
| - // A pick made in the native panel lands in the carrier; repaint. |
| + |
| + // The carrier changed underneath us: a pick in the browser's own |
| + // panel, a range's start seeding its end, an app's own script. |
| + // Repaint, so the box shows what the form will actually post. |
| + // |
| + // The guard here used to be "unless the combobox has focus", |
| + // which silently swallowed the one case it most needed to catch. |
| + // The native panel opens WITHOUT moving focus, so a date picked |
| + // in it never reached the box and the field looked like it had |
| + // ignored the selection. It now skips only our own commits, which |
| + // have painted already, and a change that arrives while somebody |
| + // is genuinely mid-word with a popup open. |
| native.addEventListener("change", function () { |
| - if (document.activeElement !== input) paint(); |
| + if (mine) return; |
| + if (mode && document.activeElement === input) return; |
| + paint(); |
| }); |
| paint(); |
| @@ -1333,6 +1688,11 @@ |
| // The pure half, for ui/datetime_node.mjs. A browser has no `module` |
| // and never takes this branch. |
| if (typeof module !== "undefined" && module && module.exports) { |
| - module.exports = { parse: parse, tables: tables, displayOptions: displayOptions }; |
| + module.exports = { |
| + parse: parse, |
| + parseAll: parseAll, |
| + tables: tables, |
| + displayOptions: displayOptions |
| + }; |
| } |
| })(); |
diff --git a/internal/calendar/static/menu.css b/internal/calendar/static/menu.css| index 2ddf029..9070652 100644 |
| --- a/internal/calendar/static/menu.css |
| +++ b/internal/calendar/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/calendar/static/tokens.css b/internal/calendar/static/tokens.css| index 16a8809..10f6da5 100644 |
| --- a/internal/calendar/static/tokens.css |
| +++ b/internal/calendar/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/calendar/viewer.go b/internal/calendar/viewer.go| index 4f2a15d..b5aec32 100644 |
| --- a/internal/calendar/viewer.go |
| +++ b/internal/calendar/viewer.go |
| @@ -2,8 +2,8 @@ package calendar |
| import ( |
| "amadan.net/oficina/home/connect" |
| - rauth "github.com/carlosframework/rastrillo/auth" |
| - "github.com/carlosframework/rastrillo/sessions" |
| + rauth "amadan.net/rastrillo/rastrillo/auth" |
| + "amadan.net/rastrillo/rastrillo/sessions" |
| "net/http" |
| ) |
diff --git a/internal/calendartest/browser_test.go b/internal/calendartest/browser_test.go| index c7bf03e..b7cb82a 100644 |
| --- a/internal/calendartest/browser_test.go |
| +++ b/internal/calendartest/browser_test.go |
| @@ -28,7 +28,7 @@ import ( |
| "strings" |
| "testing" |
| - "github.com/carlosframework/rastrillo/harness" |
| + "amadan.net/rastrillo/rastrillo/harness" |
| "github.com/chromedp/chromedp" |
| calendar "amadan.net/oficina/calendar/internal/calendar" |
diff --git a/internal/calendartest/schedule_delivery_test.go b/internal/calendartest/schedule_delivery_test.go| index cdbeba8..f7acb00 100644 |
| --- a/internal/calendartest/schedule_delivery_test.go |
| +++ b/internal/calendartest/schedule_delivery_test.go |
| @@ -14,7 +14,7 @@ import ( |
| "time" |
| calendar "amadan.net/oficina/calendar/internal/calendar" |
| - "github.com/carlosframework/rastrillo/carlos" |
| + "amadan.net/rastrillo/rastrillo/carlos" |
| ) |
| func TestPersistedWorkRegistersAtBootBeforeAnyRequest(t *testing.T) { |
diff --git a/internal/calendartest/vendored_test.go b/internal/calendartest/vendored_test.go| index 4169ad9..6311f57 100644 |
| --- a/internal/calendartest/vendored_test.go |
| +++ b/internal/calendartest/vendored_test.go |
| @@ -12,7 +12,7 @@ import ( |
| "path/filepath" |
| "testing" |
| - "github.com/carlosframework/rastrillo/ui" |
| + "amadan.net/rastrillo/rastrillo/ui" |
| ) |
| // vendoredTheme is the theme this app was scaffolded with. Change it |
| @@ -33,6 +33,14 @@ var vendoredIsMine = map[string]bool{ |
| // decisions table, row 8; this repo's ruling 2 is superseded). Pinned |
| // to estilo instead, by TestThemeIsEstilosForCalendar below. |
| "theme.css": true, |
| + // Calendar's OWN calendar.js (bring the working day into view on |
| + // load), not rastrillo's: from v0.26 rastrillo vendors a calendar.js |
| + // of its own, the month-grid overlay for date fields, under the same |
| + // name. Listed so doctor --fix never overwrites ours with it. Without |
| + // rastrillo's overlay, datetime.js's button opens the browser's own |
| + // picker, as it did on v0.23. Adopting the overlay means renaming |
| + // ours first. |
| + "calendar.js": true, |
| } |
| // TestThemeIsEstilosForCalendar pins static/theme.css to the estilo this |
diff --git a/internal/calendartest/zoom_browser_test.go b/internal/calendartest/zoom_browser_test.go| index 39c7a1e..e446140 100644 |
| --- a/internal/calendartest/zoom_browser_test.go |
| +++ b/internal/calendartest/zoom_browser_test.go |
| @@ -22,7 +22,7 @@ import ( |
| "testing" |
| "time" |
| - "github.com/carlosframework/rastrillo/harness" |
| + "amadan.net/rastrillo/rastrillo/harness" |
| "github.com/chromedp/cdproto/emulation" |
| "github.com/chromedp/cdproto/network" |
| "github.com/chromedp/chromedp" |
diff --git a/internal/guardstest/guards_test.go b/internal/guardstest/guards_test.go| index 8cf36df..5bcb83f 100644 |
| --- a/internal/guardstest/guards_test.go |
| +++ b/internal/guardstest/guards_test.go |
| @@ -127,7 +127,7 @@ func TestNeverSessionsUserID(t *testing.T) { |
| // Only if the file actually imports the sessions package. |
| imported := "" |
| for _, im := range f.Imports { |
| - if strings.Trim(im.Path.Value, `"`) == "github.com/carlosframework/rastrillo/sessions" { |
| + if p := strings.Trim(im.Path.Value, `"`); p == "amadan.net/rastrillo/rastrillo/sessions" || p == preMoveRastrillo+"/sessions" { |
| imported = "sessions" |
| if im.Name != nil { |
| imported = im.Name.Name |
| @@ -252,3 +252,39 @@ func TestUtilitiesAreNeverWrittenAsAttributes(t *testing.T) { |
| } |
| }) |
| } |
| + |
| +// 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 Calendar 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/calendar/native.go": preMoveRastrillo + "/sessions", |
| +} |
| + |
| +// TestNoPreMoveRastrillo fails on any import of rastrillo's old module |
| +// path outside preMoveAllowed. |
| +func TestNoPreMoveRastrillo(t *testing.T) { |
| + root := repoRoot(t) |
| + fset := token.NewFileSet() |
| + walk(t, root, []string{".go"}, func(path string, body []byte) { |
| + rel, _ := filepath.Rel(root, path) |
| + rel = filepath.ToSlash(rel) |
| + f, err := parser.ParseFile(fset, path, body, parser.ImportsOnly) |
| + if err != nil { |
| + return // a file that does not parse is the compiler's problem |
| + } |
| + 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) |
| + } |
| + } |
| + }) |
| +} |