@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.
@@ -0,0 +1,134 @@
1
+ <script server>
2
+ import { resolveSidebarTheme } from './themes'
3
+
4
+ // One scene of a swipeable sidebar: a pinned grid, a title, the rows, and the
5
+ // space's own actions. Everything below the title is the ordinary Sidebar
6
+ // machinery — `<SidebarSection>` flattens the item tree and `<SidebarItem>`
7
+ // renders the rows — so a space inherits disclosure, counts, icon tints,
8
+ // route selection and keyboard navigation for free.
9
+ export const id = $props.id || ''
10
+ export const label = $props.label || ''
11
+ export const icon = $props.icon || ''
12
+ export const pinned = $props.pinned || []
13
+ export const sections = $props.sections || []
14
+ export const action = $props.action || null
15
+ export const clear = $props.clear || null
16
+ export const theme = $props.theme || 'arc'
17
+ export const active = $props.active ?? false
18
+
19
+ const themeStyle = resolveSidebarTheme(theme)
20
+ export const scrollAreaClasses = themeStyle.scrollArea
21
+
22
+ // The title takes the space's accent, which is how Arc signals which space you
23
+ // are looking at even before the panel color registers.
24
+ export const titleClasses = [
25
+ 'flex items-center gap-[8px]',
26
+ 'px-[8px] pb-[4px] pt-[10px]',
27
+ 'text-[13px] font-semibold leading-[16px]',
28
+ ].join(' ')
29
+
30
+ // Actions read as quieter than rows: no card on hover, just a wash.
31
+ export const actionClasses = [
32
+ 'flex w-full items-center gap-[8px]',
33
+ 'h-[30px] rounded-[8px] px-[6px]',
34
+ 'text-[13px] leading-[16px]',
35
+ 'opacity-55 transition-[background-color,opacity] duration-150 ease-out',
36
+ 'hover:bg-white/45 hover:opacity-100 dark:hover:bg-white/8',
37
+ 'cursor-default',
38
+ ].join(' ')
39
+
40
+ export const clearClasses = [
41
+ 'flex items-center gap-[4px] rounded-[6px] px-[5px] py-[2px]',
42
+ 'text-[11px] leading-[14px]',
43
+ 'opacity-45 transition-opacity duration-150',
44
+ 'hover:opacity-90',
45
+ 'cursor-default',
46
+ ].join(' ')
47
+ </script>
48
+
49
+ <script client>
50
+ const emit = defineEmits()
51
+
52
+ function onAction(event) {
53
+ const button = event.target.closest('[data-space-action]')
54
+ if (!button) return
55
+ emit('spaceAction', {
56
+ spaceId: button.dataset.spaceOwner,
57
+ actionId: button.dataset.spaceAction,
58
+ role: button.dataset.spaceActionRole,
59
+ })
60
+ }
61
+ </script>
62
+
63
+ <section
64
+ data-space
65
+ data-space-id="{{ id }}"
66
+ data-space-active="{{ active ? 'true' : 'false' }}"
67
+ class="flex min-w-0 flex-1 flex-col"
68
+ role="tabpanel"
69
+ aria-label="{{ label || id }}"
70
+ @click="onAction($event)"
71
+ >
72
+ <div class="{{ scrollAreaClasses }} stx-sidebar-scroll">
73
+ <SidebarPinned :items="pinned" />
74
+
75
+ @if(label)
76
+ <div class="{{ titleClasses }}">
77
+ @if(icon)
78
+ <span class="{{ icon }} h-[15px] w-[15px] shrink-0 stx-space-title-icon" aria-hidden="true"></span>
79
+ @endif
80
+ <span class="truncate">{{ label }}</span>
81
+ </div>
82
+ @endif
83
+
84
+ @foreach(sections as section)
85
+ <SidebarSection
86
+ :id="section.id"
87
+ :label="section.label"
88
+ :items="section.items"
89
+ :collapsible="section.collapsible"
90
+ :collapsed="section.collapsed"
91
+ :theme="theme"
92
+ />
93
+ @endforeach
94
+
95
+ @if(clear)
96
+ <div class="flex items-center gap-[8px] px-[8px] pt-[10px]">
97
+ <span class="h-px flex-1 bg-current opacity-15" aria-hidden="true"></span>
98
+ <button
99
+ type="button"
100
+ data-space-action="{{ clear.id }}"
101
+ data-space-action-role="clear"
102
+ data-space-owner="{{ id }}"
103
+ class="{{ clearClasses }}"
104
+ >
105
+ @if(clear.icon)
106
+ <span class="{{ clear.icon }} h-[10px] w-[10px]" aria-hidden="true"></span>
107
+ @endif
108
+ <span>{{ clear.label }}</span>
109
+ </button>
110
+ </div>
111
+ @endif
112
+
113
+ @if(action)
114
+ <button
115
+ type="button"
116
+ data-space-action="{{ action.id }}"
117
+ data-space-action-role="action"
118
+ data-space-owner="{{ id }}"
119
+ class="{{ actionClasses }}"
120
+ >
121
+ <span class="{{ action.icon }} h-[14px] w-[14px] shrink-0" aria-hidden="true"></span>
122
+ <span class="truncate">{{ action.label }}</span>
123
+ </button>
124
+ @endif
125
+ </div>
126
+ </section>
127
+
128
+ <style scoped>
129
+ /* The title icon rides the space accent. Published as a custom property by
130
+ SidebarSpaces so it crossfades along with the panel instead of cutting. */
131
+ .stx-space-title-icon {
132
+ color: var(--stx-space-accent, currentColor);
133
+ }
134
+ </style>
@@ -0,0 +1,98 @@
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>