@stacksjs/components 0.2.119 → 0.2.120
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.
- package/dist/{Sidebar-hjazqxv8.stx → Sidebar-vgckg8f7.stx} +185 -33
- package/dist/{SidebarFooter-0aegr99p.stx → SidebarFooter-s7skj92y.stx} +46 -0
- package/dist/{SidebarHeader-ed5wb13t.stx → SidebarHeader-sa9mtdst.stx} +94 -1
- package/dist/SidebarPinned-tvj28bm0.stx +88 -0
- package/dist/SidebarSpace-61b662jx.stx +134 -0
- package/dist/SidebarSpaceSwitcher-882njcxq.stx +98 -0
- package/dist/SidebarSpaces-gq3mx984.stx +560 -0
- package/dist/index.js +164 -7
- package/dist/ui/sidebar/index.d.ts +17 -30
- package/dist/ui/sidebar/rows.d.ts +43 -0
- package/dist/ui/sidebar/spaces.d.ts +105 -0
- package/dist/ui/sidebar/themes.d.ts +5 -0
- package/package.json +3 -3
- package/src/ui/sidebar/Sidebar.stx +185 -33
- package/src/ui/sidebar/SidebarFooter.stx +46 -0
- package/src/ui/sidebar/SidebarHeader.stx +94 -1
- package/src/ui/sidebar/SidebarPinned.stx +88 -0
- package/src/ui/sidebar/SidebarSpace.stx +134 -0
- package/src/ui/sidebar/SidebarSpaceSwitcher.stx +98 -0
- package/src/ui/sidebar/SidebarSpaces.stx +560 -0
- package/src/ui/sidebar/index.ts +38 -42
- package/src/ui/sidebar/rows.ts +55 -0
- package/src/ui/sidebar/spaces.ts +260 -0
- package/src/ui/sidebar/themes.ts +108 -0
|
@@ -1,12 +1,25 @@
|
|
|
1
1
|
<script server>
|
|
2
|
+
import { normalizeSpaces, spaceIndexOf } from './spaces'
|
|
2
3
|
import { resolveSidebarTheme } from './themes'
|
|
3
4
|
|
|
4
5
|
// Data: [{ id, label, collapsible?, collapsed?, items: [{ id, label, icon,
|
|
5
6
|
// iconColor, image, href, count, active?, disabled?, expanded?, children? }] }]
|
|
6
7
|
export const sections = $props.sections || []
|
|
7
8
|
|
|
9
|
+
// Arc-style spaces: [{ id, label, icon, tint, pinned, sections, action, clear }].
|
|
10
|
+
// Passing them turns the sidebar into a swipeable stack of scenes instead of
|
|
11
|
+
// one list. `theme` then defaults to `arc`, because spaces are that theme's
|
|
12
|
+
// idea — a macOS source list has no equivalent.
|
|
13
|
+
export const spaces = $props.spaces || []
|
|
14
|
+
export const hasSpaces = spaces.length > 0
|
|
15
|
+
export const activeSpace = $props.space || $props.activeSpace || ''
|
|
16
|
+
export const showSpaceSwitcher = $props.showSpaceSwitcher ?? true
|
|
17
|
+
export const showSpaceAdd = $props.showSpaceAdd ?? false
|
|
18
|
+
export const spaceAddLabel = $props.spaceAddLabel || 'New space'
|
|
19
|
+
export const spacePersistKey = $props.spacePersistKey || ''
|
|
20
|
+
|
|
8
21
|
// `theme` is the new name; `variant` is accepted for backwards compatibility.
|
|
9
|
-
export const theme = $props.theme || $props.variant || 'macos'
|
|
22
|
+
export const theme = $props.theme || $props.variant || (hasSpaces ? 'arc' : 'macos')
|
|
10
23
|
export const width = $props.width || 250
|
|
11
24
|
export const position = $props.position || 'left'
|
|
12
25
|
export const placement = $props.placement || 'fixed'
|
|
@@ -24,6 +37,14 @@ export const widthVar = $props.widthVar || '--stx-sidebar-width'
|
|
|
24
37
|
export const collapsedClass = $props.collapsedClass || ''
|
|
25
38
|
export const className = $props.className || ''
|
|
26
39
|
|
|
40
|
+
// Mirror the OS light/dark preference onto the root element's `dark` class.
|
|
41
|
+
// A native sidebar follows the system appearance, and Crosswind's `dark:`
|
|
42
|
+
// variants are class-based, so something has to write that class — but it is
|
|
43
|
+
// the document's business, not a navigation pane's. Left on by default so a
|
|
44
|
+
// sidebar dropped into a page with no theme controller still behaves; turn it
|
|
45
|
+
// off in any app that owns its own light/dark switch.
|
|
46
|
+
export const followSystemAppearance = $props.followSystemAppearance ?? true
|
|
47
|
+
|
|
27
48
|
// Unique id so the client controller can find this pane even with several
|
|
28
49
|
// sidebars on the page.
|
|
29
50
|
export const sidebarUid = $props.id || `stx-sidebar-${Math.random().toString(36).slice(2, 8)}`
|
|
@@ -51,6 +72,19 @@ export const paneClasses = [
|
|
|
51
72
|
className,
|
|
52
73
|
].filter(Boolean).join(' ')
|
|
53
74
|
|
|
75
|
+
// The pane carries its own width and, for spaces, the active space's palette.
|
|
76
|
+
// Both are server-rendered rather than bound reactively so the sidebar is the
|
|
77
|
+
// right size and the right color in the very first paint, before any client
|
|
78
|
+
// script runs. The controller keeps `width` in step afterwards.
|
|
79
|
+
const initialWidth = collapsed ? (collapseMode === 'hidden' ? 0 : minWidth) : width
|
|
80
|
+
const normalizedSpaces = hasSpaces ? normalizeSpaces(spaces) : []
|
|
81
|
+
const initialSpace = normalizedSpaces[spaceIndexOf(normalizedSpaces, activeSpace)] || null
|
|
82
|
+
|
|
83
|
+
export const paneStyle = [
|
|
84
|
+
`width: ${initialWidth}px`,
|
|
85
|
+
initialSpace ? initialSpace.style : '',
|
|
86
|
+
].filter(Boolean).join('; ')
|
|
87
|
+
|
|
54
88
|
// Normalized ahead of the loop so template bindings stay simple accessors.
|
|
55
89
|
export const sectionList = sections.map(section => ({
|
|
56
90
|
id: section.id,
|
|
@@ -70,16 +104,17 @@ export const activeClasses = themeStyle.item.active
|
|
|
70
104
|
import { findActiveSidebarHref } from './route-selection'
|
|
71
105
|
|
|
72
106
|
const emit = defineEmits()
|
|
73
|
-
const
|
|
107
|
+
const componentRoot = window.__STX_CURRENT_ELEMENT__
|
|
74
108
|
const collapsedProp = {{ collapsed }}
|
|
75
109
|
const collapsibleProp = {{ collapsible }}
|
|
76
|
-
const collapseModeProp =
|
|
110
|
+
const collapseModeProp = {{ collapseMode }}
|
|
77
111
|
const widthProp = {{ width }}
|
|
78
112
|
const minWidthProp = {{ minWidth }}
|
|
79
|
-
const persistKeyProp =
|
|
80
|
-
const shellSelectorProp =
|
|
81
|
-
const widthVarProp =
|
|
82
|
-
const collapsedClassProp =
|
|
113
|
+
const persistKeyProp = {{ persistKey }}
|
|
114
|
+
const shellSelectorProp = {{ shellSelector }}
|
|
115
|
+
const widthVarProp = {{ widthVar }}
|
|
116
|
+
const collapsedClassProp = {{ collapsedClass }}
|
|
117
|
+
const followSystemAppearanceProp = {{ followSystemAppearance }}
|
|
83
118
|
|
|
84
119
|
const collapsed = state(collapsedProp)
|
|
85
120
|
const sidebarWidth = derived(() => {
|
|
@@ -162,7 +197,10 @@ function toggleRowDisclosure(row) {
|
|
|
162
197
|
|
|
163
198
|
function toggleSection(section) {
|
|
164
199
|
const expanded = section.getAttribute('data-expanded') !== 'false'
|
|
165
|
-
|
|
200
|
+
const nextExpanded = !expanded
|
|
201
|
+
section.setAttribute('data-expanded', String(nextExpanded))
|
|
202
|
+
section.querySelector('[data-sidebar-section-header]')
|
|
203
|
+
?.setAttribute('aria-expanded', String(nextExpanded))
|
|
166
204
|
syncRowVisibility()
|
|
167
205
|
emit('sectionToggle', section.dataset.sectionId)
|
|
168
206
|
}
|
|
@@ -320,6 +358,20 @@ function trackKeyWindow() {
|
|
|
320
358
|
}
|
|
321
359
|
|
|
322
360
|
function followSystemAppearance() {
|
|
361
|
+
// Writing the `dark` class puts a navigation component in charge of the
|
|
362
|
+
// whole document's color scheme, which is only defensible while nothing
|
|
363
|
+
// else wants the job. Two ways to say it does:
|
|
364
|
+
//
|
|
365
|
+
// `follow-system-appearance="false"` — typed, discoverable, and the one to
|
|
366
|
+
// reach for. An app with its own light/dark control wants this.
|
|
367
|
+
// `data-theme` on the root — the runtime signal, for apps that decide at
|
|
368
|
+
// load time. Re-checked on every sync, so claiming it later also works.
|
|
369
|
+
//
|
|
370
|
+
// Before the prop existed only the second was available, which meant an app
|
|
371
|
+
// that set `.dark` itself watched the sidebar quietly undo it on mount, with
|
|
372
|
+
// nothing in the component's API to suggest why.
|
|
373
|
+
if (!followSystemAppearanceProp) return
|
|
374
|
+
|
|
323
375
|
const media = window.matchMedia('(prefers-color-scheme: dark)')
|
|
324
376
|
function sync() {
|
|
325
377
|
if (document.documentElement.dataset.theme) return
|
|
@@ -330,7 +382,7 @@ function followSystemAppearance() {
|
|
|
330
382
|
}
|
|
331
383
|
|
|
332
384
|
onMount(() => {
|
|
333
|
-
pane =
|
|
385
|
+
pane = componentRoot?.querySelector('[data-stx-sidebar]')
|
|
334
386
|
if (!pane) return
|
|
335
387
|
|
|
336
388
|
followSystemAppearance()
|
|
@@ -344,6 +396,13 @@ onMount(() => {
|
|
|
344
396
|
}
|
|
345
397
|
applyCollapseEffects(collapsed())
|
|
346
398
|
|
|
399
|
+
// Width is server-rendered onto the pane so it is correct in the first
|
|
400
|
+
// paint; from here on the effect keeps it in step with the collapse state.
|
|
401
|
+
// (A reactive `:style` binding would have owned the whole attribute, which
|
|
402
|
+
// would mean no width until hydration and would wipe the space palette
|
|
403
|
+
// rendered alongside it.)
|
|
404
|
+
effect(() => { pane.style.width = sidebarWidth() })
|
|
405
|
+
|
|
347
406
|
pane.addEventListener('click', onClick)
|
|
348
407
|
pane.addEventListener('keydown', onKeyDown)
|
|
349
408
|
window.addEventListener('stx:navigate', onRouteChange)
|
|
@@ -372,13 +431,14 @@ defineExpose({ collapsed, toggle: toggleCollapse, syncRouteSelection })
|
|
|
372
431
|
data-window-key="true"
|
|
373
432
|
data-active-class="{{ activeClasses }}"
|
|
374
433
|
class="{{ paneClasses }}"
|
|
375
|
-
|
|
434
|
+
style="{{ paneStyle }}"
|
|
435
|
+
@if(hasSpaces)data-sidebar-spaces="true"@endif
|
|
376
436
|
:aria-hidden="String(collapsed() && collapseModeProp === 'hidden')"
|
|
377
437
|
role="navigation"
|
|
378
438
|
aria-label="Sidebar"
|
|
379
439
|
tabindex="0"
|
|
380
440
|
>
|
|
381
|
-
@if(tintClasses)
|
|
441
|
+
@if(tintClasses && !hasSpaces)
|
|
382
442
|
<div class="absolute inset-0 pointer-events-none {{ tintClasses }}"></div>
|
|
383
443
|
@endif
|
|
384
444
|
@if(hasShimmerRim)
|
|
@@ -391,27 +451,41 @@ defineExpose({ collapsed, toggle: toggleCollapse, syncRouteSelection })
|
|
|
391
451
|
</div>
|
|
392
452
|
@endif
|
|
393
453
|
|
|
394
|
-
|
|
395
|
-
<div
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
:theme="theme"
|
|
406
|
-
/>
|
|
407
|
-
@endforeach
|
|
408
|
-
</nav>
|
|
409
|
-
@else
|
|
410
|
-
<slot />
|
|
411
|
-
@endif
|
|
454
|
+
@if(hasSpaces)
|
|
455
|
+
<div class="stx-sidebar-spaces-slot relative z-10 flex min-h-0 flex-1 flex-col">
|
|
456
|
+
<SidebarSpaces
|
|
457
|
+
:spaces="spaces"
|
|
458
|
+
:space="activeSpace"
|
|
459
|
+
:theme="theme"
|
|
460
|
+
:showSwitcher="showSpaceSwitcher"
|
|
461
|
+
:showAdd="showSpaceAdd"
|
|
462
|
+
:addLabel="spaceAddLabel"
|
|
463
|
+
:persistKey="spacePersistKey"
|
|
464
|
+
/>
|
|
412
465
|
</div>
|
|
413
|
-
|
|
414
|
-
|
|
466
|
+
@else
|
|
467
|
+
<div class="relative z-10 min-h-0 flex-1">
|
|
468
|
+
<div data-sidebar-scroll class="{{ scrollAreaClasses }} h-full stx-sidebar-scroll">
|
|
469
|
+
@if(sectionList.length > 0)
|
|
470
|
+
<nav class="flex flex-col">
|
|
471
|
+
@foreach(sectionList as section)
|
|
472
|
+
<SidebarSection
|
|
473
|
+
:id="section.id"
|
|
474
|
+
:label="section.label"
|
|
475
|
+
:items="section.items"
|
|
476
|
+
:collapsible="section.collapsible"
|
|
477
|
+
:collapsed="section.collapsed"
|
|
478
|
+
:theme="theme"
|
|
479
|
+
/>
|
|
480
|
+
@endforeach
|
|
481
|
+
</nav>
|
|
482
|
+
@else
|
|
483
|
+
<slot />
|
|
484
|
+
@endif
|
|
485
|
+
</div>
|
|
486
|
+
<div data-sidebar-scroll-thumb class="stx-sidebar-scroll-thumb" aria-hidden="true"></div>
|
|
487
|
+
</div>
|
|
488
|
+
@endif
|
|
415
489
|
|
|
416
490
|
@if($slots.footer)
|
|
417
491
|
<div class="relative z-10 flex-shrink-0">
|
|
@@ -537,13 +611,91 @@ defineExpose({ collapsed, toggle: toggleCollapse, syncRouteSelection })
|
|
|
537
611
|
color: var(--stx-sidebar-icon-idle);
|
|
538
612
|
}
|
|
539
613
|
|
|
614
|
+
/* ---------------------------------------------------------------------------
|
|
615
|
+
* Spaces — the `arc` theme's swipeable scenes.
|
|
616
|
+
*
|
|
617
|
+
* The panel takes the active space's color, and switching spaces *crossfades*
|
|
618
|
+
* it rather than cutting. That crossfade is why the four properties below are
|
|
619
|
+
* registered with `@property`: an unregistered custom property is a token the
|
|
620
|
+
* browser cannot interpolate, so `transition` on it does nothing. Typed as
|
|
621
|
+
* `<color>`, they animate like any other color.
|
|
622
|
+
*
|
|
623
|
+
* Each space publishes both appearances as inline variables (SidebarSpaces
|
|
624
|
+
* writes them on swipe, the server writes the first one), and these rules pick
|
|
625
|
+
* between them. Keeping the choice in CSS is what lets a dark-mode space
|
|
626
|
+
* render correctly server-side, before any script runs.
|
|
627
|
+
* ------------------------------------------------------------------------- */
|
|
628
|
+
@property --stx-space-from { syntax: "<color>"; inherits: true; initial-value: #f2f0ec; }
|
|
629
|
+
@property --stx-space-to { syntax: "<color>"; inherits: true; initial-value: #e6e3dd; }
|
|
630
|
+
@property --stx-space-ink { syntax: "<color>"; inherits: true; initial-value: #2c2a28; }
|
|
631
|
+
@property --stx-space-accent { syntax: "<color>"; inherits: true; initial-value: #5b5b63; }
|
|
632
|
+
|
|
633
|
+
/* The doubled attribute is for specificity, not for matching: the theme's own
|
|
634
|
+
`dark:bg-…` / `dark:text-…` utilities compile to `.dark .x`, which ties with
|
|
635
|
+
a single attribute plus the scope marker. A tie would leave the outcome to
|
|
636
|
+
stylesheet order — true today, but not something the space color should
|
|
637
|
+
depend on. */
|
|
638
|
+
|
|
639
|
+
[data-sidebar-spaces="true"][data-stx-sidebar] {
|
|
640
|
+
--stx-space-from: var(--stx-space-light-from, #f2f0ec);
|
|
641
|
+
--stx-space-to: var(--stx-space-light-to, #e6e3dd);
|
|
642
|
+
--stx-space-ink: var(--stx-space-light-ink, #2c2a28);
|
|
643
|
+
--stx-space-accent: var(--stx-space-light-accent, #5b5b63);
|
|
644
|
+
/* Two layers: a sheen falling off over the top third, then the space color.
|
|
645
|
+
The sheen is what keeps a saturated space from reading as a flat swatch. */
|
|
646
|
+
background:
|
|
647
|
+
linear-gradient(180deg, rgba(255, 255, 255, 0.34) 0%, rgba(255, 255, 255, 0) 32%),
|
|
648
|
+
linear-gradient(180deg, var(--stx-space-from) 0%, var(--stx-space-to) 100%);
|
|
649
|
+
color: var(--stx-space-ink);
|
|
650
|
+
/* Width is listed here because this rule outranks the pane's
|
|
651
|
+
`transition-[width]` utility class and would otherwise drop it. */
|
|
652
|
+
transition:
|
|
653
|
+
--stx-space-from 420ms ease,
|
|
654
|
+
--stx-space-to 420ms ease,
|
|
655
|
+
--stx-space-ink 420ms ease,
|
|
656
|
+
--stx-space-accent 420ms ease,
|
|
657
|
+
width 300ms ease-out;
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
@media (prefers-color-scheme: dark) {
|
|
661
|
+
[data-sidebar-spaces="true"][data-stx-sidebar] {
|
|
662
|
+
--stx-space-from: var(--stx-space-dark-from, #17171b);
|
|
663
|
+
--stx-space-to: var(--stx-space-dark-to, #0e0e11);
|
|
664
|
+
--stx-space-ink: var(--stx-space-dark-ink, #ededf0);
|
|
665
|
+
--stx-space-accent: var(--stx-space-dark-accent, #b9b9c2);
|
|
666
|
+
background:
|
|
667
|
+
linear-gradient(180deg, rgba(255, 255, 255, 0.06) 0%, rgba(255, 255, 255, 0) 30%),
|
|
668
|
+
linear-gradient(180deg, var(--stx-space-from) 0%, var(--stx-space-to) 100%);
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
@media (prefers-reduced-motion: reduce) {
|
|
673
|
+
[data-sidebar-spaces="true"][data-stx-sidebar] { transition: none; }
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
/* A component carrying a client script renders inside a scope wrapper, and
|
|
677
|
+
that wrapper is a plain block we cannot put classes on from here — so the
|
|
678
|
+
spaces root's own `flex-1` sizes itself against the wrapper instead of
|
|
679
|
+
against this column, and the switcher rail floats up under the last row
|
|
680
|
+
rather than sitting on the floor of the pane. Collapsing the wrapper out of
|
|
681
|
+
layout makes the spaces root the flex item, which is what it was written to
|
|
682
|
+
be. The selector deliberately spares the root itself, for the case where
|
|
683
|
+
there is no wrapper at all. */
|
|
684
|
+
.stx-sidebar-spaces-slot > *:not([data-stx-sidebar-spaces]):not(script):not(style) {
|
|
685
|
+
display: contents;
|
|
686
|
+
}
|
|
687
|
+
|
|
540
688
|
/* Rows collapse like AppKit outlines: fixed row height animates to zero.
|
|
541
689
|
The 32px pitch is the row itself — AppKit leaves no gap, so a run of
|
|
542
|
-
selected or hovered rows reads as one continuous block.
|
|
690
|
+
selected or hovered rows reads as one continuous block. Arc's rows are
|
|
691
|
+
discrete cards with their own gutter, so the theme overrides the pitch. */
|
|
543
692
|
[data-sidebar-row] {
|
|
544
|
-
height: 32px;
|
|
693
|
+
height: var(--stx-sidebar-row-h, 32px);
|
|
545
694
|
transition: height 0.22s ease, opacity 0.16s ease;
|
|
546
695
|
}
|
|
696
|
+
[data-sidebar-theme="arc"] {
|
|
697
|
+
--stx-sidebar-row-h: 30px;
|
|
698
|
+
}
|
|
547
699
|
[data-sidebar-row][data-collapsed] {
|
|
548
700
|
height: 0;
|
|
549
701
|
opacity: 0;
|
|
@@ -24,6 +24,26 @@ export const actionClasses = [
|
|
|
24
24
|
'transition-colors duration-150',
|
|
25
25
|
'hover:bg-black/5 dark:hover:bg-white/10',
|
|
26
26
|
].join(' ')
|
|
27
|
+
|
|
28
|
+
// Arc keeps the footer to a quiet strip of ghost icons, tuned to the warm
|
|
29
|
+
// panel rather than the macOS neutral.
|
|
30
|
+
export const isArc = theme === 'arc'
|
|
31
|
+
|
|
32
|
+
// Like the rest of the Arc chrome, these take their color from the panel so
|
|
33
|
+
// they follow a space's ink instead of staying a fixed dark on top of it.
|
|
34
|
+
export const arcActionClasses = [
|
|
35
|
+
'grid h-[28px] w-[28px] place-items-center rounded-[7px]',
|
|
36
|
+
'opacity-50 transition-[background-color,opacity] duration-150',
|
|
37
|
+
'hover:bg-black/[0.05] hover:opacity-100 dark:hover:bg-white/10',
|
|
38
|
+
].join(' ')
|
|
39
|
+
|
|
40
|
+
export const arcProfileClasses = [
|
|
41
|
+
'flex w-full items-center gap-[8px]',
|
|
42
|
+
'h-[38px] rounded-[8px] px-[6px]',
|
|
43
|
+
'text-[13px] font-medium',
|
|
44
|
+
'transition-colors duration-150',
|
|
45
|
+
'hover:bg-black/[0.05] dark:hover:bg-white/8',
|
|
46
|
+
].join(' ')
|
|
27
47
|
</script>
|
|
28
48
|
|
|
29
49
|
<script client>
|
|
@@ -64,6 +84,32 @@ function onAction(event) {
|
|
|
64
84
|
@endif
|
|
65
85
|
<slot />
|
|
66
86
|
</div>
|
|
87
|
+
@elseif(isArc)
|
|
88
|
+
<div class="shrink-0 px-[10px] pb-[10px] pt-[4px]">
|
|
89
|
+
@if(name)
|
|
90
|
+
<button type="button" class="{{ arcProfileClasses }}" @click="onProfileClick()">
|
|
91
|
+
@if(avatar)
|
|
92
|
+
<img src="{{ avatar }}" alt="" class="h-[24px] w-[24px] rounded-full object-cover" />
|
|
93
|
+
@else
|
|
94
|
+
<span class="grid h-[24px] w-[24px] place-items-center rounded-full bg-black/8 text-[12px] font-semibold dark:bg-white/12">{{ name.charAt(0) }}</span>
|
|
95
|
+
@endif
|
|
96
|
+
<span class="min-w-0 flex-1 truncate text-left">{{ name }}</span>
|
|
97
|
+
@if(detail)
|
|
98
|
+
<span class="shrink-0 text-[12px] opacity-45">{{ detail }}</span>
|
|
99
|
+
@endif
|
|
100
|
+
</button>
|
|
101
|
+
@endif
|
|
102
|
+
@if(actions.length > 0)
|
|
103
|
+
<div class="flex items-center gap-[2px]">
|
|
104
|
+
@foreach(actions as action)
|
|
105
|
+
<button type="button" class="{{ arcActionClasses }}" aria-label="{{ action.label }}" data-sidebar-action="{{ action.id }}" @click="onAction($event)">
|
|
106
|
+
<span class="{{ action.icon }} h-[15px] w-[15px]" aria-hidden="true"></span>
|
|
107
|
+
</button>
|
|
108
|
+
@endforeach
|
|
109
|
+
</div>
|
|
110
|
+
@endif
|
|
111
|
+
<slot />
|
|
112
|
+
</div>
|
|
67
113
|
@else
|
|
68
114
|
<div class="shrink-0 border-t border-black/5 p-3 dark:border-white/10">
|
|
69
115
|
<slot />
|
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
// the left, floating "Liquid Glass" toolbar buttons on the right, and the
|
|
4
4
|
// whole strip acts as a window drag region inside a Craft window.
|
|
5
5
|
export const theme = $props.theme || $props.variant || 'macos'
|
|
6
|
-
export const showWindowControls = $props.showWindowControls ?? true
|
|
7
6
|
export const actions = $props.actions || [] // [{ id, icon, label }]
|
|
8
7
|
export const title = $props.title || ''
|
|
9
8
|
export const subtitle = $props.subtitle || ''
|
|
@@ -28,6 +27,45 @@ export const toolbarButtonClasses = [
|
|
|
28
27
|
'hover:bg-white/80 dark:hover:bg-white/20',
|
|
29
28
|
'active:bg-white/95 dark:active:bg-white/25',
|
|
30
29
|
].join(' ')
|
|
30
|
+
|
|
31
|
+
// Arc's header is quieter than macOS'. No floating glass buttons — just a
|
|
32
|
+
// recessed search field and flat ghost icons, sized to sit on the panel
|
|
33
|
+
// without competing with the row cards below it.
|
|
34
|
+
export const isArc = theme === 'arc'
|
|
35
|
+
|
|
36
|
+
// Window chrome. A macOS sidebar owns the traffic lights, so they are on by
|
|
37
|
+
// default there. Arc's sidebar can go either way — a browser window puts the
|
|
38
|
+
// lights and the back/forward controls in the sidebar, while an app that only
|
|
39
|
+
// borrows the look wants a bare strip — so on this theme both are opt-in.
|
|
40
|
+
export const showWindowControls = $props.showWindowControls ?? !isArc
|
|
41
|
+
export const showNavigation = $props.showNavigation ?? false
|
|
42
|
+
|
|
43
|
+
// Back / forward / reload, as in Arc's URL strip. Flat and ghosted, not glass.
|
|
44
|
+
export const navButtons = [
|
|
45
|
+
{ id: 'back', icon: 'i-f7-arrow-left', label: 'Back' },
|
|
46
|
+
{ id: 'forward', icon: 'i-f7-arrow-right', label: 'Forward' },
|
|
47
|
+
{ id: 'reload', icon: 'i-f7-arrow-clockwise', label: 'Reload' },
|
|
48
|
+
]
|
|
49
|
+
|
|
50
|
+
// Arc's chrome takes its color from the panel rather than declaring its own.
|
|
51
|
+
// On a plain warm panel that resolves to the theme's ink; inside a space it
|
|
52
|
+
// follows `--stx-space-ink` and crossfades along with the panel on every
|
|
53
|
+
// swipe. A hardcoded `text-[#2c2a28]` would stay dark over a saturated space
|
|
54
|
+
// and would not move when the space changed.
|
|
55
|
+
export const arcSearchClasses = [
|
|
56
|
+
'h-[30px] w-full rounded-[8px] border-0',
|
|
57
|
+
'bg-black/[0.045] dark:bg-white/[0.07]',
|
|
58
|
+
'pl-[28px] pr-[8px]',
|
|
59
|
+
'text-[13px] text-current',
|
|
60
|
+
'placeholder:text-current placeholder:opacity-40',
|
|
61
|
+
'focus:outline-none focus:ring-2 focus:ring-black/10 dark:focus:ring-white/15',
|
|
62
|
+
].join(' ')
|
|
63
|
+
|
|
64
|
+
export const arcActionClasses = [
|
|
65
|
+
'grid h-[28px] w-[28px] place-items-center rounded-[7px]',
|
|
66
|
+
'opacity-55 transition-[background-color,opacity] duration-150',
|
|
67
|
+
'hover:bg-black/[0.05] hover:opacity-100 dark:hover:bg-white/10',
|
|
68
|
+
].join(' ')
|
|
31
69
|
</script>
|
|
32
70
|
|
|
33
71
|
<script client>
|
|
@@ -85,6 +123,61 @@ function onWindowControl(action) {
|
|
|
85
123
|
</div>
|
|
86
124
|
</div>
|
|
87
125
|
@endif
|
|
126
|
+
@elseif(isArc)
|
|
127
|
+
<div class="flex flex-col gap-[8px] px-[10px] pb-[8px] pt-[12px]">
|
|
128
|
+
@if(showWindowControls || showNavigation)
|
|
129
|
+
<div class="flex items-center px-[2px] [-webkit-app-region:drag]">
|
|
130
|
+
@if(showWindowControls)
|
|
131
|
+
<div class="flex items-center gap-[7px] [-webkit-app-region:no-drag]">
|
|
132
|
+
<button type="button" class="{{ trafficLightClasses }} bg-[#ff5f57]" aria-label="Close window" @click="onWindowControl('close')"></button>
|
|
133
|
+
<button type="button" class="{{ trafficLightClasses }} bg-[#febc2e]" aria-label="Minimize window" @click="onWindowControl('minimize')"></button>
|
|
134
|
+
<button type="button" class="{{ trafficLightClasses }} bg-[#28c840]" aria-label="Zoom window" @click="onWindowControl('zoom')"></button>
|
|
135
|
+
</div>
|
|
136
|
+
@endif
|
|
137
|
+
<span class="flex-1"></span>
|
|
138
|
+
@if(showNavigation)
|
|
139
|
+
<div class="flex items-center gap-[2px] [-webkit-app-region:no-drag]">
|
|
140
|
+
@foreach(navButtons as nav)
|
|
141
|
+
<button type="button" class="{{ arcActionClasses }}" aria-label="{{ nav.label }}" data-sidebar-action="{{ nav.id }}" @click="onAction($event)">
|
|
142
|
+
<span class="{{ nav.icon }} h-[14px] w-[14px]" aria-hidden="true"></span>
|
|
143
|
+
</button>
|
|
144
|
+
@endforeach
|
|
145
|
+
</div>
|
|
146
|
+
@endif
|
|
147
|
+
</div>
|
|
148
|
+
@endif
|
|
149
|
+
|
|
150
|
+
@if(title || logo || actions.length > 0)
|
|
151
|
+
<div class="flex items-center gap-[8px]">
|
|
152
|
+
@if(logo)
|
|
153
|
+
<img src="{{ logo }}" alt="{{ title }}" class="h-[20px] w-[20px] rounded-[6px] object-cover" />
|
|
154
|
+
@endif
|
|
155
|
+
@if(title)
|
|
156
|
+
<span class="min-w-0 flex-1 truncate text-[13px] font-medium">{{ title }}</span>
|
|
157
|
+
@else
|
|
158
|
+
<span class="flex-1"></span>
|
|
159
|
+
@endif
|
|
160
|
+
@foreach(actions as action)
|
|
161
|
+
<button type="button" class="{{ arcActionClasses }}" aria-label="{{ action.label }}" data-sidebar-action="{{ action.id }}" @click="onAction($event)">
|
|
162
|
+
<span class="{{ action.icon }} h-[15px] w-[15px]" aria-hidden="true"></span>
|
|
163
|
+
</button>
|
|
164
|
+
@endforeach
|
|
165
|
+
<slot name="actions" />
|
|
166
|
+
</div>
|
|
167
|
+
@endif
|
|
168
|
+
|
|
169
|
+
@if(showSearch)
|
|
170
|
+
<div class="relative">
|
|
171
|
+
<span class="i-f7-search absolute left-[9px] top-1/2 h-[13px] w-[13px] -translate-y-1/2 opacity-40" aria-hidden="true"></span>
|
|
172
|
+
<input
|
|
173
|
+
type="text"
|
|
174
|
+
placeholder="{{ searchPlaceholder }}"
|
|
175
|
+
class="{{ arcSearchClasses }}"
|
|
176
|
+
@input="onSearchInput($event)"
|
|
177
|
+
/>
|
|
178
|
+
</div>
|
|
179
|
+
@endif
|
|
180
|
+
</div>
|
|
88
181
|
@else
|
|
89
182
|
<div class="flex h-13 items-center gap-3 border-b border-black/5 px-4 dark:border-white/10">
|
|
90
183
|
@if(logo)
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
<script server>
|
|
2
|
+
import { resolveIconColor } from './themes'
|
|
3
|
+
|
|
4
|
+
// Arc's favorites grid — the block of square tiles above a space's rows.
|
|
5
|
+
//
|
|
6
|
+
// These are deliberately NOT sidebar rows. They carry no `data-sidebar-item`,
|
|
7
|
+
// so the Sidebar controller's selection, arrow-key navigation and disclosure
|
|
8
|
+
// logic skip them entirely: a pinned tile is a shortcut, not a place in the
|
|
9
|
+
// list. Their own click handler emits `pinnedClick` and that is the whole
|
|
10
|
+
// contract.
|
|
11
|
+
export const items = ($props.items || []).map(item => ({
|
|
12
|
+
id: item.id,
|
|
13
|
+
label: item.label || '',
|
|
14
|
+
icon: item.icon || '',
|
|
15
|
+
image: item.image || '',
|
|
16
|
+
href: item.href || '',
|
|
17
|
+
iconStyle: resolveIconColor(item.iconColor) ? `color: ${resolveIconColor(item.iconColor)}` : '',
|
|
18
|
+
}))
|
|
19
|
+
|
|
20
|
+
export const columns = $props.columns || 4
|
|
21
|
+
|
|
22
|
+
// Tiles are translucent white so they pick up whichever space color sits
|
|
23
|
+
// behind them, rather than fighting it with a fixed fill.
|
|
24
|
+
export const tileClasses = [
|
|
25
|
+
'grid h-[46px] place-items-center rounded-[10px]',
|
|
26
|
+
'bg-white/45 dark:bg-white/8',
|
|
27
|
+
'transition-[background-color,transform] duration-150 ease-out',
|
|
28
|
+
'hover:bg-white/72 dark:hover:bg-white/14',
|
|
29
|
+
'active:scale-[0.96]',
|
|
30
|
+
].join(' ')
|
|
31
|
+
</script>
|
|
32
|
+
|
|
33
|
+
<script client>
|
|
34
|
+
const emit = defineEmits()
|
|
35
|
+
|
|
36
|
+
function onPinnedClick(event) {
|
|
37
|
+
const tile = event.target.closest('[data-space-pinned-item]')
|
|
38
|
+
if (!tile) return
|
|
39
|
+
emit('pinnedClick', { id: tile.dataset.pinnedId, href: tile.getAttribute('href') || '', event })
|
|
40
|
+
}
|
|
41
|
+
</script>
|
|
42
|
+
|
|
43
|
+
@if(items.length > 0)
|
|
44
|
+
<div
|
|
45
|
+
data-space-pinned
|
|
46
|
+
class="grid gap-[6px] px-[8px] pb-[10px] pt-[2px]"
|
|
47
|
+
style="grid-template-columns: repeat({{ columns }}, minmax(0, 1fr))"
|
|
48
|
+
role="list"
|
|
49
|
+
aria-label="Pinned"
|
|
50
|
+
@click="onPinnedClick($event)"
|
|
51
|
+
>
|
|
52
|
+
@foreach(items as item)
|
|
53
|
+
@if(item.href)
|
|
54
|
+
<a
|
|
55
|
+
href="{{ item.href }}"
|
|
56
|
+
data-space-pinned-item
|
|
57
|
+
data-pinned-id="{{ item.id }}"
|
|
58
|
+
class="{{ tileClasses }}"
|
|
59
|
+
role="listitem"
|
|
60
|
+
title="{{ item.label }}"
|
|
61
|
+
aria-label="{{ item.label }}"
|
|
62
|
+
>
|
|
63
|
+
@if(item.image)
|
|
64
|
+
<img src="{{ item.image }}" alt="" class="h-[22px] w-[22px] rounded-[5px] object-cover" />
|
|
65
|
+
@else
|
|
66
|
+
<span class="{{ item.icon }} h-[20px] w-[20px]" @if(item.iconStyle)style="{{ item.iconStyle }}"@endif aria-hidden="true"></span>
|
|
67
|
+
@endif
|
|
68
|
+
</a>
|
|
69
|
+
@else
|
|
70
|
+
<button
|
|
71
|
+
type="button"
|
|
72
|
+
data-space-pinned-item
|
|
73
|
+
data-pinned-id="{{ item.id }}"
|
|
74
|
+
class="{{ tileClasses }}"
|
|
75
|
+
role="listitem"
|
|
76
|
+
title="{{ item.label }}"
|
|
77
|
+
aria-label="{{ item.label }}"
|
|
78
|
+
>
|
|
79
|
+
@if(item.image)
|
|
80
|
+
<img src="{{ item.image }}" alt="" class="h-[22px] w-[22px] rounded-[5px] object-cover" />
|
|
81
|
+
@else
|
|
82
|
+
<span class="{{ item.icon }} h-[20px] w-[20px]" @if(item.iconStyle)style="{{ item.iconStyle }}"@endif aria-hidden="true"></span>
|
|
83
|
+
@endif
|
|
84
|
+
</button>
|
|
85
|
+
@endif
|
|
86
|
+
@endforeach
|
|
87
|
+
</div>
|
|
88
|
+
@endif
|