rori 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.md +25 -0
- data/LICENSE +21 -0
- data/README.md +279 -0
- data/app/assets/stylesheets/rori/_init.css +79 -0
- data/app/assets/stylesheets/rori/layout.css +162 -0
- data/app/assets/stylesheets/rori/menubar.css +117 -0
- data/app/assets/stylesheets/rori/notifications.css +120 -0
- data/app/assets/stylesheets/rori/palette.css +114 -0
- data/app/assets/stylesheets/rori/shortcuts.css +110 -0
- data/app/assets/stylesheets/rori/terminal.css +98 -0
- data/app/assets/stylesheets/rori/themes.css +156 -0
- data/app/assets/stylesheets/rori/typing_badge.css +56 -0
- data/app/assets/stylesheets/rori/wallpaper.css +52 -0
- data/app/assets/stylesheets/rori/window.css +174 -0
- data/app/controllers/concerns/rori/windowed.rb +15 -0
- data/app/controllers/rori/commands/bars_controller.rb +9 -0
- data/app/controllers/rori/commands/runs_controller.rb +22 -0
- data/app/controllers/rori/commands/themes_controller.rb +9 -0
- data/app/controllers/rori/commands/ui_controller.rb +9 -0
- data/app/controllers/rori/commands/wallpapers_controller.rb +9 -0
- data/app/controllers/rori/commands_controller.rb +7 -0
- data/app/controllers/rori/desktops_controller.rb +7 -0
- data/app/helpers/rori/assets_helper.rb +9 -0
- data/app/helpers/rori/keymap_helper.rb +50 -0
- data/app/helpers/rori/prefs_helper.rb +13 -0
- data/app/helpers/rori/theme_helper.rb +14 -0
- data/app/helpers/rori/wallpaper_helper.rb +19 -0
- data/app/helpers/rori/window_helper.rb +36 -0
- data/app/javascript/rori/clock_controller.js +18 -0
- data/app/javascript/rori/command_index.js +84 -0
- data/app/javascript/rori/fuzzy.js +50 -0
- data/app/javascript/rori/index.js +21 -0
- data/app/javascript/rori/notifications_controller.js +45 -0
- data/app/javascript/rori/palette_controller.js +307 -0
- data/app/javascript/rori/prefs_controller.js +68 -0
- data/app/javascript/rori/rori_controller.js +996 -0
- data/app/javascript/rori/terminal_controller.js +198 -0
- data/app/javascript/rori/theme_controller.js +35 -0
- data/app/javascript/rori/wallpaper_controller.js +67 -0
- data/app/models/rori/command.rb +105 -0
- data/app/models/rori/notification.rb +20 -0
- data/app/models/rori/theme/color.rb +30 -0
- data/app/models/rori/theme.rb +87 -0
- data/app/models/rori/wallpaper.rb +54 -0
- data/app/views/layouts/rori/_head.html.haml +2 -0
- data/app/views/layouts/rori/shell.html.haml +97 -0
- data/app/views/layouts/rori/window.html.haml +10 -0
- data/app/views/rori/_menubar.html.haml +14 -0
- data/app/views/rori/_notification.html.haml +9 -0
- data/app/views/rori/_notifications.html.haml +10 -0
- data/app/views/rori/_palette.html.haml +13 -0
- data/app/views/rori/_shortcuts.html.haml +47 -0
- data/app/views/rori/_terminal.html.haml +9 -0
- data/app/views/rori/_wallpaper.html.haml +7 -0
- data/app/views/rori/_wallpaper_credit.html.haml +3 -0
- data/app/views/rori/_window.html.haml +15 -0
- data/app/views/rori/commands/_command.html.haml +17 -0
- data/app/views/rori/commands/index.html.haml +5 -0
- data/app/views/rori/desktops/show.html.haml +1 -0
- data/config/importmap.rb +3 -0
- data/config/locales/rori.en.yml +133 -0
- data/config/routes/rori.rb +13 -0
- data/lib/generators/rori/install/install_generator.rb +67 -0
- data/lib/generators/rori/install/templates/rori.rb.tt +22 -0
- data/lib/rori/engine.rb +19 -0
- data/lib/rori/version.rb +3 -0
- data/lib/rori.rb +180 -0
- data/lib/tasks/rori.rake +9 -0
- data/vendor/themes/LICENSE +25 -0
- data/vendor/themes/ghostty/Catppuccin Latte +22 -0
- data/vendor/themes/ghostty/Catppuccin Mocha +22 -0
- data/vendor/themes/ghostty/Everforest Dark Med +22 -0
- data/vendor/themes/ghostty/Flexoki Light +22 -0
- data/vendor/themes/ghostty/GitHub Dark Dimmed +22 -0
- data/vendor/themes/ghostty/Gruvbox Material Dark +22 -0
- data/vendor/themes/ghostty/Kanagawa Wave +22 -0
- data/vendor/themes/ghostty/Melange Dark +22 -0
- data/vendor/themes/ghostty/Nord +22 -0
- data/vendor/themes/ghostty/Rose Pine Dawn +22 -0
- data/vendor/themes/ghostty/Rose Pine Moon +22 -0
- data/vendor/themes/ghostty/TokyoNight Storm +22 -0
- data/vendor/themes/ghostty/Tomorrow Night Eighties +22 -0
- data/vendor/themes/ghostty/Zenbones Light +22 -0
- data/vendor/wallpapers/unsplash.yml +23 -0
- metadata +219 -0
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
|
2
|
+
import { score } from "rori/fuzzy"
|
|
3
|
+
import { CommandIndex } from "rori/command_index"
|
|
4
|
+
|
|
5
|
+
const USAGE_KEY = "rori:palette:usage"
|
|
6
|
+
// Shared with the terminal: whoever runs commands listens on rori-command:*.
|
|
7
|
+
const BUS = "rori-command"
|
|
8
|
+
|
|
9
|
+
// ⌘K command palette. Command lists are server-rendered into a turbo-frame
|
|
10
|
+
// (the root list is fetched on every open so new records show up) and ranked
|
|
11
|
+
// client-side, with a bonus for commands you run often. Enter pressed while a
|
|
12
|
+
// list is still arriving runs once it lands.
|
|
13
|
+
//
|
|
14
|
+
// Commands with `data-children` are nested lists: picking one swaps the frame
|
|
15
|
+
// to that URL and pushes a breadcrumb; Backspace on an empty query or Esc goes
|
|
16
|
+
// back up. Runs are handed out as `rori-command:run`, the selection as
|
|
17
|
+
// `rori-command:preview` (e.g. live theme preview), and `rori-command:closed`
|
|
18
|
+
// on close. Commands with `data-confirm` run on the second ↵ (or click); the
|
|
19
|
+
// first one arms them, and moving the selection disarms.
|
|
20
|
+
export default class extends Controller {
|
|
21
|
+
static targets = ["dialog", "input", "frame", "list", "item", "itemTemplate", "empty", "crumbs"]
|
|
22
|
+
static values = { root: String, confirm: String }
|
|
23
|
+
|
|
24
|
+
connect() {
|
|
25
|
+
this.stack = []
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
keydown(event) {
|
|
29
|
+
if (!(event.metaKey || event.ctrlKey) || event.key.toLowerCase() !== "k") return
|
|
30
|
+
event.preventDefault()
|
|
31
|
+
this.dialogTarget.open ? this.close() : this.open()
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
open() {
|
|
35
|
+
this.returnFocus = document.activeElement
|
|
36
|
+
this.inputTarget.value = ""
|
|
37
|
+
this.dialogTarget.showModal()
|
|
38
|
+
// Flattened tree for top-level queries, crawled in the background.
|
|
39
|
+
this.index = new CommandIndex(this.rootValue, {
|
|
40
|
+
source: (source) => this.dispatch("source", { prefix: BUS, detail: { source, items: [] } }).detail.items,
|
|
41
|
+
})
|
|
42
|
+
this.index.entries()
|
|
43
|
+
this.#go([])
|
|
44
|
+
this.filter()
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Every close goes through here (Esc included, see navigate), so focus is back
|
|
48
|
+
// on the page before the next keystroke: Chrome sometimes leaves it on the
|
|
49
|
+
// closed dialog's input, where the next key would vanish.
|
|
50
|
+
close() {
|
|
51
|
+
this.pendingRun = null
|
|
52
|
+
this.dialogTarget.close()
|
|
53
|
+
if (!this.dialogTarget.contains(document.activeElement)) return
|
|
54
|
+
const back = this.returnFocus
|
|
55
|
+
back?.isConnected && back !== document.body ? back.focus({ preventScroll: true }) : document.activeElement.blur()
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
closed() {
|
|
59
|
+
this.dispatch("closed", { prefix: BUS })
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
backdrop(event) {
|
|
63
|
+
if (event.target === this.dialogTarget) this.close()
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// The new list is in: rank it, then run an Enter that was waiting for it.
|
|
67
|
+
loaded() {
|
|
68
|
+
// A response that had already arrived still renders after its request was
|
|
69
|
+
// cancelled, so a stale list can land after drilling in; put the right one back.
|
|
70
|
+
const top = this.stack.at(-1)
|
|
71
|
+
if (top?.source) return this.#showSourceList(top.source)
|
|
72
|
+
const expected = top?.src || this.rootValue
|
|
73
|
+
if (this.#path(this.frameTarget.getAttribute("src")) !== this.#path(expected)) {
|
|
74
|
+
return this.frameTarget.setAttribute("src", expected)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
this.navigating = false
|
|
78
|
+
this.filter()
|
|
79
|
+
this.#runPending()
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
filter() {
|
|
83
|
+
const query = this.inputTarget.value
|
|
84
|
+
// At the top, queries match whole paths through the tree ("uthen" → UI › Theme › Nord).
|
|
85
|
+
if (!this.stack.length && query.trim()) return this.#searchPaths(query)
|
|
86
|
+
this.#clearPathResults()
|
|
87
|
+
const usage = this.#usage()
|
|
88
|
+
const ranked = this.itemTargets.map((item, i) => {
|
|
89
|
+
item.dataset.index ||= i
|
|
90
|
+
const match = score(item.dataset.search, query)
|
|
91
|
+
item.hidden = match <= 0
|
|
92
|
+
return { item, score: match + Math.log2(1 + (usage[this.#id(item)] || 0)), match }
|
|
93
|
+
})
|
|
94
|
+
const visible = ranked
|
|
95
|
+
.filter(({ match }) => match > 0)
|
|
96
|
+
.sort((a, b) => b.score - a.score || a.item.dataset.index - b.item.dataset.index)
|
|
97
|
+
|
|
98
|
+
// Nested lists keep their server order, so a browsed list reads the same every time.
|
|
99
|
+
if (this.hasListTarget && (query.trim() || !this.stack.length)) this.listTarget.append(...visible.map(({ item }) => item))
|
|
100
|
+
this.emptyTarget.hidden = visible.length > 0 || this.#loading()
|
|
101
|
+
|
|
102
|
+
const current = !query.trim() && visible.find(({ item }) => "current" in item.dataset)
|
|
103
|
+
this.#select((current || visible[0])?.item)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
navigate(event) {
|
|
107
|
+
const items = this.itemTargets.filter((item) => !item.hidden)
|
|
108
|
+
const index = items.indexOf(this.selected)
|
|
109
|
+
|
|
110
|
+
if (event.key === "ArrowDown" || event.key === "ArrowUp") {
|
|
111
|
+
event.preventDefault()
|
|
112
|
+
const next = event.key === "ArrowDown" ? index + 1 : index - 1
|
|
113
|
+
this.#select(items[(next + items.length) % items.length])
|
|
114
|
+
} else if (event.key === "Enter") {
|
|
115
|
+
event.preventDefault()
|
|
116
|
+
const newWindow = event.shiftKey || event.metaKey || event.ctrlKey
|
|
117
|
+
if (this.navigating || this.searching) this.pendingRun = { newWindow }
|
|
118
|
+
else if (this.selected) this.#run(this.selected, newWindow)
|
|
119
|
+
} else if (event.key === "Escape" && !this.stack.length) {
|
|
120
|
+
event.preventDefault()
|
|
121
|
+
this.close()
|
|
122
|
+
} else if (this.stack.length && (event.key === "Escape" || (event.key === "Backspace" && !this.inputTarget.value))) {
|
|
123
|
+
event.preventDefault()
|
|
124
|
+
this.#go(this.stack.slice(0, -1))
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
pick(event) {
|
|
129
|
+
this.#run(event.currentTarget, event.shiftKey || event.metaKey || event.ctrlKey)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Browsers fire mousemove when a list re-renders under a resting pointer; only
|
|
133
|
+
// a real move may take the selection from the keyboard.
|
|
134
|
+
hover(event) {
|
|
135
|
+
const pointer = `${event.screenX},${event.screenY}`
|
|
136
|
+
const moved = this.pointer !== undefined && this.pointer !== pointer
|
|
137
|
+
this.pointer = pointer
|
|
138
|
+
if (moved && this.selected !== event.currentTarget) this.#select(event.currentTarget)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
#run(item, newWindow) {
|
|
142
|
+
if ("confirm" in item.dataset && !("armed" in item.dataset)) return this.#arm(item)
|
|
143
|
+
|
|
144
|
+
const usage = this.#usage()
|
|
145
|
+
const id = this.#id(item)
|
|
146
|
+
usage[id] = (usage[id] || 0) + 1
|
|
147
|
+
localStorage.setItem(USAGE_KEY, JSON.stringify(usage))
|
|
148
|
+
|
|
149
|
+
const { children: src, source, trail } = item.dataset
|
|
150
|
+
if (trail && (src || source)) {
|
|
151
|
+
return this.#go([...this.stack, ...JSON.parse(trail).map(({ label, children, source }) => ({ label, src: children, source }))])
|
|
152
|
+
}
|
|
153
|
+
if (src || source) {
|
|
154
|
+
return this.#go([...this.stack, { label: item.querySelector(".rori-palette__label").firstChild.textContent.trim(), src, source }])
|
|
155
|
+
}
|
|
156
|
+
this.close()
|
|
157
|
+
this.dispatch("run", { prefix: BUS, detail: this.#detail(item, { newWindow }) })
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
#arm(item) {
|
|
161
|
+
item.dataset.armed = ""
|
|
162
|
+
const hint = document.createElement("span")
|
|
163
|
+
hint.className = "rori-palette__confirm"
|
|
164
|
+
hint.textContent = this.confirmValue
|
|
165
|
+
item.querySelector(".rori-palette__group").before(hint)
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
#disarm(except) {
|
|
169
|
+
for (const item of this.itemTargets) {
|
|
170
|
+
if (item === except || !("armed" in item.dataset)) continue
|
|
171
|
+
delete item.dataset.armed
|
|
172
|
+
item.querySelector(".rori-palette__confirm")?.remove()
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
async #searchPaths(query) {
|
|
177
|
+
const ticket = (this.ticket = (this.ticket || 0) + 1)
|
|
178
|
+
this.searching = true
|
|
179
|
+
const usage = this.#usage()
|
|
180
|
+
const results = await this.index.search(query, { limit: 30, bonus: (item) => Math.log2(1 + (usage[this.#idOf(item)] || 0)) })
|
|
181
|
+
if (ticket !== this.ticket || !this.dialogTarget.open) return
|
|
182
|
+
|
|
183
|
+
this.searching = false
|
|
184
|
+
// Until the root list has loaded there's nowhere to put results; loaded() refilters.
|
|
185
|
+
if (!this.hasListTarget) return
|
|
186
|
+
this.itemTargets.forEach((item) => { item.hidden = true })
|
|
187
|
+
this.#clearPathResults()
|
|
188
|
+
const items = results.map(({ item, trail, text }) => {
|
|
189
|
+
const trailData = trail.map(({ label, children, source }) => ({ label, children, source }))
|
|
190
|
+
const built = this.#buildItem({ ...item, label: text, trail: JSON.stringify(trailData) })
|
|
191
|
+
built.dataset.pathResult = ""
|
|
192
|
+
return built
|
|
193
|
+
})
|
|
194
|
+
this.listTarget.prepend(...items)
|
|
195
|
+
this.emptyTarget.hidden = items.length > 0
|
|
196
|
+
this.#select(items[0])
|
|
197
|
+
this.#runPending()
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
#clearPathResults() {
|
|
201
|
+
this.ticket = (this.ticket || 0) + 1
|
|
202
|
+
this.searching = false
|
|
203
|
+
this.itemTargets.filter((item) => "pathResult" in item.dataset).forEach((item) => item.remove())
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
#runPending() {
|
|
207
|
+
if (this.navigating || this.searching) return
|
|
208
|
+
const pending = this.pendingRun
|
|
209
|
+
this.pendingRun = null
|
|
210
|
+
if (pending && this.selected) this.#run(this.selected, pending.newWindow)
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Shows the list at the end of `stack` (the root list when empty).
|
|
214
|
+
#go(stack) {
|
|
215
|
+
this.#clearPathResults()
|
|
216
|
+
this.stack = stack
|
|
217
|
+
this.inputTarget.value = ""
|
|
218
|
+
this.crumbsTarget.hidden = !stack.length
|
|
219
|
+
this.crumbsTarget.textContent = stack.map(({ label }) => `${label} ›`).join(" ")
|
|
220
|
+
const top = stack.at(-1)
|
|
221
|
+
if (top?.source) return this.#showSourceList(top.source)
|
|
222
|
+
|
|
223
|
+
// Until the new list arrives the old one is still showing; don't run it.
|
|
224
|
+
this.navigating = true
|
|
225
|
+
this.pendingRun = null
|
|
226
|
+
this.frameTarget.setAttribute("src", top?.src || this.rootValue)
|
|
227
|
+
this.inputTarget.focus()
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// A nested list another controller provides at runtime, for state that only
|
|
231
|
+
// exists in the browser: `rori-command:source` asks, the listener fills
|
|
232
|
+
// `detail.items` with { label, group, action, param }.
|
|
233
|
+
#showSourceList(source) {
|
|
234
|
+
const { detail } = this.dispatch("source", { prefix: BUS, detail: { source, items: [] } })
|
|
235
|
+
const list = document.createElement("ul")
|
|
236
|
+
list.className = "rori-palette__list"
|
|
237
|
+
list.setAttribute("role", "listbox")
|
|
238
|
+
list.dataset.roriPaletteTarget = "list"
|
|
239
|
+
list.append(...detail.items.map((attributes) => this.#buildItem(attributes)))
|
|
240
|
+
|
|
241
|
+
this.navigating = false
|
|
242
|
+
this.frameTarget.removeAttribute("src")
|
|
243
|
+
this.frameTarget.replaceChildren(list)
|
|
244
|
+
this.filter()
|
|
245
|
+
this.inputTarget.focus()
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
#buildItem({ label, group, action, param, url, children, source, run, confirm, trail, current, shortcut }) {
|
|
249
|
+
const item = this.itemTemplateTarget.content.firstElementChild.cloneNode(true)
|
|
250
|
+
const data = { roriAction: action, param: param ?? "", url, children, source, run, trail, shortcut, search: `${label} ${group}` }
|
|
251
|
+
for (const [key, value] of Object.entries(data)) if (value !== undefined) item.dataset[key] = value
|
|
252
|
+
if (current) item.dataset.current = ""
|
|
253
|
+
if (confirm) item.dataset.confirm = ""
|
|
254
|
+
item.querySelector(".rori-palette__label").textContent = label + (children || source ? " ›" : "")
|
|
255
|
+
const groupElement = item.querySelector(".rori-palette__group")
|
|
256
|
+
groupElement.textContent = (current ? "✓ " : "") + group
|
|
257
|
+
if (shortcut) {
|
|
258
|
+
const kbd = document.createElement("kbd")
|
|
259
|
+
kbd.className = "rori-palette__shortcut"
|
|
260
|
+
kbd.textContent = shortcut
|
|
261
|
+
groupElement.before(kbd)
|
|
262
|
+
}
|
|
263
|
+
return item
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
#path(url) {
|
|
267
|
+
if (!url) return null
|
|
268
|
+
const { pathname, search } = new URL(url, location.href)
|
|
269
|
+
return pathname + search
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
#loading() {
|
|
273
|
+
const frame = this.frameTarget
|
|
274
|
+
return frame.hasAttribute("busy") || (frame.hasAttribute("src") && !frame.hasAttribute("complete"))
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
#select(item) {
|
|
278
|
+
this.#disarm(item)
|
|
279
|
+
this.itemTargets.forEach((other) => other.setAttribute("aria-selected", other === item))
|
|
280
|
+
this.selected = item
|
|
281
|
+
item?.scrollIntoView({ block: "nearest" })
|
|
282
|
+
// A list can finish loading after the palette closed; previewing then would undo the pick.
|
|
283
|
+
if (item && this.dialogTarget.open) this.dispatch("preview", { prefix: BUS, detail: this.#detail(item) })
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
#detail(item, extra = {}) {
|
|
287
|
+
const { url, roriAction: action, param, run } = item.dataset
|
|
288
|
+
return { url, action, param, run, ...extra }
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
#id(item) {
|
|
292
|
+
const { url, roriAction: action, param, children, source, run } = item.dataset
|
|
293
|
+
return this.#idOf({ url, action, param, children, source, run })
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
#idOf({ url, action, param, children, source, run }) {
|
|
297
|
+
return url || children || source || (run && `run:${run}`) || [action, param].filter((part) => part !== undefined).join(":")
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
#usage() {
|
|
301
|
+
try {
|
|
302
|
+
return JSON.parse(localStorage.getItem(USAGE_KEY)) || {}
|
|
303
|
+
} catch {
|
|
304
|
+
return {}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
|
2
|
+
|
|
3
|
+
const ONE_YEAR = 60 * 60 * 24 * 365
|
|
4
|
+
|
|
5
|
+
// Small cookie-backed preferences, applied as classes on <body> so the server
|
|
6
|
+
// renders them on load too:
|
|
7
|
+
// bars (UI › Menu bar › top | bottom) — previews while browsing the list and
|
|
8
|
+
// reverts if you don't pick, like themes.
|
|
9
|
+
// the empty-desk hint — dismissed from the hint, brought back from the
|
|
10
|
+
// keyboard shortcuts modal (which this controller also opens).
|
|
11
|
+
export default class extends Controller {
|
|
12
|
+
static targets = ["shortcuts", "hintToggle"]
|
|
13
|
+
static values = { barsCookie: String, bars: String, hintCookie: String }
|
|
14
|
+
|
|
15
|
+
run({ detail: { action, param } }) {
|
|
16
|
+
if (action !== "bars") return
|
|
17
|
+
this.barsValue = param
|
|
18
|
+
this.#setCookie(this.barsCookieValue, param)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
preview({ detail: { action, param } }) {
|
|
22
|
+
action === "bars" ? this.#applyBars(param) : this.revert()
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
revert() {
|
|
26
|
+
this.#applyBars(this.barsValue)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
barsValueChanged(bars) {
|
|
30
|
+
this.#applyBars(bars)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
openShortcuts() {
|
|
34
|
+
if (this.shortcutsTarget.open) return this.shortcutsTarget.close()
|
|
35
|
+
this.hintToggleTarget.checked = !this.element.classList.contains("hint-hidden")
|
|
36
|
+
this.shortcutsTarget.showModal()
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
closeShortcuts() {
|
|
40
|
+
this.shortcutsTarget.close()
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
shortcutsBackdrop(event) {
|
|
44
|
+
if (event.target === this.shortcutsTarget) this.closeShortcuts()
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
dismissHint() {
|
|
48
|
+
this.#setHint(false)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
toggleHint() {
|
|
52
|
+
this.#setHint(this.hintToggleTarget.checked)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
#setHint(shown) {
|
|
56
|
+
this.element.classList.toggle("hint-hidden", !shown)
|
|
57
|
+
this.#setCookie(this.hintCookieValue, shown ? "" : "hidden")
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
#applyBars(bars) {
|
|
61
|
+
this.element.classList.toggle("bars-bottom", bars === "bottom")
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// An empty value removes the cookie.
|
|
65
|
+
#setCookie(name, value) {
|
|
66
|
+
document.cookie = `${name}=${encodeURIComponent(value)}; path=/; max-age=${value ? ONE_YEAR : 0}; samesite=lax`
|
|
67
|
+
}
|
|
68
|
+
}
|