@owdproject/nuxt-theme-paper 0.0.0-stage → 4.0.0-alpha.3

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.
Files changed (78) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +62 -2
  3. package/dist/module.d.mts +7 -0
  4. package/dist/module.json +9 -0
  5. package/dist/module.mjs +55 -0
  6. package/dist/runtime/assets/styles/index.css +533 -0
  7. package/dist/runtime/assets/styles/partials/checkbox.css +158 -0
  8. package/dist/runtime/assets/styles/partials/contextmenu.css +22 -0
  9. package/dist/runtime/assets/styles/partials/desktop.css +69 -0
  10. package/dist/runtime/assets/styles/partials/input.css +155 -0
  11. package/dist/runtime/assets/styles/partials/menu.css +72 -0
  12. package/dist/runtime/assets/styles/partials/scrollbar.css +25 -0
  13. package/dist/runtime/assets/styles/partials/tabs.css +163 -0
  14. package/dist/runtime/assets/styles/partials/window-chrome.css +45 -0
  15. package/dist/runtime/assets/styles/partials/window.css +30 -0
  16. package/dist/runtime/components/ButtonWindowNav.d.vue.ts +17 -0
  17. package/dist/runtime/components/ButtonWindowNav.vue +12 -0
  18. package/dist/runtime/components/ButtonWindowNav.vue.d.ts +17 -0
  19. package/dist/runtime/components/Desktop.d.vue.ts +13 -0
  20. package/dist/runtime/components/Desktop.vue +21 -0
  21. package/dist/runtime/components/Desktop.vue.d.ts +13 -0
  22. package/dist/runtime/components/DesktopContent.d.vue.ts +13 -0
  23. package/dist/runtime/components/DesktopContent.vue +9 -0
  24. package/dist/runtime/components/DesktopContent.vue.d.ts +13 -0
  25. package/dist/runtime/components/DesktopWindow.d.vue.ts +21 -0
  26. package/dist/runtime/components/DesktopWindow.vue +51 -0
  27. package/dist/runtime/components/DesktopWindow.vue.d.ts +21 -0
  28. package/dist/runtime/components/DesktopWindowContent.d.vue.ts +13 -0
  29. package/dist/runtime/components/DesktopWindowContent.vue +31 -0
  30. package/dist/runtime/components/DesktopWindowContent.vue.d.ts +13 -0
  31. package/dist/runtime/components/DesktopWindowIframe.d.vue.ts +20 -0
  32. package/dist/runtime/components/DesktopWindowIframe.vue +30 -0
  33. package/dist/runtime/components/DesktopWindowIframe.vue.d.ts +20 -0
  34. package/dist/runtime/components/DesktopWindowNav.d.vue.ts +15 -0
  35. package/dist/runtime/components/DesktopWindowNav.vue +113 -0
  36. package/dist/runtime/components/DesktopWindowNav.vue.d.ts +15 -0
  37. package/dist/runtime/components/DesktopWindowNavButton.d.vue.ts +17 -0
  38. package/dist/runtime/components/DesktopWindowNavButton.vue +54 -0
  39. package/dist/runtime/components/DesktopWindowNavButton.vue.d.ts +17 -0
  40. package/dist/runtime/components/PaperSystemBar.d.vue.ts +3 -0
  41. package/dist/runtime/components/PaperSystemBar.vue +808 -0
  42. package/dist/runtime/components/PaperSystemBar.vue.d.ts +3 -0
  43. package/dist/runtime/components/PaperWindowSnapHints.d.vue.ts +3 -0
  44. package/dist/runtime/components/PaperWindowSnapHints.vue +7 -0
  45. package/dist/runtime/components/PaperWindowSnapHints.vue.d.ts +3 -0
  46. package/dist/runtime/composables/usePaperAppearance.d.ts +8 -0
  47. package/dist/runtime/composables/usePaperAppearance.js +18 -0
  48. package/dist/runtime/plugins/paper-appearance.client.d.ts +6 -0
  49. package/dist/runtime/plugins/paper-appearance.client.js +16 -0
  50. package/dist/runtime/utils/paperAppearance.d.ts +10 -0
  51. package/dist/runtime/utils/paperAppearance.js +18 -0
  52. package/dist/types.d.mts +7 -0
  53. package/package.json +61 -4
  54. package/src/module.ts +71 -0
  55. package/src/runtime/assets/styles/index.scss +13 -0
  56. package/src/runtime/assets/styles/partials/_variables.scss +111 -0
  57. package/src/runtime/assets/styles/partials/checkbox.scss +64 -0
  58. package/src/runtime/assets/styles/partials/contextmenu.scss +25 -0
  59. package/src/runtime/assets/styles/partials/desktop.scss +70 -0
  60. package/src/runtime/assets/styles/partials/input.scss +55 -0
  61. package/src/runtime/assets/styles/partials/menu.scss +88 -0
  62. package/src/runtime/assets/styles/partials/scrollbar.scss +35 -0
  63. package/src/runtime/assets/styles/partials/tabs.scss +68 -0
  64. package/src/runtime/assets/styles/partials/window-chrome.scss +57 -0
  65. package/src/runtime/assets/styles/partials/window.scss +32 -0
  66. package/src/runtime/components/ButtonWindowNav.vue +12 -0
  67. package/src/runtime/components/Desktop.vue +22 -0
  68. package/src/runtime/components/DesktopContent.vue +14 -0
  69. package/src/runtime/components/DesktopWindow.vue +76 -0
  70. package/src/runtime/components/DesktopWindowContent.vue +31 -0
  71. package/src/runtime/components/DesktopWindowIframe.vue +30 -0
  72. package/src/runtime/components/DesktopWindowNav.vue +115 -0
  73. package/src/runtime/components/DesktopWindowNavButton.vue +55 -0
  74. package/src/runtime/components/PaperSystemBar.vue +1482 -0
  75. package/src/runtime/components/PaperWindowSnapHints.vue +7 -0
  76. package/src/runtime/composables/usePaperAppearance.ts +26 -0
  77. package/src/runtime/plugins/paper-appearance.client.ts +23 -0
  78. package/src/runtime/utils/paperAppearance.ts +31 -0
@@ -0,0 +1,1482 @@
1
+ <script setup lang="ts">
2
+ import { ref, computed, onMounted, onUnmounted, watch, nextTick } from 'vue'
3
+ import { useI18n } from 'vue-i18n'
4
+ import { useApplicationManager } from '@owdproject/nuxt/runtime/composables/useApplicationManager'
5
+ import { useApplicationEntries } from '@owdproject/nuxt/runtime/composables/useApplicationEntries'
6
+ import { useDesktopVolumeStore } from '@owdproject/nuxt/runtime/stores/storeDesktopVolume'
7
+ import { usePaperAppearance } from '../composables/usePaperAppearance'
8
+
9
+ const open = ref(false)
10
+ const calendarOpen = ref(false)
11
+ const time = ref('')
12
+ const barRef = ref<HTMLElement | null>(null)
13
+ const triggerRef = ref<HTMLButtonElement | null>(null)
14
+ const clockRef = ref<HTMLButtonElement | null>(null)
15
+ const panelPos = ref({ top: '0px', left: '0px', minWidth: '18rem' })
16
+ const calendarPos = ref({ top: '0px', left: '0px', minWidth: '17.5rem' })
17
+ const volumeOpen = ref(false)
18
+ const volumeRef = ref<HTMLButtonElement | null>(null)
19
+ const volumePos = ref({ top: '0px', left: '0px', minWidth: '11.5rem' })
20
+ const volumeBeforeMute = ref(100)
21
+ const muted = ref(false)
22
+
23
+ const WEEKDAYS = ['Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa', 'Su'] as const
24
+
25
+ function startOfDay(date: Date) {
26
+ return new Date(date.getFullYear(), date.getMonth(), date.getDate())
27
+ }
28
+
29
+ const today = startOfDay(new Date())
30
+ const selectedDate = ref(startOfDay(new Date()))
31
+ const viewDate = ref(new Date(today.getFullYear(), today.getMonth(), 1))
32
+
33
+ const applicationManager = useApplicationManager()
34
+ const apps = useApplicationEntries().sortedAppEntries('title', 'primary')
35
+ const { appearance, toggleAppearance } = usePaperAppearance()
36
+ const volumeStore = useDesktopVolumeStore()
37
+
38
+ const appCount = computed(() => apps.value.length)
39
+
40
+ const runningApps = computed(() => applicationManager.appsRunning.value)
41
+
42
+ const isDark = computed(() => appearance.value === 'dark')
43
+
44
+ const appearanceLabel = computed(() =>
45
+ isDark.value ? 'Switch to light mode' : 'Switch to dark mode',
46
+ )
47
+
48
+ const volumeLevel = computed(() => volumeStore.master)
49
+
50
+ const isVolumeMuted = computed(
51
+ () => muted.value || volumeLevel.value === 0,
52
+ )
53
+
54
+ const volumeLabel = computed(() => {
55
+ if (isVolumeMuted.value) return 'Volume, muted'
56
+ return `Volume, ${volumeLevel.value}%`
57
+ })
58
+
59
+ const { te, t } = useI18n()
60
+
61
+ const groupedApps = computed(() => {
62
+ const map = new Map<string, ApplicationEntryWithInherited[]>()
63
+ for (const entry of apps.value) {
64
+ const category = entry.category?.trim() || 'Programs'
65
+ const list = map.get(category) ?? []
66
+ list.push(entry)
67
+ map.set(category, list)
68
+ }
69
+ return [...map.entries()]
70
+ .map(([category, entries]) => {
71
+ const translationKey = `applications.categories.${category}`
72
+ const translatedName = te(translationKey) ? t(translationKey) : category
73
+ return { category, translatedName, entries }
74
+ })
75
+ .sort((a, b) => a.translatedName.localeCompare(b.translatedName))
76
+ .map(({ category, entries }) => ({
77
+ category,
78
+ entries: [...entries].sort((a, b) =>
79
+ (a.title || '').localeCompare(b.title || ''),
80
+ ),
81
+ }))
82
+ })
83
+
84
+ const showGroupHeaders = computed(() => {
85
+ const groups = groupedApps.value
86
+ if (groups.length <= 1) return false
87
+ return !(
88
+ groups.length === 1 &&
89
+ (groups[0]?.category === 'Programs' ||
90
+ groups[0]?.category === 'Other')
91
+ )
92
+ })
93
+
94
+ function entrySubtitle(entry: ApplicationEntryWithInherited) {
95
+ const desc = entry.application.config.description?.trim()
96
+ if (desc && desc !== entry.title) return desc
97
+ return ''
98
+ }
99
+
100
+ function isIconUrl(icon: string) {
101
+ return (
102
+ icon.startsWith('http://') ||
103
+ icon.startsWith('https://') ||
104
+ icon.startsWith('/') ||
105
+ icon.startsWith('data:')
106
+ )
107
+ }
108
+
109
+ function isAppFocused(application: IApplicationController) {
110
+ return [...application.windows.values()].some(
111
+ (w) => w.state.active && w.state.focused,
112
+ )
113
+ }
114
+
115
+ function getAppTargetWindow(application: IApplicationController) {
116
+ const windows = [...application.windows.values()]
117
+ if (windows.length === 0) return undefined
118
+ const focused = windows.find((w) => w.state.focused && w.state.active)
119
+ if (focused) return focused
120
+ const active = windows.find((w) => w.state.active)
121
+ if (active) return active
122
+ return windows[windows.length - 1]
123
+ }
124
+
125
+ function focusRunningApp(application: IApplicationController) {
126
+ const window = getAppTargetWindow(application)
127
+ if (!window) return
128
+ if (window.state.active && window.state.focused) {
129
+ window.actions.minimize()
130
+ return
131
+ }
132
+ if (!window.state.active) {
133
+ window.actions.setActive(true)
134
+ }
135
+ window.actions.bringToFront()
136
+ }
137
+
138
+ function isSameDay(a: Date, b: Date) {
139
+ return (
140
+ a.getFullYear() === b.getFullYear() &&
141
+ a.getMonth() === b.getMonth() &&
142
+ a.getDate() === b.getDate()
143
+ )
144
+ }
145
+
146
+ const monthLabel = computed(() =>
147
+ viewDate.value.toLocaleDateString(undefined, {
148
+ month: 'long',
149
+ year: 'numeric',
150
+ }),
151
+ )
152
+
153
+ const calendarCells = computed(() => {
154
+ const year = viewDate.value.getFullYear()
155
+ const month = viewDate.value.getMonth()
156
+ const firstWeekday = (new Date(year, month, 1).getDay() + 6) % 7
157
+ const daysInMonth = new Date(year, month + 1, 0).getDate()
158
+ const cells: { date: Date | null; key: string }[] = []
159
+
160
+ for (let i = 0; i < firstWeekday; i++) {
161
+ cells.push({ date: null, key: `pad-${year}-${month}-${i}` })
162
+ }
163
+ for (let day = 1; day <= daysInMonth; day++) {
164
+ cells.push({
165
+ date: new Date(year, month, day),
166
+ key: `${year}-${month}-${day}`,
167
+ })
168
+ }
169
+ return cells
170
+ })
171
+
172
+ function tick() {
173
+ time.value = new Date().toLocaleTimeString([], {
174
+ hour: '2-digit',
175
+ minute: '2-digit',
176
+ })
177
+ }
178
+
179
+ function updatePanelPosition() {
180
+ const trigger = triggerRef.value
181
+ const bar = barRef.value
182
+ if (!trigger || !bar) return
183
+ const triggerRect = trigger.getBoundingClientRect()
184
+ const barRect = bar.getBoundingClientRect()
185
+ const panelMin = 288
186
+ const panelMax = Math.min(360, window.innerWidth - 24)
187
+ const width = Math.max(panelMin, Math.min(panelMax, panelMax))
188
+ let left = triggerRect.left
189
+ if (left + width > window.innerWidth - 12) {
190
+ left = window.innerWidth - 12 - width
191
+ }
192
+ left = Math.max(12, left)
193
+
194
+ panelPos.value = {
195
+ top: `${barRect.bottom + 6}px`,
196
+ left: `${left}px`,
197
+ minWidth: `${width}px`,
198
+ }
199
+ }
200
+
201
+ function updateCalendarPosition() {
202
+ const trigger = clockRef.value
203
+ const bar = barRef.value
204
+ if (!trigger || !bar) return
205
+ const triggerRect = trigger.getBoundingClientRect()
206
+ const barRect = bar.getBoundingClientRect()
207
+ const width = 280
208
+ let left = triggerRect.right - width
209
+ left = Math.max(12, Math.min(left, window.innerWidth - width - 12))
210
+
211
+ calendarPos.value = {
212
+ top: `${barRect.bottom + 6}px`,
213
+ left: `${left}px`,
214
+ minWidth: `${width}px`,
215
+ }
216
+ }
217
+
218
+ function updateVolumePosition() {
219
+ const trigger = volumeRef.value
220
+ const bar = barRef.value
221
+ if (!trigger || !bar) return
222
+ const triggerRect = trigger.getBoundingClientRect()
223
+ const barRect = bar.getBoundingClientRect()
224
+ const width = 184
225
+ let left = triggerRect.right - width
226
+ left = Math.max(12, Math.min(left, window.innerWidth - width - 12))
227
+
228
+ volumePos.value = {
229
+ top: `${barRect.bottom + 6}px`,
230
+ left: `${left}px`,
231
+ minWidth: `${width}px`,
232
+ }
233
+ }
234
+
235
+ function toggleApps() {
236
+ calendarOpen.value = false
237
+ volumeOpen.value = false
238
+ open.value = !open.value
239
+ }
240
+
241
+ function closeApps() {
242
+ open.value = false
243
+ }
244
+
245
+ function toggleCalendar() {
246
+ open.value = false
247
+ volumeOpen.value = false
248
+ if (!calendarOpen.value) {
249
+ const now = startOfDay(new Date())
250
+ selectedDate.value = now
251
+ viewDate.value = new Date(now.getFullYear(), now.getMonth(), 1)
252
+ }
253
+ calendarOpen.value = !calendarOpen.value
254
+ }
255
+
256
+ function closeCalendar() {
257
+ calendarOpen.value = false
258
+ }
259
+
260
+ function toggleVolume() {
261
+ open.value = false
262
+ calendarOpen.value = false
263
+ volumeOpen.value = !volumeOpen.value
264
+ }
265
+
266
+ function closeVolume() {
267
+ volumeOpen.value = false
268
+ }
269
+
270
+ function onVolumeInput(e: Event) {
271
+ const value = Number((e.target as HTMLInputElement).value)
272
+ muted.value = false
273
+ volumeStore.setMasterVolume(value)
274
+ }
275
+
276
+ function toggleMute() {
277
+ if (isVolumeMuted.value) {
278
+ volumeStore.setMasterVolume(volumeBeforeMute.value || 50)
279
+ muted.value = false
280
+ } else {
281
+ volumeBeforeMute.value = volumeLevel.value
282
+ volumeStore.setMasterVolume(0)
283
+ muted.value = true
284
+ }
285
+ }
286
+
287
+ function selectDay(date: Date) {
288
+ selectedDate.value = startOfDay(date)
289
+ }
290
+
291
+ function prevMonth() {
292
+ const d = viewDate.value
293
+ viewDate.value = new Date(d.getFullYear(), d.getMonth() - 1, 1)
294
+ }
295
+
296
+ function nextMonth() {
297
+ const d = viewDate.value
298
+ viewDate.value = new Date(d.getFullYear(), d.getMonth() + 1, 1)
299
+ }
300
+
301
+ async function launch(entry: ApplicationEntryWithInherited) {
302
+ if (!entry.application?.id || !entry.command) return
303
+ await applicationManager.execAppCommand(entry.application.id, entry.command)
304
+ closeApps()
305
+ }
306
+
307
+ let timer: ReturnType<typeof setInterval>
308
+
309
+ function onKey(e: KeyboardEvent) {
310
+ if (e.key !== 'Escape') return
311
+ if (volumeOpen.value) closeVolume()
312
+ else if (calendarOpen.value) closeCalendar()
313
+ else if (open.value) closeApps()
314
+ }
315
+
316
+ function onResize() {
317
+ if (open.value) updatePanelPosition()
318
+ if (calendarOpen.value) updateCalendarPosition()
319
+ if (volumeOpen.value) updateVolumePosition()
320
+ }
321
+
322
+ watch(open, async (isOpen) => {
323
+ if (!isOpen) return
324
+ await nextTick()
325
+ updatePanelPosition()
326
+ })
327
+
328
+ watch(calendarOpen, async (isOpen) => {
329
+ if (!isOpen) return
330
+ await nextTick()
331
+ updateCalendarPosition()
332
+ })
333
+
334
+ watch(volumeOpen, async (isOpen) => {
335
+ if (!isOpen) return
336
+ await nextTick()
337
+ updateVolumePosition()
338
+ })
339
+
340
+ onMounted(() => {
341
+ tick()
342
+ timer = setInterval(tick, 30_000)
343
+ window.addEventListener('keydown', onKey)
344
+ window.addEventListener('resize', onResize)
345
+ })
346
+
347
+ onUnmounted(() => {
348
+ clearInterval(timer)
349
+ window.removeEventListener('keydown', onKey)
350
+ window.removeEventListener('resize', onResize)
351
+ })
352
+ </script>
353
+
354
+ <template>
355
+ <header
356
+ ref="barRef"
357
+ class="paper-bar owd-desktop__system-bar"
358
+ :class="{ 'paper-bar--menu-open': open || calendarOpen || volumeOpen }"
359
+ >
360
+ <div class="paper-bar__start">
361
+ <button
362
+ ref="triggerRef"
363
+ type="button"
364
+ class="paper-bar__trigger"
365
+ :class="{ 'paper-bar__trigger--open': open }"
366
+ :aria-expanded="open"
367
+ aria-haspopup="menu"
368
+ @click="toggleApps()"
369
+ >
370
+ <span class="paper-bar__trigger-icon" aria-hidden="true">
371
+ <svg width="16" height="16" viewBox="0 0 16 16" fill="none">
372
+ <rect
373
+ x="2"
374
+ y="2"
375
+ width="5"
376
+ height="5"
377
+ rx="1"
378
+ fill="currentColor"
379
+ />
380
+ <rect
381
+ x="9"
382
+ y="2"
383
+ width="5"
384
+ height="5"
385
+ rx="1"
386
+ fill="currentColor"
387
+ />
388
+ <rect
389
+ x="2"
390
+ y="9"
391
+ width="5"
392
+ height="5"
393
+ rx="1"
394
+ fill="currentColor"
395
+ />
396
+ <rect
397
+ x="9"
398
+ y="9"
399
+ width="5"
400
+ height="5"
401
+ rx="1"
402
+ fill="currentColor"
403
+ />
404
+ </svg>
405
+ </span>
406
+ <span class="paper-bar__trigger-label">Applications</span>
407
+ <span v-if="appCount > 0" class="paper-bar__trigger-badge">{{
408
+ appCount
409
+ }}</span>
410
+ </button>
411
+ <div
412
+ v-if="runningApps.length > 0"
413
+ class="paper-bar__running"
414
+ role="list"
415
+ aria-label="Open applications"
416
+ >
417
+ <button
418
+ v-for="application in runningApps"
419
+ :key="`${application.id}-${application.openWindowCount.value}`"
420
+ type="button"
421
+ role="listitem"
422
+ class="paper-bar__app-btn"
423
+ :class="{ 'paper-bar__app-btn--active': isAppFocused(application) }"
424
+ :title="application.config.title"
425
+ :aria-label="application.config.title"
426
+ @click="focusRunningApp(application)"
427
+ >
428
+ <img
429
+ v-if="application.config.icon && isIconUrl(application.config.icon)"
430
+ class="paper-bar__app-btn-img"
431
+ :src="application.config.icon"
432
+ alt=""
433
+ />
434
+ <Icon
435
+ v-else-if="application.config.icon"
436
+ :name="application.config.icon"
437
+ :size="16"
438
+ />
439
+ <span v-else class="paper-bar__app-btn-fallback">
440
+ {{ (application.config.title || '?').charAt(0).toUpperCase() }}
441
+ </span>
442
+ </button>
443
+ </div>
444
+
445
+ </div>
446
+
447
+ <div class="paper-bar__end">
448
+ <button
449
+ type="button"
450
+ class="paper-bar__icon-btn"
451
+ :aria-label="appearanceLabel"
452
+ :aria-pressed="isDark"
453
+ @click="toggleAppearance()"
454
+ >
455
+ <svg
456
+ v-if="isDark"
457
+ class="paper-bar__icon-btn-svg"
458
+ width="16"
459
+ height="16"
460
+ viewBox="0 0 16 16"
461
+ fill="none"
462
+ aria-hidden="true"
463
+ >
464
+ <circle cx="8" cy="8" r="3.25" stroke="currentColor" stroke-width="1.5" />
465
+ <path
466
+ d="M8 1.25v1.5M8 13.25v1.5M1.25 8h1.5M13.25 8h1.5M3.1 3.1l1.06 1.06M11.84 11.84l1.06 1.06M3.1 12.9l1.06-1.06M11.84 4.16l1.06-1.06"
467
+ stroke="currentColor"
468
+ stroke-width="1.5"
469
+ stroke-linecap="round"
470
+ />
471
+ </svg>
472
+ <svg
473
+ v-else
474
+ class="paper-bar__icon-btn-svg"
475
+ width="16"
476
+ height="16"
477
+ viewBox="0 0 16 16"
478
+ fill="none"
479
+ aria-hidden="true"
480
+ >
481
+ <path
482
+ d="M8 2.5a5.5 5.5 0 1 0 0 11 4 4 0 0 1 0-11z"
483
+ stroke="currentColor"
484
+ stroke-width="1.5"
485
+ stroke-linejoin="round"
486
+ />
487
+ </svg>
488
+ </button>
489
+ <button
490
+ ref="volumeRef"
491
+ type="button"
492
+ class="paper-bar__icon-btn"
493
+ :class="{ 'paper-bar__icon-btn--open': volumeOpen }"
494
+ :aria-expanded="volumeOpen"
495
+ aria-haspopup="dialog"
496
+ :aria-label="volumeLabel"
497
+ @click="toggleVolume()"
498
+ >
499
+ <svg
500
+ v-if="isVolumeMuted"
501
+ class="paper-bar__icon-btn-svg"
502
+ width="16"
503
+ height="16"
504
+ viewBox="0 0 16 16"
505
+ fill="none"
506
+ aria-hidden="true"
507
+ >
508
+ <path
509
+ d="M3 6.5v3h2.5L9 13V3L5.5 6.5H3z"
510
+ stroke="currentColor"
511
+ stroke-width="1.5"
512
+ stroke-linejoin="round"
513
+ />
514
+ <path
515
+ d="M11.5 6l3 4-3 4M14.5 6l-3 4 3 4"
516
+ stroke="currentColor"
517
+ stroke-width="1.5"
518
+ stroke-linecap="round"
519
+ />
520
+ </svg>
521
+ <svg
522
+ v-else-if="volumeLevel <= 33"
523
+ class="paper-bar__icon-btn-svg"
524
+ width="16"
525
+ height="16"
526
+ viewBox="0 0 16 16"
527
+ fill="none"
528
+ aria-hidden="true"
529
+ >
530
+ <path
531
+ d="M3 6.5v3h2.5L9 13V3L5.5 6.5H3z"
532
+ stroke="currentColor"
533
+ stroke-width="1.5"
534
+ stroke-linejoin="round"
535
+ />
536
+ <path
537
+ d="M11 8.5c0-1-.5-1.75-1-2.25"
538
+ stroke="currentColor"
539
+ stroke-width="1.5"
540
+ stroke-linecap="round"
541
+ />
542
+ </svg>
543
+ <svg
544
+ v-else-if="volumeLevel <= 66"
545
+ class="paper-bar__icon-btn-svg"
546
+ width="16"
547
+ height="16"
548
+ viewBox="0 0 16 16"
549
+ fill="none"
550
+ aria-hidden="true"
551
+ >
552
+ <path
553
+ d="M3 6.5v3h2.5L9 13V3L5.5 6.5H3z"
554
+ stroke="currentColor"
555
+ stroke-width="1.5"
556
+ stroke-linejoin="round"
557
+ />
558
+ <path
559
+ d="M10.5 6.5a3 3 0 0 1 0 3M12.5 5a5 5 0 0 1 0 6"
560
+ stroke="currentColor"
561
+ stroke-width="1.5"
562
+ stroke-linecap="round"
563
+ />
564
+ </svg>
565
+ <svg
566
+ v-else
567
+ class="paper-bar__icon-btn-svg"
568
+ width="16"
569
+ height="16"
570
+ viewBox="0 0 16 16"
571
+ fill="none"
572
+ aria-hidden="true"
573
+ >
574
+ <path
575
+ d="M3 6.5v3h2.5L9 13V3L5.5 6.5H3z"
576
+ stroke="currentColor"
577
+ stroke-width="1.5"
578
+ stroke-linejoin="round"
579
+ />
580
+ <path
581
+ d="M10.5 5.5a4.5 4.5 0 0 1 0 5M12.5 3.5a7.5 7.5 0 0 1 0 9"
582
+ stroke="currentColor"
583
+ stroke-width="1.5"
584
+ stroke-linecap="round"
585
+ />
586
+ </svg>
587
+ </button>
588
+
589
+ <button
590
+ ref="clockRef"
591
+ type="button"
592
+ class="paper-bar__icon-btn paper-bar__time"
593
+ :class="{ 'paper-bar__icon-btn--open': calendarOpen }"
594
+ :aria-expanded="calendarOpen"
595
+ aria-haspopup="dialog"
596
+ :aria-label="`Calendar, ${monthLabel}`"
597
+ @click="toggleCalendar()"
598
+ >
599
+ <time :datetime="selectedDate.toISOString().slice(0, 10)">{{ time }}</time>
600
+ </button>
601
+ </div>
602
+ </header>
603
+
604
+ <Teleport to="body">
605
+ <Transition name="paper-menu">
606
+ <div v-if="open" class="paper-menu" role="presentation">
607
+ <button
608
+ type="button"
609
+ class="paper-menu__scrim"
610
+ aria-label="Close menu"
611
+ @click="closeApps()"
612
+ />
613
+ <nav
614
+ class="paper-menu__panel"
615
+ role="menu"
616
+ aria-label="Applications"
617
+ :style="{
618
+ top: panelPos.top,
619
+ left: panelPos.left,
620
+ minWidth: panelPos.minWidth,
621
+ }"
622
+ >
623
+ <header class="paper-menu__header">
624
+ <p class="paper-menu__eyebrow">Launch</p>
625
+ <p class="paper-menu__title">Applications</p>
626
+ </header>
627
+
628
+ <p v-if="apps.length === 0" class="paper-menu__empty">
629
+ No applications installed.
630
+ </p>
631
+
632
+ <div v-else class="paper-menu__body">
633
+ <section
634
+ v-for="group in groupedApps"
635
+ :key="group.category"
636
+ class="paper-menu__section"
637
+ >
638
+ <h2
639
+ v-if="showGroupHeaders"
640
+ class="paper-menu__section-title"
641
+ >
642
+ {{ $te(`applications.categories.${group.category}`) ? $t(`applications.categories.${group.category}`) : group.category }}
643
+ </h2>
644
+ <ul class="paper-menu__list" role="none">
645
+ <li
646
+ v-for="(entry, i) in group.entries"
647
+ :key="`${entry.application.id}:${entry.command}:${i}`"
648
+ role="none"
649
+ >
650
+ <button
651
+ type="button"
652
+ role="menuitem"
653
+ class="paper-menu__item"
654
+ @click="launch(entry)"
655
+ >
656
+ <span class="paper-menu__icon" aria-hidden="true">
657
+ <img
658
+ v-if="entry.icon && isIconUrl(entry.icon)"
659
+ class="paper-menu__icon-img"
660
+ :src="entry.icon"
661
+ alt=""
662
+ />
663
+ <Icon
664
+ v-else-if="entry.icon"
665
+ :name="entry.icon"
666
+ :size="18"
667
+ />
668
+ <span v-else class="paper-menu__icon-fallback">
669
+ {{ (entry.title || '?').charAt(0).toUpperCase() }}
670
+ </span>
671
+ </span>
672
+ <span class="paper-menu__copy">
673
+ <span class="paper-menu__label">{{ entry.title }}</span>
674
+ <span
675
+ v-if="entrySubtitle(entry)"
676
+ class="paper-menu__meta"
677
+ >
678
+ {{ entrySubtitle(entry) }}
679
+ </span>
680
+ </span>
681
+ <span class="paper-menu__chevron" aria-hidden="true">
682
+ <svg width="14" height="14" viewBox="0 0 14 14">
683
+ <path
684
+ d="M5 3l4 4-4 4"
685
+ fill="none"
686
+ stroke="currentColor"
687
+ stroke-width="1.5"
688
+ stroke-linecap="round"
689
+ stroke-linejoin="round"
690
+ />
691
+ </svg>
692
+ </span>
693
+ </button>
694
+ </li>
695
+ </ul>
696
+ </section>
697
+ </div>
698
+ </nav>
699
+ </div>
700
+ </Transition>
701
+
702
+ <Transition name="paper-menu">
703
+ <div v-if="calendarOpen" class="paper-menu" role="presentation">
704
+ <button
705
+ type="button"
706
+ class="paper-menu__scrim"
707
+ aria-label="Close calendar"
708
+ @click="closeCalendar()"
709
+ />
710
+ <div
711
+ class="paper-calendar paper-menu__panel"
712
+ role="dialog"
713
+ aria-label="Calendar"
714
+ :style="{
715
+ top: calendarPos.top,
716
+ left: calendarPos.left,
717
+ minWidth: calendarPos.minWidth,
718
+ }"
719
+ >
720
+ <header class="paper-calendar__nav">
721
+ <button
722
+ type="button"
723
+ class="paper-calendar__nav-btn"
724
+ aria-label="Previous month"
725
+ @click="prevMonth()"
726
+ >
727
+ ‹
728
+ </button>
729
+ <p class="paper-calendar__month">{{ monthLabel }}</p>
730
+ <button
731
+ type="button"
732
+ class="paper-calendar__nav-btn"
733
+ aria-label="Next month"
734
+ @click="nextMonth()"
735
+ >
736
+ ›
737
+ </button>
738
+ </header>
739
+
740
+ <div class="paper-calendar__weekdays" aria-hidden="true">
741
+ <span
742
+ v-for="label in WEEKDAYS"
743
+ :key="label"
744
+ class="paper-calendar__weekday"
745
+ >
746
+ {{ label }}
747
+ </span>
748
+ </div>
749
+
750
+ <div class="paper-calendar__grid" role="grid">
751
+ <button
752
+ v-for="cell in calendarCells"
753
+ :key="cell.key"
754
+ type="button"
755
+ class="paper-calendar__day"
756
+ :class="{
757
+ 'paper-calendar__day--empty': !cell.date,
758
+ 'paper-calendar__day--today':
759
+ cell.date && isSameDay(cell.date, today),
760
+ 'paper-calendar__day--selected':
761
+ cell.date && isSameDay(cell.date, selectedDate),
762
+ }"
763
+ role="gridcell"
764
+ :tabindex="cell.date ? 0 : -1"
765
+ :disabled="!cell.date"
766
+ :aria-label="
767
+ cell.date
768
+ ? cell.date.toLocaleDateString(undefined, {
769
+ weekday: 'long',
770
+ day: 'numeric',
771
+ month: 'long',
772
+ year: 'numeric',
773
+ })
774
+ : undefined
775
+ "
776
+ :aria-selected="
777
+ cell.date ? isSameDay(cell.date, selectedDate) : undefined
778
+ "
779
+ @click="cell.date && selectDay(cell.date)"
780
+ >
781
+ {{ cell.date?.getDate() }}
782
+ </button>
783
+ </div>
784
+ </div>
785
+ </div>
786
+ </Transition>
787
+
788
+ <Transition name="paper-menu">
789
+ <div v-if="volumeOpen" class="paper-menu" role="presentation">
790
+ <button
791
+ type="button"
792
+ class="paper-menu__scrim"
793
+ aria-label="Close volume"
794
+ @click="closeVolume()"
795
+ />
796
+ <div
797
+ class="paper-volume paper-menu__panel"
798
+ role="dialog"
799
+ aria-label="Volume"
800
+ :style="{
801
+ top: volumePos.top,
802
+ left: volumePos.left,
803
+ minWidth: volumePos.minWidth,
804
+ }"
805
+ >
806
+ <div class="paper-volume__row">
807
+ <button
808
+ type="button"
809
+ class="paper-volume__mute"
810
+ :aria-label="isVolumeMuted ? 'Unmute' : 'Mute'"
811
+ :aria-pressed="isVolumeMuted"
812
+ @click="toggleMute()"
813
+ >
814
+ <svg
815
+ v-if="isVolumeMuted"
816
+ width="14"
817
+ height="14"
818
+ viewBox="0 0 16 16"
819
+ fill="none"
820
+ aria-hidden="true"
821
+ >
822
+ <path
823
+ d="M3 6.5v3h2.5L9 13V3L5.5 6.5H3z"
824
+ stroke="currentColor"
825
+ stroke-width="1.5"
826
+ stroke-linejoin="round"
827
+ />
828
+ <path
829
+ d="M11.5 6l3 4-3 4M14.5 6l-3 4 3 4"
830
+ stroke="currentColor"
831
+ stroke-width="1.5"
832
+ stroke-linecap="round"
833
+ />
834
+ </svg>
835
+ <svg
836
+ v-else
837
+ width="14"
838
+ height="14"
839
+ viewBox="0 0 16 16"
840
+ fill="none"
841
+ aria-hidden="true"
842
+ >
843
+ <path
844
+ d="M3 6.5v3h2.5L9 13V3L5.5 6.5H3z"
845
+ stroke="currentColor"
846
+ stroke-width="1.5"
847
+ stroke-linejoin="round"
848
+ />
849
+ <path
850
+ d="M10.5 5.5a4.5 4.5 0 0 1 0 5M12.5 3.5a7.5 7.5 0 0 1 0 9"
851
+ stroke="currentColor"
852
+ stroke-width="1.5"
853
+ stroke-linecap="round"
854
+ />
855
+ </svg>
856
+ </button>
857
+ <input
858
+ type="range"
859
+ class="paper-volume__slider"
860
+ min="0"
861
+ max="100"
862
+ step="1"
863
+ :value="volumeLevel"
864
+ :aria-valuenow="volumeLevel"
865
+ aria-valuemin="0"
866
+ aria-valuemax="100"
867
+ aria-label="Volume level"
868
+ @input="onVolumeInput"
869
+ />
870
+ </div>
871
+ </div>
872
+ </div>
873
+ </Transition>
874
+ </Teleport>
875
+ </template>
876
+
877
+ <style scoped>
878
+ .paper-bar {
879
+ display: flex;
880
+ align-items: center;
881
+ gap: 1rem;
882
+ flex-shrink: 0;
883
+ height: var(--paper-bar-height);
884
+ padding: 0 1rem;
885
+ border-bottom: 1px solid var(--paper-border);
886
+ background: var(--paper-surface);
887
+ }
888
+
889
+ .paper-bar--menu-open {
890
+ border-bottom-color: var(--paper-border);
891
+ }
892
+
893
+ .paper-bar__start {
894
+ display: flex;
895
+ align-items: center;
896
+ gap: 0.375rem;
897
+ min-width: 0;
898
+ }
899
+
900
+ .paper-bar__running {
901
+ display: flex;
902
+ align-items: center;
903
+ gap: 0.125rem;
904
+ min-width: 0;
905
+ max-width: min(40vw, 24rem);
906
+ overflow-x: auto;
907
+ overscroll-behavior: contain;
908
+ scrollbar-width: none;
909
+ }
910
+
911
+ .paper-bar__running::-webkit-scrollbar {
912
+ display: none;
913
+ }
914
+
915
+ .paper-bar__app-btn {
916
+ display: inline-flex;
917
+ align-items: center;
918
+ justify-content: center;
919
+ flex-shrink: 0;
920
+ width: 1.625rem;
921
+ height: 1.625rem;
922
+ margin: 0;
923
+ padding: 0;
924
+ border: 1px solid transparent;
925
+ border-radius: var(--paper-radius);
926
+ background: none;
927
+ color: var(--paper-text-secondary);
928
+ cursor: pointer;
929
+ opacity: 0.72;
930
+ transition:
931
+ border-color 0.12s ease,
932
+ background-color 0.12s ease,
933
+ color 0.12s ease,
934
+ opacity 0.12s ease;
935
+ }
936
+
937
+ .paper-bar__app-btn:hover {
938
+ opacity: 1;
939
+ background: color-mix(in srgb, var(--paper-bg) 80%, transparent);
940
+ }
941
+
942
+ .paper-bar__app-btn--active,
943
+ .paper-bar__app-btn--active:hover {
944
+ opacity: 1;
945
+ border-color: var(--paper-border);
946
+ background: var(--paper-accent-soft);
947
+ color: var(--paper-accent);
948
+ }
949
+
950
+ .paper-bar__app-btn-img {
951
+ width: 1rem;
952
+ height: 1rem;
953
+ object-fit: contain;
954
+ }
955
+
956
+ .paper-bar__app-btn-fallback {
957
+ font-size: 0.6875rem;
958
+ font-weight: 600;
959
+ line-height: 1;
960
+ }
961
+
962
+ .paper-bar__trigger {
963
+ display: inline-flex;
964
+ align-items: center;
965
+ gap: 0.5rem;
966
+ margin: 0;
967
+ padding: 0.375rem 0.75rem 0.375rem 0.625rem;
968
+ border: 1px solid var(--paper-border);
969
+ border-radius: var(--paper-radius);
970
+ background: var(--paper-surface);
971
+ color: var(--paper-text);
972
+ font: inherit;
973
+ font-size: 0.8125rem;
974
+ font-weight: 500;
975
+ line-height: 1;
976
+ cursor: pointer;
977
+ transition:
978
+ border-color 0.15s ease,
979
+ background-color 0.15s ease,
980
+ box-shadow 0.15s ease,
981
+ color 0.15s ease;
982
+ }
983
+
984
+ .paper-bar__trigger:hover {
985
+ border-color: var(--paper-border);
986
+ background: var(--paper-bg);
987
+ }
988
+
989
+ .paper-bar__trigger--open,
990
+ .paper-bar__trigger--open:hover {
991
+ border-color: color-mix(in srgb, var(--paper-accent) 35%, var(--paper-border));
992
+ background: var(--paper-accent-soft);
993
+ color: var(--paper-accent);
994
+ box-shadow: 0 0 0 3px color-mix(in srgb, var(--paper-accent) 12%, transparent);
995
+ }
996
+
997
+ .paper-bar__trigger-icon {
998
+ display: flex;
999
+ color: var(--paper-text-secondary);
1000
+ }
1001
+
1002
+ .paper-bar__trigger--open .paper-bar__trigger-icon {
1003
+ color: var(--paper-accent);
1004
+ }
1005
+
1006
+ .paper-bar__trigger-badge {
1007
+ display: inline-flex;
1008
+ align-items: center;
1009
+ justify-content: center;
1010
+ min-width: 1.125rem;
1011
+ height: 1.125rem;
1012
+ padding: 0 0.25rem;
1013
+ border-radius: var(--paper-radius-pill);
1014
+ background: var(--paper-bg);
1015
+ color: var(--paper-text-secondary);
1016
+ font-size: 0.6875rem;
1017
+ font-weight: 600;
1018
+ font-variant-numeric: tabular-nums;
1019
+ }
1020
+
1021
+ .paper-bar__trigger--open .paper-bar__trigger-badge {
1022
+ background: var(--paper-accent-soft);
1023
+ color: var(--paper-accent);
1024
+ }
1025
+
1026
+ .paper-bar__end {
1027
+ display: flex;
1028
+ align-items: center;
1029
+ gap: 0.125rem;
1030
+ margin-left: auto;
1031
+ margin-right: -0.375rem;
1032
+ }
1033
+
1034
+ .paper-bar__icon-btn {
1035
+ display: inline-flex;
1036
+ align-items: center;
1037
+ justify-content: center;
1038
+ margin: 0;
1039
+ padding: 0.25rem 0.375rem;
1040
+ border: none;
1041
+ border-radius: var(--paper-radius);
1042
+ background: none;
1043
+ color: var(--paper-text-secondary);
1044
+ font: inherit;
1045
+ cursor: pointer;
1046
+ transition:
1047
+ background-color 0.12s ease,
1048
+ color 0.12s ease;
1049
+ }
1050
+
1051
+ .paper-bar__icon-btn-svg {
1052
+ display: block;
1053
+ }
1054
+
1055
+ .paper-bar__time {
1056
+ font-family: var(--paper-mono);
1057
+ font-size: 0.75rem;
1058
+ font-variant-numeric: tabular-nums;
1059
+ }
1060
+
1061
+ .paper-bar__icon-btn:hover {
1062
+ color: var(--paper-text);
1063
+ background: color-mix(in srgb, var(--paper-bg) 80%, transparent);
1064
+ }
1065
+
1066
+ .paper-bar__icon-btn--open,
1067
+ .paper-bar__icon-btn--open:hover {
1068
+ color: var(--paper-accent);
1069
+ background: var(--paper-accent-soft);
1070
+ }
1071
+
1072
+ /* Menu overlay */
1073
+ .paper-menu {
1074
+ position: fixed;
1075
+ inset: 0;
1076
+ z-index: var(--paper-z-menu);
1077
+ pointer-events: none;
1078
+ }
1079
+
1080
+ .paper-menu__scrim {
1081
+ position: absolute;
1082
+ inset: 0;
1083
+ margin: 0;
1084
+ padding: 0;
1085
+ border: 0;
1086
+ background: var(--paper-scrim);
1087
+ cursor: default;
1088
+ pointer-events: auto;
1089
+ }
1090
+
1091
+ .paper-menu__panel {
1092
+ position: fixed;
1093
+ display: flex;
1094
+ flex-direction: column;
1095
+ box-sizing: border-box;
1096
+ max-height: min(28rem, calc(100dvh - var(--paper-bar-height) - 1.5rem));
1097
+ overflow: hidden;
1098
+ border: 1px solid var(--paper-border);
1099
+ border-radius: var(--paper-radius);
1100
+ background-color: var(--paper-menu-bg);
1101
+ box-shadow: none;
1102
+ pointer-events: auto;
1103
+ }
1104
+
1105
+ .paper-menu__header {
1106
+ flex-shrink: 0;
1107
+ padding: 0.875rem 1rem 0.625rem;
1108
+ border-bottom: 1px solid var(--paper-border);
1109
+ background-color: var(--paper-menu-header-bg);
1110
+ }
1111
+
1112
+ .paper-menu__eyebrow {
1113
+ margin: 0;
1114
+ font-size: 0.6875rem;
1115
+ font-weight: 600;
1116
+ letter-spacing: 0.08em;
1117
+ text-transform: uppercase;
1118
+ color: var(--paper-accent);
1119
+ }
1120
+
1121
+ .paper-menu__title {
1122
+ margin: 0.125rem 0 0;
1123
+ font-size: 1rem;
1124
+ font-weight: 600;
1125
+ letter-spacing: -0.02em;
1126
+ color: var(--paper-text);
1127
+ }
1128
+
1129
+ .paper-menu__body {
1130
+ display: flex;
1131
+ flex-direction: column;
1132
+ overflow-y: auto;
1133
+ overscroll-behavior: contain;
1134
+ padding: 0.25rem 0 0.375rem;
1135
+ background-color: var(--paper-menu-bg);
1136
+ }
1137
+
1138
+ .paper-menu__section + .paper-menu__section {
1139
+ margin-top: 0.25rem;
1140
+ padding-top: 0.25rem;
1141
+ border-top: 1px solid var(--paper-border);
1142
+ }
1143
+
1144
+ .paper-menu__section-title {
1145
+ margin: 0;
1146
+ padding: 0.5rem 1rem 0.25rem;
1147
+ font-size: 0.6875rem;
1148
+ font-weight: 600;
1149
+ letter-spacing: 0.06em;
1150
+ text-transform: uppercase;
1151
+ color: var(--paper-text-tertiary);
1152
+ }
1153
+
1154
+ .paper-menu__list {
1155
+ display: flex;
1156
+ flex-direction: column;
1157
+ gap: 0.125rem;
1158
+ margin: 0;
1159
+ padding: 0 0.375rem;
1160
+ list-style: none;
1161
+ }
1162
+
1163
+ .paper-menu__empty {
1164
+ margin: 0;
1165
+ padding: 1rem 1rem 1.125rem;
1166
+ font-size: 0.8125rem;
1167
+ color: var(--paper-text-secondary);
1168
+ background-color: var(--paper-menu-bg);
1169
+ }
1170
+
1171
+ .paper-menu__item {
1172
+ display: flex;
1173
+ align-items: center;
1174
+ gap: 0.75rem;
1175
+ width: 100%;
1176
+ margin: 0;
1177
+ padding: 0.5rem 0.625rem;
1178
+ border: none;
1179
+ border-radius: var(--paper-radius);
1180
+ background: none;
1181
+ color: var(--paper-text);
1182
+ font: inherit;
1183
+ text-align: left;
1184
+ cursor: pointer;
1185
+ transition:
1186
+ background-color 0.12s ease,
1187
+ color 0.12s ease;
1188
+ }
1189
+
1190
+ .paper-menu__icon {
1191
+ display: flex;
1192
+ flex-shrink: 0;
1193
+ align-items: center;
1194
+ justify-content: center;
1195
+ width: 2rem;
1196
+ height: 2rem;
1197
+ border-radius: var(--paper-radius);
1198
+ background: var(--paper-bg);
1199
+ color: var(--paper-text-secondary);
1200
+ }
1201
+
1202
+ .paper-menu__icon-img {
1203
+ width: 1.125rem;
1204
+ height: 1.125rem;
1205
+ object-fit: contain;
1206
+ }
1207
+
1208
+ .paper-menu__icon-fallback {
1209
+ font-size: 0.8125rem;
1210
+ font-weight: 600;
1211
+ line-height: 1;
1212
+ }
1213
+
1214
+ .paper-menu__copy {
1215
+ flex: 1;
1216
+ min-width: 0;
1217
+ display: flex;
1218
+ flex-direction: column;
1219
+ gap: 0.125rem;
1220
+ }
1221
+
1222
+ .paper-menu__label {
1223
+ overflow: hidden;
1224
+ font-size: 0.875rem;
1225
+ font-weight: 500;
1226
+ line-height: 1.2;
1227
+ text-overflow: ellipsis;
1228
+ white-space: nowrap;
1229
+ }
1230
+
1231
+ .paper-menu__meta {
1232
+ overflow: hidden;
1233
+ font-size: 0.75rem;
1234
+ line-height: 1.2;
1235
+ color: var(--paper-text-secondary);
1236
+ text-overflow: ellipsis;
1237
+ white-space: nowrap;
1238
+ }
1239
+
1240
+ .paper-menu__chevron {
1241
+ flex-shrink: 0;
1242
+ color: var(--paper-text-tertiary);
1243
+ opacity: 0;
1244
+ transform: translateX(-4px);
1245
+ transition:
1246
+ opacity 0.12s ease,
1247
+ transform 0.12s ease,
1248
+ color 0.12s ease;
1249
+ }
1250
+
1251
+ .paper-menu__item:hover,
1252
+ .paper-menu__item:focus-visible {
1253
+ background: var(--paper-accent-soft);
1254
+ outline: none;
1255
+ }
1256
+
1257
+ .paper-menu__item:hover .paper-menu__label,
1258
+ .paper-menu__item:focus-visible .paper-menu__label {
1259
+ color: var(--paper-accent);
1260
+ }
1261
+
1262
+ .paper-menu__item:hover .paper-menu__icon,
1263
+ .paper-menu__item:focus-visible .paper-menu__icon {
1264
+ background: color-mix(in srgb, var(--paper-accent) 12%, white);
1265
+ color: var(--paper-accent);
1266
+ }
1267
+
1268
+ .paper-menu__item:hover .paper-menu__chevron,
1269
+ .paper-menu__item:focus-visible .paper-menu__chevron {
1270
+ opacity: 1;
1271
+ transform: translateX(0);
1272
+ color: var(--paper-accent);
1273
+ }
1274
+
1275
+ /* Enter / leave */
1276
+ .paper-menu-enter-active,
1277
+ .paper-menu-leave-active {
1278
+ transition: opacity 0.18s ease;
1279
+ }
1280
+
1281
+ .paper-menu-enter-active .paper-menu__panel,
1282
+ .paper-menu-leave-active .paper-menu__panel {
1283
+ transition:
1284
+ opacity 0.18s ease,
1285
+ transform 0.18s cubic-bezier(0.22, 1, 0.36, 1);
1286
+ }
1287
+
1288
+ .paper-menu-enter-from,
1289
+ .paper-menu-leave-to {
1290
+ opacity: 0;
1291
+ }
1292
+
1293
+ .paper-menu-enter-from .paper-menu__panel,
1294
+ .paper-menu-leave-to .paper-menu__panel {
1295
+ opacity: 0;
1296
+ transform: translateY(-6px) scale(0.98);
1297
+ }
1298
+
1299
+ /* Calendar */
1300
+ .paper-calendar {
1301
+ padding: 0.75rem;
1302
+ background-color: var(--paper-menu-bg);
1303
+ }
1304
+
1305
+ .paper-calendar__nav {
1306
+ display: flex;
1307
+ align-items: center;
1308
+ justify-content: space-between;
1309
+ gap: 0.5rem;
1310
+ margin-bottom: 0.75rem;
1311
+ }
1312
+
1313
+ .paper-calendar__month {
1314
+ margin: 0;
1315
+ flex: 1;
1316
+ font-size: 0.875rem;
1317
+ font-weight: 600;
1318
+ text-align: center;
1319
+ color: var(--paper-text);
1320
+ }
1321
+
1322
+ .paper-calendar__nav-btn {
1323
+ display: flex;
1324
+ align-items: center;
1325
+ justify-content: center;
1326
+ width: 1.75rem;
1327
+ height: 1.75rem;
1328
+ margin: 0;
1329
+ padding: 0;
1330
+ border: 1px solid var(--paper-border);
1331
+ border-radius: var(--paper-radius);
1332
+ background: var(--paper-surface);
1333
+ color: var(--paper-text-secondary);
1334
+ font-size: 1.125rem;
1335
+ line-height: 1;
1336
+ cursor: pointer;
1337
+ }
1338
+
1339
+ .paper-calendar__nav-btn:hover {
1340
+ border-color: var(--paper-border);
1341
+ color: var(--paper-text);
1342
+ background: var(--paper-bg);
1343
+ }
1344
+
1345
+ .paper-calendar__weekdays {
1346
+ display: grid;
1347
+ grid-template-columns: repeat(7, 1fr);
1348
+ gap: 0.125rem;
1349
+ margin-bottom: 0.25rem;
1350
+ }
1351
+
1352
+ .paper-calendar__weekday {
1353
+ font-size: 0.6875rem;
1354
+ font-weight: 600;
1355
+ text-align: center;
1356
+ color: var(--paper-text-tertiary);
1357
+ }
1358
+
1359
+ .paper-calendar__grid {
1360
+ display: grid;
1361
+ grid-template-columns: repeat(7, 1fr);
1362
+ gap: 0.125rem;
1363
+ }
1364
+
1365
+ .paper-calendar__day {
1366
+ display: flex;
1367
+ align-items: center;
1368
+ justify-content: center;
1369
+ aspect-ratio: 1;
1370
+ min-height: 2rem;
1371
+ margin: 0;
1372
+ padding: 0;
1373
+ border: 1px solid transparent;
1374
+ border-radius: var(--paper-radius);
1375
+ background: none;
1376
+ color: var(--paper-text);
1377
+ font: inherit;
1378
+ font-size: 0.8125rem;
1379
+ font-variant-numeric: tabular-nums;
1380
+ cursor: pointer;
1381
+ }
1382
+
1383
+ .paper-calendar__day--empty {
1384
+ visibility: hidden;
1385
+ pointer-events: none;
1386
+ }
1387
+
1388
+ .paper-calendar__day:not(.paper-calendar__day--empty):hover {
1389
+ background: var(--paper-bg);
1390
+ }
1391
+
1392
+ .paper-calendar__day--today:not(.paper-calendar__day--selected) {
1393
+ border-color: var(--paper-border);
1394
+ font-weight: 600;
1395
+ }
1396
+
1397
+ .paper-calendar__day--selected {
1398
+ border-color: color-mix(in srgb, var(--paper-accent) 40%, var(--paper-border));
1399
+ background: var(--paper-accent-soft);
1400
+ color: var(--paper-accent);
1401
+ font-weight: 600;
1402
+ }
1403
+
1404
+ .paper-calendar__day--selected:hover {
1405
+ background: var(--paper-accent-soft);
1406
+ }
1407
+
1408
+ /* Volume */
1409
+ .paper-volume {
1410
+ padding: 0.625rem 0.75rem;
1411
+ background-color: var(--paper-menu-bg);
1412
+ }
1413
+
1414
+ .paper-volume__row {
1415
+ display: flex;
1416
+ align-items: center;
1417
+ gap: 0.625rem;
1418
+ }
1419
+
1420
+ .paper-volume__mute {
1421
+ display: flex;
1422
+ align-items: center;
1423
+ justify-content: center;
1424
+ flex-shrink: 0;
1425
+ width: 1.75rem;
1426
+ height: 1.75rem;
1427
+ margin: 0;
1428
+ padding: 0;
1429
+ border: 1px solid var(--paper-border);
1430
+ border-radius: var(--paper-radius);
1431
+ background: var(--paper-surface);
1432
+ color: var(--paper-text-secondary);
1433
+ cursor: pointer;
1434
+ }
1435
+
1436
+ .paper-volume__mute:hover {
1437
+ border-color: var(--paper-border);
1438
+ color: var(--paper-text);
1439
+ background: var(--paper-bg);
1440
+ }
1441
+
1442
+ .paper-volume__slider {
1443
+ flex: 1;
1444
+ min-width: 0;
1445
+ height: 0.25rem;
1446
+ margin: 0;
1447
+ padding: 0;
1448
+ border: none;
1449
+ border-radius: var(--paper-radius-pill);
1450
+ background: var(--paper-border);
1451
+ accent-color: var(--paper-accent);
1452
+ cursor: pointer;
1453
+ -webkit-appearance: none;
1454
+ appearance: none;
1455
+ }
1456
+
1457
+ .paper-volume__slider::-webkit-slider-thumb {
1458
+ -webkit-appearance: none;
1459
+ width: 0.875rem;
1460
+ height: 0.875rem;
1461
+ border: 1px solid var(--paper-border);
1462
+ border-radius: var(--paper-radius-circle);
1463
+ background: var(--paper-surface);
1464
+ box-shadow: none;
1465
+ cursor: pointer;
1466
+ }
1467
+
1468
+ .paper-volume__slider::-moz-range-thumb {
1469
+ width: 0.875rem;
1470
+ height: 0.875rem;
1471
+ border: 1px solid var(--paper-border);
1472
+ border-radius: var(--paper-radius-circle);
1473
+ background: var(--paper-surface);
1474
+ box-shadow: none;
1475
+ cursor: pointer;
1476
+ }
1477
+
1478
+ .paper-volume__slider:focus-visible {
1479
+ outline: 2px solid color-mix(in srgb, var(--paper-accent) 40%, transparent);
1480
+ outline-offset: 2px;
1481
+ }
1482
+ </style>