@stacksjs/components 0.2.165 → 0.2.167
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-56px4jha.stx → Sidebar-v7ecx5gs.stx} +6 -4
- package/dist/{SidebarSpace-h50n47va.stx → SidebarSpace-r062weq2.stx} +37 -3
- package/dist/SidebarSpaceSwitcher-az9wqm87.stx +189 -0
- package/dist/{SidebarSpaces-sy8dy0gb.stx → SidebarSpaces-fgxhn4ac.stx} +31 -4
- package/dist/index.js +10 -9
- package/dist/ui/sidebar/spaces.d.ts +2 -0
- package/package.json +3 -3
- package/src/ui/sidebar/Sidebar.stx +6 -4
- package/src/ui/sidebar/SidebarSpace.stx +37 -3
- package/src/ui/sidebar/SidebarSpaceSwitcher.stx +150 -59
- package/src/ui/sidebar/SidebarSpaces.stx +31 -4
- package/src/ui/sidebar/spaces.ts +17 -0
- package/dist/SidebarSpaceSwitcher-882njcxq.stx +0 -98
|
@@ -639,10 +639,12 @@ defineExpose({ collapsed, toggle: toggleCollapse, syncRouteSelection })
|
|
|
639
639
|
/* Width is listed here because this rule outranks the pane's
|
|
640
640
|
`transition-[width]` utility class and would otherwise drop it. */
|
|
641
641
|
transition:
|
|
642
|
-
--stx-space-from 420ms ease,
|
|
643
|
-
--stx-space-to 420ms ease,
|
|
644
|
-
--stx-space-ink 420ms ease,
|
|
645
|
-
--stx-space-accent 420ms ease,
|
|
642
|
+
--stx-space-from var(--stx-space-settle, 420ms) var(--stx-space-ease, ease),
|
|
643
|
+
--stx-space-to var(--stx-space-settle, 420ms) var(--stx-space-ease, ease),
|
|
644
|
+
--stx-space-ink var(--stx-space-settle, 420ms) var(--stx-space-ease, ease),
|
|
645
|
+
--stx-space-accent var(--stx-space-settle, 420ms) var(--stx-space-ease, ease),
|
|
646
|
+
/* Collapsing the pane is a different intent from changing space, so it
|
|
647
|
+
keeps its own timing. */
|
|
646
648
|
width 300ms ease-out;
|
|
647
649
|
}
|
|
648
650
|
|
|
@@ -16,6 +16,19 @@ export const clear = $props.clear || null
|
|
|
16
16
|
export const theme = $props.theme || 'arc'
|
|
17
17
|
export const active = $props.active ?? false
|
|
18
18
|
|
|
19
|
+
// Render the shell without its contents.
|
|
20
|
+
//
|
|
21
|
+
// Every space's rows are rendered on the server even though the Arc design
|
|
22
|
+
// shows one at a time, and that cost is linear in the number of spaces: a host
|
|
23
|
+
// with fourteen projects pays for fourteen panels on every page load to display
|
|
24
|
+
// one. `deferred` keeps the panel — the track's geometry, the tab role and the
|
|
25
|
+
// swipe all depend on it existing — and drops what is inside it.
|
|
26
|
+
//
|
|
27
|
+
// The space still announces itself as a tabpanel, so the switcher and keyboard
|
|
28
|
+
// navigation are unchanged; `data-space-deferred` tells the controller this one
|
|
29
|
+
// has nothing yet, and `spaceChange` is where a host fills it or navigates.
|
|
30
|
+
export const deferred = $props.deferred ?? false
|
|
31
|
+
|
|
19
32
|
const themeStyle = resolveSidebarTheme(theme)
|
|
20
33
|
export const scrollAreaClasses = themeStyle.scrollArea
|
|
21
34
|
|
|
@@ -32,7 +45,7 @@ export const actionClasses = [
|
|
|
32
45
|
'flex w-full items-center gap-[8px]',
|
|
33
46
|
'h-[30px] rounded-[8px] px-[6px]',
|
|
34
47
|
'text-[13px] leading-[16px]',
|
|
35
|
-
'opacity-55
|
|
48
|
+
'opacity-55 stx-space-timed',
|
|
36
49
|
'hover:bg-white/45 hover:opacity-100 dark:hover:bg-white/8',
|
|
37
50
|
'cursor-default',
|
|
38
51
|
].join(' ')
|
|
@@ -40,7 +53,7 @@ export const actionClasses = [
|
|
|
40
53
|
export const clearClasses = [
|
|
41
54
|
'flex items-center gap-[4px] rounded-[6px] px-[5px] py-[2px]',
|
|
42
55
|
'text-[11px] leading-[14px]',
|
|
43
|
-
'opacity-45
|
|
56
|
+
'opacity-45 stx-space-timed',
|
|
44
57
|
'hover:opacity-90',
|
|
45
58
|
'cursor-default',
|
|
46
59
|
].join(' ')
|
|
@@ -64,6 +77,7 @@ function onAction(event) {
|
|
|
64
77
|
data-space
|
|
65
78
|
data-space-id="{{ id }}"
|
|
66
79
|
data-space-active="{{ active ? 'true' : 'false' }}"
|
|
80
|
+
@if(deferred)data-space-deferred@endif
|
|
67
81
|
@if(!active)inert@endif
|
|
68
82
|
class="flex min-w-0 flex-1 flex-col"
|
|
69
83
|
role="tabpanel"
|
|
@@ -71,7 +85,9 @@ function onAction(event) {
|
|
|
71
85
|
@click="onAction($event)"
|
|
72
86
|
>
|
|
73
87
|
<div class="{{ scrollAreaClasses }} stx-sidebar-scroll">
|
|
74
|
-
|
|
88
|
+
@if(!deferred)
|
|
89
|
+
<SidebarPinned :items="pinned" />
|
|
90
|
+
@endif
|
|
75
91
|
|
|
76
92
|
@if(label)
|
|
77
93
|
<div class="{{ titleClasses }}">
|
|
@@ -82,6 +98,12 @@ function onAction(event) {
|
|
|
82
98
|
</div>
|
|
83
99
|
@endif
|
|
84
100
|
|
|
101
|
+
<!--
|
|
102
|
+
Rows, and the actions that belong with them, are what a deferred panel
|
|
103
|
+
drops. The title above stays: it is a single line, and it is what tells
|
|
104
|
+
you which space you are swiping into.
|
|
105
|
+
-->
|
|
106
|
+
@if(!deferred)
|
|
85
107
|
@foreach(sections as section)
|
|
86
108
|
<SidebarSection
|
|
87
109
|
:id="section.id"
|
|
@@ -123,10 +145,22 @@ function onAction(event) {
|
|
|
123
145
|
<span class="truncate">{{ action.label }}</span>
|
|
124
146
|
</button>
|
|
125
147
|
@endif
|
|
148
|
+
@endif
|
|
126
149
|
</div>
|
|
127
150
|
</section>
|
|
128
151
|
|
|
129
152
|
<style scoped>
|
|
153
|
+
/* Anything that changes with the space shares one duration and one curve, so a
|
|
154
|
+
swipe is a single motion. See `--stx-space-settle` in SidebarSpaces. */
|
|
155
|
+
.stx-space-timed {
|
|
156
|
+
transition:
|
|
157
|
+
background-color var(--stx-space-settle, 420ms) var(--stx-space-ease, ease),
|
|
158
|
+
opacity var(--stx-space-settle, 420ms) var(--stx-space-ease, ease),
|
|
159
|
+
color var(--stx-space-settle, 420ms) var(--stx-space-ease, ease);
|
|
160
|
+
}
|
|
161
|
+
/* Hover is a separate intent and stays quick. */
|
|
162
|
+
.stx-space-timed:hover { transition-duration: 120ms; }
|
|
163
|
+
|
|
130
164
|
/* The title icon rides the space accent. Published as a custom property by
|
|
131
165
|
SidebarSpaces so it crossfades along with the panel instead of cutting. */
|
|
132
166
|
.stx-space-title-icon {
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
<script server>
|
|
2
|
+
// The indicator along the bottom of an Arc-style sidebar: one dot per space,
|
|
3
|
+
// plus an optional trailing "+".
|
|
4
|
+
//
|
|
5
|
+
// Dots rather than one icon per space, which is what Dia does and what this
|
|
6
|
+
// used to get wrong. An icon rail costs ~28px of width each, so it fits about
|
|
7
|
+
// eight spaces in a 280px sidebar and then overflows — and with every space
|
|
8
|
+
// showing the same default folder glyph, thirteen of them read as a broken
|
|
9
|
+
// progress bar rather than as a switcher. A dot is 6px, says only "which of
|
|
10
|
+
// how many", and leaves the identifying work to the space title and the tint,
|
|
11
|
+
// which is where it belongs.
|
|
12
|
+
//
|
|
13
|
+
// The buttons are markup only. `<SidebarSpaces>` owns the single click handler
|
|
14
|
+
// and flips `data-space-current`, so the rail never holds its own copy of
|
|
15
|
+
// which space is active — there is exactly one source of truth.
|
|
16
|
+
export const spaces = ($props.spaces || []).map(space => ({
|
|
17
|
+
id: space.id,
|
|
18
|
+
label: space.label || space.id,
|
|
19
|
+
}))
|
|
20
|
+
|
|
21
|
+
export const active = $props.active || (spaces[0] && spaces[0].id) || ''
|
|
22
|
+
export const showAdd = $props.showAdd ?? false
|
|
23
|
+
export const addLabel = $props.addLabel || 'New space'
|
|
24
|
+
|
|
25
|
+
// A single space has nothing to indicate. Dia hides the row entirely rather
|
|
26
|
+
// than showing one lonely dot, and so does this — unless "+" needs a home.
|
|
27
|
+
export const showDots = spaces.length > 1
|
|
28
|
+
</script>
|
|
29
|
+
|
|
30
|
+
<div
|
|
31
|
+
data-space-switcher
|
|
32
|
+
class="stx-space-rail"
|
|
33
|
+
role="tablist"
|
|
34
|
+
aria-label="Spaces"
|
|
35
|
+
aria-orientation="horizontal"
|
|
36
|
+
>
|
|
37
|
+
<!--
|
|
38
|
+
A spacer of the same width as the trailing button, so the dots are centred
|
|
39
|
+
on the sidebar rather than on the space left over beside "+".
|
|
40
|
+
-->
|
|
41
|
+
<span class="stx-space-rail-gutter" aria-hidden="true"></span>
|
|
42
|
+
|
|
43
|
+
<div class="stx-space-dots">
|
|
44
|
+
@if(showDots)
|
|
45
|
+
@foreach(spaces as space)
|
|
46
|
+
<button
|
|
47
|
+
type="button"
|
|
48
|
+
data-space-target="{{ space.id }}"
|
|
49
|
+
data-space-current="{{ space.id === active ? 'true' : 'false' }}"
|
|
50
|
+
class="stx-space-dot"
|
|
51
|
+
role="tab"
|
|
52
|
+
aria-selected="{{ space.id === active ? 'true' : 'false' }}"
|
|
53
|
+
aria-label="{{ space.label }}"
|
|
54
|
+
title="{{ space.label }}"
|
|
55
|
+
tabindex="{{ space.id === active ? '0' : '-1' }}"
|
|
56
|
+
>
|
|
57
|
+
<!--
|
|
58
|
+
The dot is a child rather than the button itself: the button stays a
|
|
59
|
+
comfortable target while the mark stays small.
|
|
60
|
+
-->
|
|
61
|
+
<span class="stx-space-dot-mark" aria-hidden="true"></span>
|
|
62
|
+
</button>
|
|
63
|
+
@endforeach
|
|
64
|
+
@endif
|
|
65
|
+
</div>
|
|
66
|
+
|
|
67
|
+
<span class="stx-space-rail-gutter">
|
|
68
|
+
@if(showAdd)
|
|
69
|
+
<button
|
|
70
|
+
type="button"
|
|
71
|
+
data-space-add
|
|
72
|
+
class="stx-space-add"
|
|
73
|
+
aria-label="{{ addLabel }}"
|
|
74
|
+
title="{{ addLabel }}"
|
|
75
|
+
>
|
|
76
|
+
<span class="i-f7-plus h-[11px] w-[11px]" aria-hidden="true"></span>
|
|
77
|
+
</button>
|
|
78
|
+
@endif
|
|
79
|
+
</span>
|
|
80
|
+
</div>
|
|
81
|
+
|
|
82
|
+
<style scoped>
|
|
83
|
+
.stx-space-rail {
|
|
84
|
+
display: flex;
|
|
85
|
+
align-items: center;
|
|
86
|
+
flex-shrink: 0;
|
|
87
|
+
height: 34px;
|
|
88
|
+
padding: 0 8px 6px;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
.stx-space-rail-gutter {
|
|
92
|
+
display: grid;
|
|
93
|
+
place-items: center;
|
|
94
|
+
width: 22px;
|
|
95
|
+
flex-shrink: 0;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
.stx-space-dots {
|
|
99
|
+
display: flex;
|
|
100
|
+
flex: 1;
|
|
101
|
+
align-items: center;
|
|
102
|
+
justify-content: center;
|
|
103
|
+
gap: 2px;
|
|
104
|
+
min-width: 0;
|
|
105
|
+
/* Many spaces scroll rather than squeeze: dots that shrink to fit stop
|
|
106
|
+
reading as dots, and a sidebar cannot get wider. The bar is hidden because
|
|
107
|
+
the dots are the affordance. */
|
|
108
|
+
overflow-x: auto;
|
|
109
|
+
scrollbar-width: none;
|
|
110
|
+
}
|
|
111
|
+
.stx-space-dots::-webkit-scrollbar { display: none; }
|
|
112
|
+
|
|
113
|
+
.stx-space-dot {
|
|
114
|
+
display: grid;
|
|
115
|
+
place-items: center;
|
|
116
|
+
width: 14px;
|
|
117
|
+
height: 18px;
|
|
118
|
+
flex-shrink: 0;
|
|
119
|
+
background: transparent;
|
|
120
|
+
border: 0;
|
|
121
|
+
padding: 0;
|
|
122
|
+
cursor: default;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
.stx-space-dot-mark {
|
|
126
|
+
width: 6px;
|
|
127
|
+
height: 6px;
|
|
128
|
+
border-radius: 9999px;
|
|
129
|
+
background: currentColor;
|
|
130
|
+
opacity: 0.25;
|
|
131
|
+
/* One duration and one curve for everything that moves when the space
|
|
132
|
+
changes — see `--stx-space-settle` in SidebarSpaces. Independent timings
|
|
133
|
+
are what made a swipe read as several things happening at once. */
|
|
134
|
+
transition:
|
|
135
|
+
opacity var(--stx-space-settle, 420ms) var(--stx-space-ease, ease),
|
|
136
|
+
background-color var(--stx-space-settle, 420ms) var(--stx-space-ease, ease),
|
|
137
|
+
transform var(--stx-space-settle, 420ms) var(--stx-space-ease, ease);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/* Hover is not part of a space change, so it stays quick. */
|
|
141
|
+
.stx-space-dot:hover .stx-space-dot-mark {
|
|
142
|
+
opacity: 0.5;
|
|
143
|
+
transition-duration: 120ms;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
.stx-space-dot[data-space-current="true"] .stx-space-dot-mark {
|
|
147
|
+
opacity: 1;
|
|
148
|
+
background: var(--stx-space-accent, currentColor);
|
|
149
|
+
/* Barely larger. Enough to find at a glance, not enough to jump. */
|
|
150
|
+
transform: scale(1.15);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
.stx-space-dot:focus-visible {
|
|
154
|
+
outline: 2px solid var(--stx-space-accent, currentColor);
|
|
155
|
+
outline-offset: 2px;
|
|
156
|
+
border-radius: 9999px;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
.stx-space-add {
|
|
160
|
+
display: grid;
|
|
161
|
+
place-items: center;
|
|
162
|
+
width: 20px;
|
|
163
|
+
height: 20px;
|
|
164
|
+
border-radius: 6px;
|
|
165
|
+
color: currentColor;
|
|
166
|
+
opacity: 0.35;
|
|
167
|
+
background: transparent;
|
|
168
|
+
border: 0;
|
|
169
|
+
padding: 0;
|
|
170
|
+
cursor: default;
|
|
171
|
+
transition: opacity 120ms ease, background-color 120ms ease;
|
|
172
|
+
}
|
|
173
|
+
.stx-space-add:hover {
|
|
174
|
+
opacity: 0.8;
|
|
175
|
+
background: rgba(255, 255, 255, 0.4);
|
|
176
|
+
}
|
|
177
|
+
.stx-space-add:focus-visible {
|
|
178
|
+
outline: 2px solid var(--stx-space-accent, currentColor);
|
|
179
|
+
outline-offset: 1px;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
@media (prefers-color-scheme: dark) {
|
|
183
|
+
.stx-space-add:hover { background: rgba(255, 255, 255, 0.1); }
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
@media (prefers-reduced-motion: reduce) {
|
|
187
|
+
.stx-space-dot-mark { transition: none; }
|
|
188
|
+
}
|
|
189
|
+
</style>
|
|
@@ -106,6 +106,20 @@ function tintTarget() {
|
|
|
106
106
|
return root.closest('[data-stx-sidebar]') || root
|
|
107
107
|
}
|
|
108
108
|
|
|
109
|
+
/**
|
|
110
|
+
* Publish the settle timing so CSS and the track animation cannot drift.
|
|
111
|
+
*
|
|
112
|
+
* `SETTLE_MS` drives the track's transform from JS; everything else that moves
|
|
113
|
+
* during a space change reads these properties. Written once at mount rather
|
|
114
|
+
* than duplicated as literals in four stylesheets, which is how they came to
|
|
115
|
+
* disagree — 420ms on one curve, 420ms on another, 300ms, and 150ms.
|
|
116
|
+
*/
|
|
117
|
+
function publishTiming() {
|
|
118
|
+
const style = tintTarget().style
|
|
119
|
+
style.setProperty('--stx-space-settle', `${SETTLE_MS}ms`)
|
|
120
|
+
style.setProperty('--stx-space-ease', SETTLE_EASE)
|
|
121
|
+
}
|
|
122
|
+
|
|
109
123
|
function applyTint(position) {
|
|
110
124
|
const tint = tints[position]
|
|
111
125
|
if (!tint) return
|
|
@@ -452,6 +466,10 @@ onMount(() => {
|
|
|
452
466
|
tints = spaces.map(space => space.tint)
|
|
453
467
|
if (ids.length === 0) return
|
|
454
468
|
|
|
469
|
+
// Before the first paint, so nothing animates on the default timing once and
|
|
470
|
+
// the shared one afterwards.
|
|
471
|
+
publishTiming()
|
|
472
|
+
|
|
455
473
|
const startIndex = clampIndex(config.index || 0, ids.length)
|
|
456
474
|
const saved = restored()
|
|
457
475
|
const initial = saved === -1 ? startIndex : saved
|
|
@@ -514,6 +532,7 @@ defineExpose({
|
|
|
514
532
|
:action="space.action"
|
|
515
533
|
:clear="space.clear"
|
|
516
534
|
:active="space.id === activeId"
|
|
535
|
+
:deferred="space.deferred === true"
|
|
517
536
|
:theme="theme"
|
|
518
537
|
/>
|
|
519
538
|
@endforeach
|
|
@@ -559,11 +578,19 @@ defineExpose({
|
|
|
559
578
|
linear-gradient(180deg, rgba(255, 255, 255, 0.34) 0%, rgba(255, 255, 255, 0) 32%),
|
|
560
579
|
linear-gradient(180deg, var(--stx-space-from) 0%, var(--stx-space-to) 100%);
|
|
561
580
|
color: var(--stx-space-ink);
|
|
581
|
+
/* One duration and one curve for everything a space change moves.
|
|
582
|
+
This used to be four timings and three easings across the track, the
|
|
583
|
+
palette, the switcher and the space's own buttons — 420ms on one curve,
|
|
584
|
+
420ms on another, 300ms, 150ms — so a swipe read as several things
|
|
585
|
+
happening near each other rather than as one motion. Published as custom
|
|
586
|
+
properties so the pieces cannot drift apart again. */
|
|
587
|
+
--stx-space-settle: 420ms;
|
|
588
|
+
--stx-space-ease: cubic-bezier(0.22, 0.61, 0.36, 1);
|
|
562
589
|
transition:
|
|
563
|
-
--stx-space-from
|
|
564
|
-
--stx-space-to
|
|
565
|
-
--stx-space-ink
|
|
566
|
-
--stx-space-accent
|
|
590
|
+
--stx-space-from var(--stx-space-settle) var(--stx-space-ease),
|
|
591
|
+
--stx-space-to var(--stx-space-settle) var(--stx-space-ease),
|
|
592
|
+
--stx-space-ink var(--stx-space-settle) var(--stx-space-ease),
|
|
593
|
+
--stx-space-accent var(--stx-space-settle) var(--stx-space-ease);
|
|
567
594
|
}
|
|
568
595
|
|
|
569
596
|
@media (prefers-color-scheme: dark) {
|
package/dist/index.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
// @bun
|
|
2
|
-
// ../../../../../../../tmp/stx-components-build-
|
|
2
|
+
// ../../../../../../../tmp/stx-components-build-RdlquU/stubs/stub-0.js
|
|
3
3
|
var stub_0_default = { __stx: true, src: "./components/CodeBlock.stx" };
|
|
4
|
-
// ../../../../../../../tmp/stx-components-build-
|
|
4
|
+
// ../../../../../../../tmp/stx-components-build-RdlquU/stubs/stub-1.js
|
|
5
5
|
var stub_1_default = { __stx: true, src: "./components/Footer.stx" };
|
|
6
|
-
// ../../../../../../../tmp/stx-components-build-
|
|
6
|
+
// ../../../../../../../tmp/stx-components-build-RdlquU/stubs/stub-2.js
|
|
7
7
|
var stub_2_default = { __stx: true, src: "./components/Hero.stx" };
|
|
8
|
-
// ../../../../../../../tmp/stx-components-build-
|
|
8
|
+
// ../../../../../../../tmp/stx-components-build-RdlquU/stubs/stub-3.js
|
|
9
9
|
var stub_3_default = { __stx: true, src: "./components/Installation.stx" };
|
|
10
10
|
// src/composables/useCopyCode.ts
|
|
11
11
|
function useCopyCode(options) {
|
|
@@ -1132,7 +1132,7 @@ var RadioGroupOption_default = "./RadioGroupOption-s1d6r221.stx";
|
|
|
1132
1132
|
// src/ui/select/Select.stx
|
|
1133
1133
|
var Select_default = "./Select-1vnd0brz.stx";
|
|
1134
1134
|
// src/ui/sidebar/Sidebar.stx
|
|
1135
|
-
var Sidebar_default = "./Sidebar-
|
|
1135
|
+
var Sidebar_default = "./Sidebar-v7ecx5gs.stx";
|
|
1136
1136
|
// src/ui/sidebar/SidebarFooter.stx
|
|
1137
1137
|
var SidebarFooter_default = "./SidebarFooter-s7skj92y.stx";
|
|
1138
1138
|
// src/ui/sidebar/SidebarHeader.stx
|
|
@@ -1144,11 +1144,11 @@ var SidebarPinned_default = "./SidebarPinned-tvj28bm0.stx";
|
|
|
1144
1144
|
// src/ui/sidebar/SidebarSection.stx
|
|
1145
1145
|
var SidebarSection_default = "./SidebarSection-zd9z35ch.stx";
|
|
1146
1146
|
// src/ui/sidebar/SidebarSpace.stx
|
|
1147
|
-
var SidebarSpace_default = "./SidebarSpace-
|
|
1147
|
+
var SidebarSpace_default = "./SidebarSpace-r062weq2.stx";
|
|
1148
1148
|
// src/ui/sidebar/SidebarSpaces.stx
|
|
1149
|
-
var SidebarSpaces_default = "./SidebarSpaces-
|
|
1149
|
+
var SidebarSpaces_default = "./SidebarSpaces-fgxhn4ac.stx";
|
|
1150
1150
|
// src/ui/sidebar/SidebarSpaceSwitcher.stx
|
|
1151
|
-
var SidebarSpaceSwitcher_default = "./SidebarSpaceSwitcher-
|
|
1151
|
+
var SidebarSpaceSwitcher_default = "./SidebarSpaceSwitcher-az9wqm87.stx";
|
|
1152
1152
|
// src/ui/sidebar/themes.ts
|
|
1153
1153
|
var macosColors = {
|
|
1154
1154
|
blue: "#0088ff",
|
|
@@ -1427,7 +1427,8 @@ function normalizeSpace(space) {
|
|
|
1427
1427
|
action: normalizeAction(space.action, space.id, "action", "i-f7-plus"),
|
|
1428
1428
|
clear: normalizeAction(space.clear, space.id, "clear", "i-f7-arrow-down"),
|
|
1429
1429
|
tint,
|
|
1430
|
-
style: spaceTintVars(tint)
|
|
1430
|
+
style: spaceTintVars(tint),
|
|
1431
|
+
deferred: space.deferred === true
|
|
1431
1432
|
};
|
|
1432
1433
|
}
|
|
1433
1434
|
function normalizeSpaces(spaces) {
|
|
@@ -81,6 +81,7 @@ export declare interface SidebarSpaceData {
|
|
|
81
81
|
icon?: string
|
|
82
82
|
tint?: string | SidebarSpaceTint
|
|
83
83
|
pinned?: SidebarPinnedItemData[]
|
|
84
|
+
deferred?: boolean
|
|
84
85
|
sections?: SidebarSectionData[]
|
|
85
86
|
action?: SidebarSpaceActionData
|
|
86
87
|
clear?: SidebarSpaceActionData
|
|
@@ -102,4 +103,5 @@ export declare interface NormalizedSidebarSpace {
|
|
|
102
103
|
clear: SidebarSpaceActionData | null
|
|
103
104
|
tint: SidebarSpaceTint
|
|
104
105
|
style: string
|
|
106
|
+
deferred: boolean
|
|
105
107
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stacksjs/components",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.2.
|
|
4
|
+
"version": "0.2.167",
|
|
5
5
|
"description": "A modern component library built with STX, featuring headwind utility classes and TypeScript syntax highlighting.",
|
|
6
6
|
"author": "Chris Breuer <chris@stacksjs.org>",
|
|
7
7
|
"license": "MIT",
|
|
@@ -64,8 +64,8 @@
|
|
|
64
64
|
"dependencies": {
|
|
65
65
|
"@iconify-json/f7": "^1.2.2",
|
|
66
66
|
"@cwcss/crosswind": "^0.2.14",
|
|
67
|
-
"@stacksjs/stx": "0.2.
|
|
68
|
-
"bun-plugin-stx": "0.2.
|
|
67
|
+
"@stacksjs/stx": "0.2.167",
|
|
68
|
+
"bun-plugin-stx": "0.2.167",
|
|
69
69
|
"ts-syntax-highlighter": "^0.2.1"
|
|
70
70
|
},
|
|
71
71
|
"devDependencies": {
|
|
@@ -639,10 +639,12 @@ defineExpose({ collapsed, toggle: toggleCollapse, syncRouteSelection })
|
|
|
639
639
|
/* Width is listed here because this rule outranks the pane's
|
|
640
640
|
`transition-[width]` utility class and would otherwise drop it. */
|
|
641
641
|
transition:
|
|
642
|
-
--stx-space-from 420ms ease,
|
|
643
|
-
--stx-space-to 420ms ease,
|
|
644
|
-
--stx-space-ink 420ms ease,
|
|
645
|
-
--stx-space-accent 420ms ease,
|
|
642
|
+
--stx-space-from var(--stx-space-settle, 420ms) var(--stx-space-ease, ease),
|
|
643
|
+
--stx-space-to var(--stx-space-settle, 420ms) var(--stx-space-ease, ease),
|
|
644
|
+
--stx-space-ink var(--stx-space-settle, 420ms) var(--stx-space-ease, ease),
|
|
645
|
+
--stx-space-accent var(--stx-space-settle, 420ms) var(--stx-space-ease, ease),
|
|
646
|
+
/* Collapsing the pane is a different intent from changing space, so it
|
|
647
|
+
keeps its own timing. */
|
|
646
648
|
width 300ms ease-out;
|
|
647
649
|
}
|
|
648
650
|
|
|
@@ -16,6 +16,19 @@ export const clear = $props.clear || null
|
|
|
16
16
|
export const theme = $props.theme || 'arc'
|
|
17
17
|
export const active = $props.active ?? false
|
|
18
18
|
|
|
19
|
+
// Render the shell without its contents.
|
|
20
|
+
//
|
|
21
|
+
// Every space's rows are rendered on the server even though the Arc design
|
|
22
|
+
// shows one at a time, and that cost is linear in the number of spaces: a host
|
|
23
|
+
// with fourteen projects pays for fourteen panels on every page load to display
|
|
24
|
+
// one. `deferred` keeps the panel — the track's geometry, the tab role and the
|
|
25
|
+
// swipe all depend on it existing — and drops what is inside it.
|
|
26
|
+
//
|
|
27
|
+
// The space still announces itself as a tabpanel, so the switcher and keyboard
|
|
28
|
+
// navigation are unchanged; `data-space-deferred` tells the controller this one
|
|
29
|
+
// has nothing yet, and `spaceChange` is where a host fills it or navigates.
|
|
30
|
+
export const deferred = $props.deferred ?? false
|
|
31
|
+
|
|
19
32
|
const themeStyle = resolveSidebarTheme(theme)
|
|
20
33
|
export const scrollAreaClasses = themeStyle.scrollArea
|
|
21
34
|
|
|
@@ -32,7 +45,7 @@ export const actionClasses = [
|
|
|
32
45
|
'flex w-full items-center gap-[8px]',
|
|
33
46
|
'h-[30px] rounded-[8px] px-[6px]',
|
|
34
47
|
'text-[13px] leading-[16px]',
|
|
35
|
-
'opacity-55
|
|
48
|
+
'opacity-55 stx-space-timed',
|
|
36
49
|
'hover:bg-white/45 hover:opacity-100 dark:hover:bg-white/8',
|
|
37
50
|
'cursor-default',
|
|
38
51
|
].join(' ')
|
|
@@ -40,7 +53,7 @@ export const actionClasses = [
|
|
|
40
53
|
export const clearClasses = [
|
|
41
54
|
'flex items-center gap-[4px] rounded-[6px] px-[5px] py-[2px]',
|
|
42
55
|
'text-[11px] leading-[14px]',
|
|
43
|
-
'opacity-45
|
|
56
|
+
'opacity-45 stx-space-timed',
|
|
44
57
|
'hover:opacity-90',
|
|
45
58
|
'cursor-default',
|
|
46
59
|
].join(' ')
|
|
@@ -64,6 +77,7 @@ function onAction(event) {
|
|
|
64
77
|
data-space
|
|
65
78
|
data-space-id="{{ id }}"
|
|
66
79
|
data-space-active="{{ active ? 'true' : 'false' }}"
|
|
80
|
+
@if(deferred)data-space-deferred@endif
|
|
67
81
|
@if(!active)inert@endif
|
|
68
82
|
class="flex min-w-0 flex-1 flex-col"
|
|
69
83
|
role="tabpanel"
|
|
@@ -71,7 +85,9 @@ function onAction(event) {
|
|
|
71
85
|
@click="onAction($event)"
|
|
72
86
|
>
|
|
73
87
|
<div class="{{ scrollAreaClasses }} stx-sidebar-scroll">
|
|
74
|
-
|
|
88
|
+
@if(!deferred)
|
|
89
|
+
<SidebarPinned :items="pinned" />
|
|
90
|
+
@endif
|
|
75
91
|
|
|
76
92
|
@if(label)
|
|
77
93
|
<div class="{{ titleClasses }}">
|
|
@@ -82,6 +98,12 @@ function onAction(event) {
|
|
|
82
98
|
</div>
|
|
83
99
|
@endif
|
|
84
100
|
|
|
101
|
+
<!--
|
|
102
|
+
Rows, and the actions that belong with them, are what a deferred panel
|
|
103
|
+
drops. The title above stays: it is a single line, and it is what tells
|
|
104
|
+
you which space you are swiping into.
|
|
105
|
+
-->
|
|
106
|
+
@if(!deferred)
|
|
85
107
|
@foreach(sections as section)
|
|
86
108
|
<SidebarSection
|
|
87
109
|
:id="section.id"
|
|
@@ -123,10 +145,22 @@ function onAction(event) {
|
|
|
123
145
|
<span class="truncate">{{ action.label }}</span>
|
|
124
146
|
</button>
|
|
125
147
|
@endif
|
|
148
|
+
@endif
|
|
126
149
|
</div>
|
|
127
150
|
</section>
|
|
128
151
|
|
|
129
152
|
<style scoped>
|
|
153
|
+
/* Anything that changes with the space shares one duration and one curve, so a
|
|
154
|
+
swipe is a single motion. See `--stx-space-settle` in SidebarSpaces. */
|
|
155
|
+
.stx-space-timed {
|
|
156
|
+
transition:
|
|
157
|
+
background-color var(--stx-space-settle, 420ms) var(--stx-space-ease, ease),
|
|
158
|
+
opacity var(--stx-space-settle, 420ms) var(--stx-space-ease, ease),
|
|
159
|
+
color var(--stx-space-settle, 420ms) var(--stx-space-ease, ease);
|
|
160
|
+
}
|
|
161
|
+
/* Hover is a separate intent and stays quick. */
|
|
162
|
+
.stx-space-timed:hover { transition-duration: 120ms; }
|
|
163
|
+
|
|
130
164
|
/* The title icon rides the space accent. Published as a custom property by
|
|
131
165
|
SidebarSpaces so it crossfades along with the panel instead of cutting. */
|
|
132
166
|
.stx-space-title-icon {
|
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
<script server>
|
|
2
|
-
// The
|
|
3
|
-
// an optional trailing "+".
|
|
4
|
-
//
|
|
5
|
-
//
|
|
2
|
+
// The indicator along the bottom of an Arc-style sidebar: one dot per space,
|
|
3
|
+
// plus an optional trailing "+".
|
|
4
|
+
//
|
|
5
|
+
// Dots rather than one icon per space, which is what Dia does and what this
|
|
6
|
+
// used to get wrong. An icon rail costs ~28px of width each, so it fits about
|
|
7
|
+
// eight spaces in a 280px sidebar and then overflows — and with every space
|
|
8
|
+
// showing the same default folder glyph, thirteen of them read as a broken
|
|
9
|
+
// progress bar rather than as a switcher. A dot is 6px, says only "which of
|
|
10
|
+
// how many", and leaves the identifying work to the space title and the tint,
|
|
11
|
+
// which is where it belongs.
|
|
6
12
|
//
|
|
7
13
|
// The buttons are markup only. `<SidebarSpaces>` owns the single click handler
|
|
8
14
|
// and flips `data-space-current`, so the rail never holds its own copy of
|
|
@@ -10,89 +16,174 @@
|
|
|
10
16
|
export const spaces = ($props.spaces || []).map(space => ({
|
|
11
17
|
id: space.id,
|
|
12
18
|
label: space.label || space.id,
|
|
13
|
-
icon: space.icon || 'i-f7-circle',
|
|
14
19
|
}))
|
|
15
20
|
|
|
16
21
|
export const active = $props.active || (spaces[0] && spaces[0].id) || ''
|
|
17
22
|
export const showAdd = $props.showAdd ?? false
|
|
18
23
|
export const addLabel = $props.addLabel || 'New space'
|
|
24
|
+
|
|
25
|
+
// A single space has nothing to indicate. Dia hides the row entirely rather
|
|
26
|
+
// than showing one lonely dot, and so does this — unless "+" needs a home.
|
|
27
|
+
export const showDots = spaces.length > 1
|
|
19
28
|
</script>
|
|
20
29
|
|
|
21
30
|
<div
|
|
22
31
|
data-space-switcher
|
|
23
|
-
class="
|
|
32
|
+
class="stx-space-rail"
|
|
24
33
|
role="tablist"
|
|
25
34
|
aria-label="Spaces"
|
|
26
35
|
aria-orientation="horizontal"
|
|
27
36
|
>
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
37
|
+
<!--
|
|
38
|
+
A spacer of the same width as the trailing button, so the dots are centred
|
|
39
|
+
on the sidebar rather than on the space left over beside "+".
|
|
40
|
+
-->
|
|
41
|
+
<span class="stx-space-rail-gutter" aria-hidden="true"></span>
|
|
42
|
+
|
|
43
|
+
<div class="stx-space-dots">
|
|
44
|
+
@if(showDots)
|
|
45
|
+
@foreach(spaces as space)
|
|
46
|
+
<button
|
|
47
|
+
type="button"
|
|
48
|
+
data-space-target="{{ space.id }}"
|
|
49
|
+
data-space-current="{{ space.id === active ? 'true' : 'false' }}"
|
|
50
|
+
class="stx-space-dot"
|
|
51
|
+
role="tab"
|
|
52
|
+
aria-selected="{{ space.id === active ? 'true' : 'false' }}"
|
|
53
|
+
aria-label="{{ space.label }}"
|
|
54
|
+
title="{{ space.label }}"
|
|
55
|
+
tabindex="{{ space.id === active ? '0' : '-1' }}"
|
|
56
|
+
>
|
|
57
|
+
<!--
|
|
58
|
+
The dot is a child rather than the button itself: the button stays a
|
|
59
|
+
comfortable target while the mark stays small.
|
|
60
|
+
-->
|
|
61
|
+
<span class="stx-space-dot-mark" aria-hidden="true"></span>
|
|
62
|
+
</button>
|
|
63
|
+
@endforeach
|
|
64
|
+
@endif
|
|
65
|
+
</div>
|
|
66
|
+
|
|
67
|
+
<span class="stx-space-rail-gutter">
|
|
68
|
+
@if(showAdd)
|
|
69
|
+
<button
|
|
70
|
+
type="button"
|
|
71
|
+
data-space-add
|
|
72
|
+
class="stx-space-add"
|
|
73
|
+
aria-label="{{ addLabel }}"
|
|
74
|
+
title="{{ addLabel }}"
|
|
75
|
+
>
|
|
76
|
+
<span class="i-f7-plus h-[11px] w-[11px]" aria-hidden="true"></span>
|
|
77
|
+
</button>
|
|
78
|
+
@endif
|
|
79
|
+
</span>
|
|
57
80
|
</div>
|
|
58
81
|
|
|
59
82
|
<style scoped>
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
83
|
+
.stx-space-rail {
|
|
84
|
+
display: flex;
|
|
85
|
+
align-items: center;
|
|
86
|
+
flex-shrink: 0;
|
|
87
|
+
height: 34px;
|
|
88
|
+
padding: 0 8px 6px;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
.stx-space-rail-gutter {
|
|
65
92
|
display: grid;
|
|
66
93
|
place-items: center;
|
|
67
|
-
width:
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
94
|
+
width: 22px;
|
|
95
|
+
flex-shrink: 0;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
.stx-space-dots {
|
|
99
|
+
display: flex;
|
|
100
|
+
flex: 1;
|
|
101
|
+
align-items: center;
|
|
102
|
+
justify-content: center;
|
|
103
|
+
gap: 2px;
|
|
104
|
+
min-width: 0;
|
|
105
|
+
/* Many spaces scroll rather than squeeze: dots that shrink to fit stop
|
|
106
|
+
reading as dots, and a sidebar cannot get wider. The bar is hidden because
|
|
107
|
+
the dots are the affordance. */
|
|
108
|
+
overflow-x: auto;
|
|
109
|
+
scrollbar-width: none;
|
|
110
|
+
}
|
|
111
|
+
.stx-space-dots::-webkit-scrollbar { display: none; }
|
|
112
|
+
|
|
113
|
+
.stx-space-dot {
|
|
114
|
+
display: grid;
|
|
115
|
+
place-items: center;
|
|
116
|
+
width: 14px;
|
|
117
|
+
height: 18px;
|
|
118
|
+
flex-shrink: 0;
|
|
72
119
|
background: transparent;
|
|
73
|
-
|
|
120
|
+
border: 0;
|
|
121
|
+
padding: 0;
|
|
74
122
|
cursor: default;
|
|
75
123
|
}
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
124
|
+
|
|
125
|
+
.stx-space-dot-mark {
|
|
126
|
+
width: 6px;
|
|
127
|
+
height: 6px;
|
|
128
|
+
border-radius: 9999px;
|
|
129
|
+
background: currentColor;
|
|
130
|
+
opacity: 0.25;
|
|
131
|
+
/* One duration and one curve for everything that moves when the space
|
|
132
|
+
changes — see `--stx-space-settle` in SidebarSpaces. Independent timings
|
|
133
|
+
are what made a swipe read as several things happening at once. */
|
|
134
|
+
transition:
|
|
135
|
+
opacity var(--stx-space-settle, 420ms) var(--stx-space-ease, ease),
|
|
136
|
+
background-color var(--stx-space-settle, 420ms) var(--stx-space-ease, ease),
|
|
137
|
+
transform var(--stx-space-settle, 420ms) var(--stx-space-ease, ease);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/* Hover is not part of a space change, so it stays quick. */
|
|
141
|
+
.stx-space-dot:hover .stx-space-dot-mark {
|
|
142
|
+
opacity: 0.5;
|
|
143
|
+
transition-duration: 120ms;
|
|
79
144
|
}
|
|
80
|
-
|
|
145
|
+
|
|
146
|
+
.stx-space-dot[data-space-current="true"] .stx-space-dot-mark {
|
|
81
147
|
opacity: 1;
|
|
82
|
-
|
|
83
|
-
|
|
148
|
+
background: var(--stx-space-accent, currentColor);
|
|
149
|
+
/* Barely larger. Enough to find at a glance, not enough to jump. */
|
|
150
|
+
transform: scale(1.15);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
.stx-space-dot:focus-visible {
|
|
154
|
+
outline: 2px solid var(--stx-space-accent, currentColor);
|
|
155
|
+
outline-offset: 2px;
|
|
156
|
+
border-radius: 9999px;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
.stx-space-add {
|
|
160
|
+
display: grid;
|
|
161
|
+
place-items: center;
|
|
162
|
+
width: 20px;
|
|
163
|
+
height: 20px;
|
|
164
|
+
border-radius: 6px;
|
|
165
|
+
color: currentColor;
|
|
166
|
+
opacity: 0.35;
|
|
167
|
+
background: transparent;
|
|
168
|
+
border: 0;
|
|
169
|
+
padding: 0;
|
|
170
|
+
cursor: default;
|
|
171
|
+
transition: opacity 120ms ease, background-color 120ms ease;
|
|
84
172
|
}
|
|
85
|
-
.stx-space-
|
|
173
|
+
.stx-space-add:hover {
|
|
174
|
+
opacity: 0.8;
|
|
175
|
+
background: rgba(255, 255, 255, 0.4);
|
|
176
|
+
}
|
|
177
|
+
.stx-space-add:focus-visible {
|
|
86
178
|
outline: 2px solid var(--stx-space-accent, currentColor);
|
|
87
179
|
outline-offset: 1px;
|
|
88
180
|
}
|
|
89
181
|
|
|
90
182
|
@media (prefers-color-scheme: dark) {
|
|
91
|
-
.stx-space-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
}
|
|
183
|
+
.stx-space-add:hover { background: rgba(255, 255, 255, 0.1); }
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
@media (prefers-reduced-motion: reduce) {
|
|
187
|
+
.stx-space-dot-mark { transition: none; }
|
|
97
188
|
}
|
|
98
189
|
</style>
|
|
@@ -106,6 +106,20 @@ function tintTarget() {
|
|
|
106
106
|
return root.closest('[data-stx-sidebar]') || root
|
|
107
107
|
}
|
|
108
108
|
|
|
109
|
+
/**
|
|
110
|
+
* Publish the settle timing so CSS and the track animation cannot drift.
|
|
111
|
+
*
|
|
112
|
+
* `SETTLE_MS` drives the track's transform from JS; everything else that moves
|
|
113
|
+
* during a space change reads these properties. Written once at mount rather
|
|
114
|
+
* than duplicated as literals in four stylesheets, which is how they came to
|
|
115
|
+
* disagree — 420ms on one curve, 420ms on another, 300ms, and 150ms.
|
|
116
|
+
*/
|
|
117
|
+
function publishTiming() {
|
|
118
|
+
const style = tintTarget().style
|
|
119
|
+
style.setProperty('--stx-space-settle', `${SETTLE_MS}ms`)
|
|
120
|
+
style.setProperty('--stx-space-ease', SETTLE_EASE)
|
|
121
|
+
}
|
|
122
|
+
|
|
109
123
|
function applyTint(position) {
|
|
110
124
|
const tint = tints[position]
|
|
111
125
|
if (!tint) return
|
|
@@ -452,6 +466,10 @@ onMount(() => {
|
|
|
452
466
|
tints = spaces.map(space => space.tint)
|
|
453
467
|
if (ids.length === 0) return
|
|
454
468
|
|
|
469
|
+
// Before the first paint, so nothing animates on the default timing once and
|
|
470
|
+
// the shared one afterwards.
|
|
471
|
+
publishTiming()
|
|
472
|
+
|
|
455
473
|
const startIndex = clampIndex(config.index || 0, ids.length)
|
|
456
474
|
const saved = restored()
|
|
457
475
|
const initial = saved === -1 ? startIndex : saved
|
|
@@ -514,6 +532,7 @@ defineExpose({
|
|
|
514
532
|
:action="space.action"
|
|
515
533
|
:clear="space.clear"
|
|
516
534
|
:active="space.id === activeId"
|
|
535
|
+
:deferred="space.deferred === true"
|
|
517
536
|
:theme="theme"
|
|
518
537
|
/>
|
|
519
538
|
@endforeach
|
|
@@ -559,11 +578,19 @@ defineExpose({
|
|
|
559
578
|
linear-gradient(180deg, rgba(255, 255, 255, 0.34) 0%, rgba(255, 255, 255, 0) 32%),
|
|
560
579
|
linear-gradient(180deg, var(--stx-space-from) 0%, var(--stx-space-to) 100%);
|
|
561
580
|
color: var(--stx-space-ink);
|
|
581
|
+
/* One duration and one curve for everything a space change moves.
|
|
582
|
+
This used to be four timings and three easings across the track, the
|
|
583
|
+
palette, the switcher and the space's own buttons — 420ms on one curve,
|
|
584
|
+
420ms on another, 300ms, 150ms — so a swipe read as several things
|
|
585
|
+
happening near each other rather than as one motion. Published as custom
|
|
586
|
+
properties so the pieces cannot drift apart again. */
|
|
587
|
+
--stx-space-settle: 420ms;
|
|
588
|
+
--stx-space-ease: cubic-bezier(0.22, 0.61, 0.36, 1);
|
|
562
589
|
transition:
|
|
563
|
-
--stx-space-from
|
|
564
|
-
--stx-space-to
|
|
565
|
-
--stx-space-ink
|
|
566
|
-
--stx-space-accent
|
|
590
|
+
--stx-space-from var(--stx-space-settle) var(--stx-space-ease),
|
|
591
|
+
--stx-space-to var(--stx-space-settle) var(--stx-space-ease),
|
|
592
|
+
--stx-space-ink var(--stx-space-settle) var(--stx-space-ease),
|
|
593
|
+
--stx-space-accent var(--stx-space-settle) var(--stx-space-ease);
|
|
567
594
|
}
|
|
568
595
|
|
|
569
596
|
@media (prefers-color-scheme: dark) {
|
package/src/ui/sidebar/spaces.ts
CHANGED
|
@@ -94,6 +94,20 @@ export interface SidebarSpaceData {
|
|
|
94
94
|
tint?: string | SidebarSpaceTint
|
|
95
95
|
/** Favorites grid pinned above the rows. */
|
|
96
96
|
pinned?: SidebarPinnedItemData[]
|
|
97
|
+
/**
|
|
98
|
+
* Render this space's shell without its rows.
|
|
99
|
+
*
|
|
100
|
+
* Every space is server-rendered even though the Arc design shows one at a
|
|
101
|
+
* time, so the cost is linear in the number of spaces: fourteen projects
|
|
102
|
+
* means paying for fourteen panels on every page load to display one. A
|
|
103
|
+
* deferred space keeps its panel — the track geometry, the tab role and the
|
|
104
|
+
* swipe all depend on it existing — and its title, which is what identifies
|
|
105
|
+
* it mid-swipe, but drops the rows.
|
|
106
|
+
*
|
|
107
|
+
* The panel carries `data-space-deferred`, and `spaceChange` fires as usual,
|
|
108
|
+
* so a host can fill it in or navigate when the user arrives.
|
|
109
|
+
*/
|
|
110
|
+
deferred?: boolean
|
|
97
111
|
/** Row groups, identical in shape to a plain `<Sidebar>`'s sections. */
|
|
98
112
|
sections?: SidebarSectionData[]
|
|
99
113
|
/** Primary action row at the foot of the list. */
|
|
@@ -122,6 +136,8 @@ export interface NormalizedSidebarSpace {
|
|
|
122
136
|
tint: SidebarSpaceTint
|
|
123
137
|
/** `style` attribute value that publishes this space's palette. */
|
|
124
138
|
style: string
|
|
139
|
+
/** See {@link SidebarSpaceData.deferred}. */
|
|
140
|
+
deferred: boolean
|
|
125
141
|
}
|
|
126
142
|
|
|
127
143
|
/**
|
|
@@ -228,6 +244,7 @@ export function normalizeSpace(space: SidebarSpaceData): NormalizedSidebarSpace
|
|
|
228
244
|
clear: normalizeAction(space.clear, space.id, 'clear', 'i-f7-arrow-down'),
|
|
229
245
|
tint,
|
|
230
246
|
style: spaceTintVars(tint),
|
|
247
|
+
deferred: space.deferred === true,
|
|
231
248
|
}
|
|
232
249
|
}
|
|
233
250
|
|
|
@@ -1,98 +0,0 @@
|
|
|
1
|
-
<script server>
|
|
2
|
-
// The rail along the bottom of an Arc sidebar: one small icon per space, plus
|
|
3
|
-
// an optional trailing "+". It is the discoverable counterpart to the swipe —
|
|
4
|
-
// the gesture is what you use once you know the spaces exist, the rail is how
|
|
5
|
-
// you find out.
|
|
6
|
-
//
|
|
7
|
-
// The buttons are markup only. `<SidebarSpaces>` owns the single click handler
|
|
8
|
-
// and flips `data-space-current`, so the rail never holds its own copy of
|
|
9
|
-
// which space is active — there is exactly one source of truth.
|
|
10
|
-
export const spaces = ($props.spaces || []).map(space => ({
|
|
11
|
-
id: space.id,
|
|
12
|
-
label: space.label || space.id,
|
|
13
|
-
icon: space.icon || 'i-f7-circle',
|
|
14
|
-
}))
|
|
15
|
-
|
|
16
|
-
export const active = $props.active || (spaces[0] && spaces[0].id) || ''
|
|
17
|
-
export const showAdd = $props.showAdd ?? false
|
|
18
|
-
export const addLabel = $props.addLabel || 'New space'
|
|
19
|
-
</script>
|
|
20
|
-
|
|
21
|
-
<div
|
|
22
|
-
data-space-switcher
|
|
23
|
-
class="flex h-[38px] shrink-0 items-center gap-[1px] px-[8px] pb-[6px]"
|
|
24
|
-
role="tablist"
|
|
25
|
-
aria-label="Spaces"
|
|
26
|
-
aria-orientation="horizontal"
|
|
27
|
-
>
|
|
28
|
-
@foreach(spaces as space)
|
|
29
|
-
<button
|
|
30
|
-
type="button"
|
|
31
|
-
data-space-target="{{ space.id }}"
|
|
32
|
-
data-space-current="{{ space.id === active ? 'true' : 'false' }}"
|
|
33
|
-
class="stx-space-tab"
|
|
34
|
-
role="tab"
|
|
35
|
-
aria-selected="{{ space.id === active ? 'true' : 'false' }}"
|
|
36
|
-
aria-label="{{ space.label }}"
|
|
37
|
-
title="{{ space.label }}"
|
|
38
|
-
tabindex="{{ space.id === active ? '0' : '-1' }}"
|
|
39
|
-
>
|
|
40
|
-
<span class="{{ space.icon }} h-[15px] w-[15px]" aria-hidden="true"></span>
|
|
41
|
-
</button>
|
|
42
|
-
@endforeach
|
|
43
|
-
|
|
44
|
-
<span class="flex-1"></span>
|
|
45
|
-
|
|
46
|
-
@if(showAdd)
|
|
47
|
-
<button
|
|
48
|
-
type="button"
|
|
49
|
-
data-space-add
|
|
50
|
-
class="stx-space-tab"
|
|
51
|
-
aria-label="{{ addLabel }}"
|
|
52
|
-
title="{{ addLabel }}"
|
|
53
|
-
>
|
|
54
|
-
<span class="i-f7-plus h-[14px] w-[14px]" aria-hidden="true"></span>
|
|
55
|
-
</button>
|
|
56
|
-
@endif
|
|
57
|
-
</div>
|
|
58
|
-
|
|
59
|
-
<style scoped>
|
|
60
|
-
/* Rail buttons are painted here rather than with utility classes because the
|
|
61
|
-
current one has to pick up `--stx-space-accent`, which changes as you swipe.
|
|
62
|
-
A class swap would have to be re-applied on every transition; an attribute
|
|
63
|
-
rule keyed on `data-space-current` just follows along. */
|
|
64
|
-
.stx-space-tab {
|
|
65
|
-
display: grid;
|
|
66
|
-
place-items: center;
|
|
67
|
-
width: 28px;
|
|
68
|
-
height: 26px;
|
|
69
|
-
border-radius: 7px;
|
|
70
|
-
color: currentColor;
|
|
71
|
-
opacity: 0.4;
|
|
72
|
-
background: transparent;
|
|
73
|
-
transition: opacity 150ms ease, background-color 150ms ease, color 300ms ease;
|
|
74
|
-
cursor: default;
|
|
75
|
-
}
|
|
76
|
-
.stx-space-tab:hover {
|
|
77
|
-
opacity: 0.75;
|
|
78
|
-
background: rgba(255, 255, 255, 0.4);
|
|
79
|
-
}
|
|
80
|
-
.stx-space-tab[data-space-current="true"] {
|
|
81
|
-
opacity: 1;
|
|
82
|
-
color: var(--stx-space-accent, currentColor);
|
|
83
|
-
background: rgba(255, 255, 255, 0.55);
|
|
84
|
-
}
|
|
85
|
-
.stx-space-tab:focus-visible {
|
|
86
|
-
outline: 2px solid var(--stx-space-accent, currentColor);
|
|
87
|
-
outline-offset: 1px;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
@media (prefers-color-scheme: dark) {
|
|
91
|
-
.stx-space-tab:hover {
|
|
92
|
-
background: rgba(255, 255, 255, 0.08);
|
|
93
|
-
}
|
|
94
|
-
.stx-space-tab[data-space-current="true"] {
|
|
95
|
-
background: rgba(255, 255, 255, 0.12);
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
</style>
|