oficina / estilo Public

Add shared open-source notice component

Co-Authored-By: Codex <noreply@openai.com>
Paul Campbell pushed by paul@keymail.dev 5eea6ef4adc9a46ea2590dc3dd2310ec60f6e0c4 parent 9873c64
5 files changed, +106 −0
  • credits/credits.css +7 −0
  • credits/credits.go +24 −0
  • credits/credits_test.go +51 −0
  • credits/templates/credits.html +14 −0
  • implementation/open-source-credits.json +10 −0

CI passed — run details

diff --git a/credits/credits.css b/credits/credits.css
new file mode 100644
index 0000000..a44d881
--- /dev/null
+++ b/credits/credits.css
@@ -0,0 +1,7 @@
+[estilo-credits] { min-inline-size: 0; overflow-wrap: anywhere; }
+[estilo-credits] > ul { list-style: none; padding: 0; }
+[estilo-credits] > ul > li { border-block-end: 1px solid var(--rst-border); padding-block: .5rem; }
+[estilo-credits] summary { min-block-size: 24px; padding-block: .5rem; cursor: pointer; }
+[estilo-credits] a { display: inline-block; min-block-size: 24px; max-inline-size: 100%; }
+[estilo-credits] pre { white-space: pre-wrap; overflow-wrap: anywhere; font: inherit; }
+[estilo-credits-version] { font-size: .875em; }
diff --git a/credits/credits.go b/credits/credits.go
new file mode 100644
index 0000000..4ef02b1
--- /dev/null
+++ b/credits/credits.go
@@ -0,0 +1,24 @@
+// Package credits renders dependency notices without JavaScript. Applications
+// supply the page title in their active locale and the original notice text.
+package credits
+
+import "embed"
+
+//go:embed templates/*.html
+var templateFS embed.FS
+
+//go:embed credits.css
+var stylesheet []byte
+
+func Templates() embed.FS { return templateFS }
+func CSS() []byte { return append([]byte(nil), stylesheet...) }
+
+type Notice struct{ Name, Text string }
+type Component struct {
+ Name, Version, SourceURL string
+ Notices []Notice
+}
+type PageData struct {
+ Title string
+ Components []Component
+}
diff --git a/credits/credits_test.go b/credits/credits_test.go
new file mode 100644
index 0000000..d7c70d8
--- /dev/null
+++ b/credits/credits_test.go
@@ -0,0 +1,51 @@
+package credits
+
+import (
+ "bytes"
+ "html/template"
+ "strings"
+ "testing"
+)
+
+func TestNoticesAreEscapedAndNeedNoScript(t *testing.T) {
+ tpl, err := template.ParseFS(Templates(), "templates/*.html")
+ if err != nil {
+ t.Fatal(err)
+ }
+ notice := "Copyright <script>alert(1)</script>\nPermission & conditions"
+ data := PageData{Title: "Open source", Components: []Component{{Name: "<img src=x onerror=alert(1)>", Version: "v1.2.3", SourceURL: "https://example.org/source", Notices: []Notice{{Name: "LICENSE", Text: notice}}}}}
+ var out bytes.Buffer
+ if err := tpl.ExecuteTemplate(&out, "estilo/credits", data); err != nil {
+ t.Fatal(err)
+ }
+ html := out.String()
+ if strings.Contains(html, "<script") || strings.Contains(html, "<img") || !strings.Contains(html, "<details>") || !strings.Contains(html, "&lt;script&gt;") || !strings.Contains(html, "Permission &amp; conditions") {
+ t.Fatal("notices were interpreted as markup or lost")
+ }
+ data.Components[0].SourceURL = "javascript:alert(1)"
+ out.Reset()
+ if err := tpl.ExecuteTemplate(&out, "estilo/credits", data); err != nil {
+ t.Fatal(err)
+ }
+ if strings.Contains(out.String(), `href="javascript:`) {
+ t.Fatal("unsafe source URL reached link")
+ }
+}
+
+func TestCallerSuppliesLocalizedTitleAndNoticesRemainOriginal(t *testing.T) {
+ tpl, err := template.ParseFS(Templates(), "templates/*.html")
+ if err != nil {
+ t.Fatal(err)
+ }
+ // The component has no translated strings of its own. Even a right-to-left
+ // title leaves package identifiers isolated and notice text unchanged.
+ for _, title := range []string{"Open source", "Foinse oscailte", "Código abierto", "Code source ouvert", "Código aberto", "Open source-software", "Openbronsoftware", "Codice aperto", "Kod źródłowy", "Öppen källkod", "オープンソース", "المصدر المفتوح"} {
+ var out bytes.Buffer
+ if err := tpl.ExecuteTemplate(&out, "estilo/credits", PageData{Title: title, Components: []Component{{Name: "example/module", Notices: []Notice{{Name: "LICENSE", Text: "Original notice"}}}}}); err != nil {
+ t.Fatal(err)
+ }
+ if !strings.Contains(out.String(), title) || !strings.Contains(out.String(), "Original notice") || !strings.Contains(out.String(), "<bdi>example/module</bdi>") {
+ t.Fatal("localized title or original notice lost")
+ }
+ }
+}
diff --git a/credits/templates/credits.html b/credits/templates/credits.html
new file mode 100644
index 0000000..727c002
--- /dev/null
+++ b/credits/templates/credits.html
@@ -0,0 +1,14 @@
+{{define "estilo/credits"}}
+<section estilo-credits>
+ <h1>{{.Title}}</h1>
+ <ul role="list">
+ {{range .Components}}
+ <li><details>
+ <summary><bdi>{{.Name}}</bdi>{{if .Version}} <bdi estilo-credits-version>{{.Version}}</bdi>{{end}}</summary>
+ {{if .SourceURL}}<p><a href="{{.SourceURL}}"><bdi>{{.SourceURL}}</bdi></a></p>{{end}}
+ {{range .Notices}}<h2>{{.Name}}</h2><pre>{{.Text}}</pre>{{end}}
+ </details></li>
+ {{end}}
+ </ul>
+</section>
+{{end}}
diff --git a/implementation/open-source-credits.json b/implementation/open-source-credits.json
new file mode 100644
index 0000000..8e16873
--- /dev/null
+++ b/implementation/open-source-credits.json
@@ -0,0 +1,10 @@
+{
+ "author": "Codex",
+ "user_request": "I want to add attribution to all the open source we're using ... would that go in a help menu?",
+ "suite_requirement": "Extract reusable components and maintain consistent UI/UX via Estilo.",
+ "copy_review": "User waived copy review for this round. Component contains no fixed user-facing strings; callers supply translated title and original upstream notices.",
+ "first_consumer": "Home credits page; adoption in progress. Not an agreed suite interface until a second actual consumer adopts it.",
+ "behavior": "Native details list of component name/version, source link, and complete original notice text. No JavaScript. Template autoescapes all notice data and source URL. Bare estilo attributes define wrapping and minimum24px controls. Theme colors remain inherited and unchanged.",
+ "verification": "Full Estilo make test passed. Tests cover notice markup escaping, unsafe source URL filtering, translated caller title, bidi isolation and preservation of notice text. Native browser320px/keyboard review and actual Home catalog remain pending.",
+ "review": "Pending Claude Fable after quota reset; no merge or deployment. A rendered credits list is not a claim that dependency/license inventory is complete."
+}