oficina / estilo Public

Clone
git clone https://amadan.net/oficina/estilo

Plain git — no account needed to clone.

Download

Download this file

1// Package menu is the suite's one floating panel ([estilo-menu]), its
2// rows ([estilo-menu-item]) and the document menu bar
3// ([estilo-menubar]). Docs is its first consumer.
4//
5// Scripts off, a menu is a <details>: it opens, and its links and form
6// buttons work; it closes only when its summary is clicked again. Script
7// may add Escape, outside-click close and menubar hover-switching; none
8// of it is required.
9//
10// It carries no strings. Callers supply every label in their locale.
11package menu
12
13import (
14 "embed"
15 "net/http"
16 "strings"
17)
18
19//go:embed menu.css
20var css []byte
21
22//go:embed templates/*.html
23var templateFS embed.FS
24
25// CSS returns menu.css. Link it after base.css and before chrome.css.
26func CSS() []byte { return append([]byte(nil), css...) }
27
28// Templates holds estilo-menu-item and estilo-icon-check. Parse it into
29// the app's page templates with ParseFS(Templates(), "templates/*.html").
30func Templates() embed.FS { return templateFS }
31
32// Item is one row rendered by estilo-menu-item. Set exactly one of Href,
33// Action and Dialog; each is a different element, and each works with
34// scripts off:
35//
36// - Href: a link, <a href>, for a row that navigates.
37// - Action: a one-button <form method="post"> to that path, for a row
38// that changes something ("New sheet").
39// - Dialog: a <button> that opens the <dialog> with that id through the
40// platform's invoker command (command="show-modal"), for a row that
41// asks for more before anything happens ("Keyboard shortcuts…").
42// chrome.js opens it where the engine has no invoker commands.
43//
44// Action and Dialog came with Sheets, the second adopter: Docs' rows were
45// all links, and Sheets' Help moved into the account menu as a dialog.
46type Item struct {
47 Href, Label string
48 // Key is an already-formatted shortcut, from Shortcut. Optional.
49 Key string
50 // Action is the POST target of a form row.
51 Action string
52 // Dialog is the id of the <dialog> a button row opens.
53 Dialog string
54 // Danger draws a destructive row in the negative tone
55 // (estilo-menu-item="danger"). Calls, the fifth adopter, was the
56 // first with one in a menu: "End meeting for everyone". The row still
57 // goes to its own confirm page; the tone only says it is the one to
58 // read twice.
59 Danger bool
60}
61
62// Apple reports whether the request comes from an Apple platform, the
63// server-side choice behind Shortcut: Sec-CH-UA-Platform "macOS" or
64// "iOS", or a User-Agent naming a Macintosh, iPhone or iPad. A response
65// whose markup depends on it must be private or no-store, or say
66// Vary: User-Agent.
67func Apple(r *http.Request) bool {
68 switch strings.Trim(r.Header.Get("Sec-CH-UA-Platform"), `"`) {
69 case "macOS", "iOS":
70 return true
71 }
72 ua := r.UserAgent()
73 return strings.Contains(ua, "Macintosh") || strings.Contains(ua, "iPhone") || strings.Contains(ua, "iPad")
74}
75
76// Shortcut formats a key chord for the request's platform, so a label
77// is right in the first paint and nothing flips once script runs.
78//
79// keys is written in one platform-neutral spelling, parts joined by
80// "+": Mod (Command on Apple, Ctrl elsewhere), Shift, Alt, Ctrl (the
81// literal Control key), then one key. Enter, Delete, Backspace, Tab and
82// Esc are named; anything else is printed as given. Apple chords print
83// their modifiers in the order ⌃⌥⇧⌘ with no separator (⇧⌘K); other
84// platforms join words with "+" in the order Ctrl, Alt, Shift
85// (Ctrl+Shift+K).
86func Shortcut(r *http.Request, keys string) string {
87 return format(Apple(r), keys)
88}
89
90var named = map[string][2]string{
91 // name: {apple, other}
92 "Enter": {"↩", "Enter"},
93 "Delete": {"⌫", "Delete"},
94 "Backspace": {"⌫", "Backspace"},
95 "Tab": {"⇥", "Tab"},
96 "Esc": {"⎋", "Esc"},
97}
98
99func format(apple bool, keys string) string {
100 var ctrl, alt, shift, cmd bool
101 key := ""
102 for _, part := range strings.Split(keys, "+") {
103 switch part {
104 case "Mod":
105 if apple {
106 cmd = true
107 } else {
108 ctrl = true
109 }
110 case "Ctrl":
111 ctrl = true
112 case "Alt":
113 alt = true
114 case "Shift":
115 shift = true
116 default:
117 key = part
118 }
119 }
120 if n, ok := named[key]; ok {
121 if apple {
122 key = n[0]
123 } else {
124 key = n[1]
125 }
126 }
127 if apple {
128 var b strings.Builder
129 for _, m := range []struct {
130 on bool
131 glyph string
132 }{{ctrl, "⌃"}, {alt, "⌥"}, {shift, "⇧"}, {cmd, "⌘"}} {
133 if m.on {
134 b.WriteString(m.glyph)
135 }
136 }
137 return b.String() + key
138 }
139 var parts []string
140 for _, m := range []struct {
141 on bool
142 word string
143 }{{ctrl, "Ctrl"}, {alt, "Alt"}, {shift, "Shift"}} {
144 if m.on {
145 parts = append(parts, m.word)
146 }
147 }
148 if key != "" {
149 parts = append(parts, key)
150 }
151 return strings.Join(parts, "+")
152}
153