@1001-digital/components 1.2.3 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@1001-digital/components",
3
- "version": "1.2.3",
3
+ "version": "1.4.0",
4
4
  "type": "module",
5
5
  "sideEffects": [
6
6
  "*.css"
@@ -14,7 +14,7 @@
14
14
  ],
15
15
  "peerDependencies": {
16
16
  "vue": "^3.5.0",
17
- "@1001-digital/styles": "^1.0.1"
17
+ "@1001-digital/styles": "^1.2.0"
18
18
  },
19
19
  "dependencies": {
20
20
  "@iconify/vue": "^5.0.0",
@@ -0,0 +1,60 @@
1
+ <template>
2
+ <div class="app-shell">
3
+ <Sidebar
4
+ v-if="$slots.sidebar"
5
+ v-model:open="sidebarOpen"
6
+ :side="sidebarSide"
7
+ :swipeable="sidebarSwipeable"
8
+ >
9
+ <slot name="sidebar" />
10
+ </Sidebar>
11
+
12
+ <main class="app-shell-main">
13
+ <slot />
14
+ </main>
15
+
16
+ <BottomNav v-if="$slots['bottom-nav']">
17
+ <slot name="bottom-nav" />
18
+ </BottomNav>
19
+ </div>
20
+ </template>
21
+
22
+ <script setup lang="ts">
23
+ import Sidebar from './Sidebar.vue'
24
+ import BottomNav from './BottomNav.vue'
25
+
26
+ withDefaults(
27
+ defineProps<{
28
+ sidebarSide?: 'left' | 'right'
29
+ sidebarSwipeable?: boolean
30
+ }>(),
31
+ {
32
+ sidebarSide: 'left',
33
+ sidebarSwipeable: true,
34
+ },
35
+ )
36
+
37
+ const sidebarOpen = defineModel<boolean>('sidebarOpen', { default: false })
38
+ </script>
39
+
40
+ <style>
41
+ @layer components {
42
+ .app-shell {
43
+ display: flex;
44
+ min-block-size: var(--100vh);
45
+ }
46
+
47
+ .app-shell-main {
48
+ flex: 1;
49
+ min-inline-size: 0;
50
+ }
51
+
52
+ .app-shell:has(.bottom-nav) .app-shell-main {
53
+ padding-block-end: var(--bottom-nav-height);
54
+
55
+ @media (min-width: 1024px) {
56
+ padding-block-end: 0;
57
+ }
58
+ }
59
+ }
60
+ </style>
@@ -0,0 +1,44 @@
1
+ <template>
2
+ <nav class="bottom-nav">
3
+ <slot />
4
+ </nav>
5
+ </template>
6
+
7
+ <style>
8
+ @layer components {
9
+ .bottom-nav {
10
+ position: fixed;
11
+ inset-inline: 0;
12
+ inset-block-end: 0;
13
+ z-index: var(--bottom-nav-z-index);
14
+ block-size: var(--bottom-nav-height);
15
+ background: var(--bottom-nav-background);
16
+ backdrop-filter: var(--blur);
17
+ border-block-start: var(--border);
18
+ display: flex;
19
+ align-items: center;
20
+ justify-content: space-evenly;
21
+ padding-block-end: env(safe-area-inset-bottom);
22
+
23
+ @media (min-width: 1024px) {
24
+ display: none;
25
+ }
26
+
27
+ > a,
28
+ > button {
29
+ display: flex;
30
+ flex-direction: column;
31
+ align-items: center;
32
+ justify-content: center;
33
+ gap: var(--size-1);
34
+ color: var(--muted);
35
+ transition: color var(--speed);
36
+ padding: var(--spacer-sm);
37
+
38
+ &:is(:hover, :focus-visible, .router-link-active) {
39
+ color: var(--color);
40
+ }
41
+ }
42
+ }
43
+ }
44
+ </style>
@@ -0,0 +1,282 @@
1
+ <template>
2
+ <aside
3
+ ref="el"
4
+ class="sidebar"
5
+ :class="[
6
+ `sidebar-${side}`,
7
+ {
8
+ 'is-open': open,
9
+ 'is-swiping': swiping.active,
10
+ },
11
+ ]"
12
+ :style="sidebarStyle"
13
+ >
14
+ <slot />
15
+ </aside>
16
+ <div
17
+ class="sidebar-overlay"
18
+ :class="{
19
+ 'is-visible': open && !isLargeScreen,
20
+ 'is-swiping': swiping.active,
21
+ }"
22
+ :style="overlayStyle"
23
+ @click="open = false"
24
+ />
25
+ </template>
26
+
27
+ <script setup lang="ts">
28
+ import { ref, reactive, computed, watch, watchEffect, onMounted, onBeforeUnmount } from 'vue'
29
+ import { useMediaQuery, useScrollLock, useElementSize } from '@vueuse/core'
30
+
31
+ const open = defineModel<boolean>('open', { default: false })
32
+
33
+ const props = withDefaults(
34
+ defineProps<{
35
+ side?: 'left' | 'right'
36
+ swipeable?: boolean
37
+ }>(),
38
+ {
39
+ side: 'left',
40
+ swipeable: true,
41
+ },
42
+ )
43
+
44
+ const el = ref<HTMLElement>()
45
+ const isLargeScreen = useMediaQuery('(min-width: 1024px)')
46
+ const { width: sidebarWidth } = useElementSize(el)
47
+
48
+ const swiping = reactive({
49
+ active: false,
50
+ translateX: 0,
51
+ })
52
+
53
+ // Scroll lock on mobile when open
54
+ if (typeof document !== 'undefined') {
55
+ const scrollLocked = useScrollLock(document.body)
56
+ watchEffect(() => {
57
+ scrollLocked.value = (open.value || swiping.active) && !isLargeScreen.value
58
+ })
59
+ }
60
+
61
+ // Auto open/close on breakpoint change
62
+ watch(isLargeScreen, (lg) => {
63
+ open.value = lg
64
+ })
65
+
66
+ // Swipe gesture handling
67
+ const EDGE_WIDTH = 30
68
+ const SNAP_THRESHOLD = 0.3
69
+
70
+ let cleanupSwipe: (() => void) | undefined
71
+
72
+ onMounted(() => {
73
+ if (!props.swipeable) return
74
+
75
+ let startX = 0
76
+ let startY = 0
77
+ let tracking = false
78
+ let directionLocked = false
79
+ let isHorizontal = false
80
+
81
+ const onTouchStart = (e: TouchEvent) => {
82
+ if (isLargeScreen.value) return
83
+
84
+ const touch = e.touches[0]
85
+ startX = touch.clientX
86
+ startY = touch.clientY
87
+ tracking = false
88
+ directionLocked = false
89
+ isHorizontal = false
90
+
91
+ if (props.side === 'left') {
92
+ if (!open.value && startX > EDGE_WIDTH) return
93
+ if (open.value && startX > sidebarWidth.value) return
94
+ } else {
95
+ if (!open.value && startX < window.innerWidth - EDGE_WIDTH) return
96
+ if (open.value && startX < window.innerWidth - sidebarWidth.value) return
97
+ }
98
+
99
+ tracking = true
100
+ }
101
+
102
+ const onTouchMove = (e: TouchEvent) => {
103
+ if (!tracking || isLargeScreen.value) return
104
+
105
+ const touch = e.touches[0]
106
+ const dx = touch.clientX - startX
107
+ const dy = touch.clientY - startY
108
+
109
+ if (!directionLocked) {
110
+ if (Math.abs(dx) < 5 && Math.abs(dy) < 5) return
111
+ directionLocked = true
112
+ isHorizontal = Math.abs(dx) > Math.abs(dy)
113
+
114
+ if (!isHorizontal) {
115
+ tracking = false
116
+ return
117
+ }
118
+ }
119
+
120
+ const w = sidebarWidth.value
121
+ if (!w) return
122
+
123
+ swiping.active = true
124
+
125
+ if (props.side === 'left') {
126
+ if (open.value) {
127
+ swiping.translateX = Math.max(-w, Math.min(0, dx))
128
+ } else {
129
+ swiping.translateX = Math.max(-w, Math.min(0, -w + dx))
130
+ }
131
+ } else {
132
+ if (open.value) {
133
+ swiping.translateX = Math.min(w, Math.max(0, dx))
134
+ } else {
135
+ swiping.translateX = Math.min(w, Math.max(0, w + dx))
136
+ }
137
+ }
138
+ }
139
+
140
+ const onTouchEnd = () => {
141
+ if (!swiping.active) {
142
+ tracking = false
143
+ return
144
+ }
145
+
146
+ const w = sidebarWidth.value
147
+ const threshold = w * SNAP_THRESHOLD
148
+
149
+ if (props.side === 'left') {
150
+ open.value = swiping.translateX > -w + threshold
151
+ } else {
152
+ open.value = swiping.translateX < threshold
153
+ }
154
+
155
+ swiping.active = false
156
+ swiping.translateX = 0
157
+ tracking = false
158
+ }
159
+
160
+ document.addEventListener('touchstart', onTouchStart, { passive: true })
161
+ document.addEventListener('touchmove', onTouchMove, { passive: true })
162
+ document.addEventListener('touchend', onTouchEnd)
163
+
164
+ cleanupSwipe = () => {
165
+ document.removeEventListener('touchstart', onTouchStart)
166
+ document.removeEventListener('touchmove', onTouchMove)
167
+ document.removeEventListener('touchend', onTouchEnd)
168
+ }
169
+ })
170
+
171
+ onBeforeUnmount(() => {
172
+ cleanupSwipe?.()
173
+ })
174
+
175
+ const sidebarStyle = computed(() => {
176
+ if (isLargeScreen.value || !swiping.active) return {}
177
+
178
+ return {
179
+ transform: `translateX(${swiping.translateX}px)`,
180
+ }
181
+ })
182
+
183
+ const overlayStyle = computed(() => {
184
+ if (isLargeScreen.value || !swiping.active || !sidebarWidth.value) return {}
185
+
186
+ const progress = 1 - Math.abs(swiping.translateX) / sidebarWidth.value
187
+
188
+ return {
189
+ opacity: String(progress * 0.6),
190
+ pointerEvents: progress > 0 ? ('all' as const) : ('none' as const),
191
+ }
192
+ })
193
+
194
+ defineExpose({
195
+ open: () => {
196
+ open.value = true
197
+ },
198
+ close: () => {
199
+ open.value = false
200
+ },
201
+ toggle: () => {
202
+ open.value = !open.value
203
+ },
204
+ })
205
+ </script>
206
+
207
+ <style>
208
+ @layer components {
209
+ .sidebar {
210
+ position: fixed;
211
+ inset-block: 0;
212
+ z-index: var(--sidebar-z-index);
213
+ inline-size: var(--sidebar-width);
214
+ background-color: var(--sidebar-background);
215
+ display: flex;
216
+ flex-direction: column;
217
+ overflow-y: auto;
218
+ overscroll-behavior: contain;
219
+ transition: transform var(--speed);
220
+
221
+ &.sidebar-left {
222
+ inset-inline-start: 0;
223
+ border-inline-end: var(--sidebar-border);
224
+ transform: translateX(-100%);
225
+
226
+ &.is-open {
227
+ transform: translateX(0);
228
+ }
229
+ }
230
+
231
+ &.sidebar-right {
232
+ inset-inline-end: 0;
233
+ border-inline-start: var(--sidebar-border);
234
+ transform: translateX(100%);
235
+
236
+ &.is-open {
237
+ transform: translateX(0);
238
+ }
239
+ }
240
+
241
+ &.is-swiping {
242
+ transition: none;
243
+ }
244
+
245
+ @media (min-width: 1024px) {
246
+ position: sticky;
247
+ inset-block-start: 0;
248
+ block-size: var(--100vh);
249
+ z-index: auto;
250
+ flex-shrink: 0;
251
+
252
+ &.sidebar-left,
253
+ &.sidebar-right {
254
+ transform: none;
255
+ }
256
+ }
257
+ }
258
+
259
+ .sidebar-overlay {
260
+ position: fixed;
261
+ inset: 0;
262
+ z-index: calc(var(--sidebar-z-index) - 1);
263
+ background-color: var(--sidebar-overlay-background);
264
+ opacity: 0;
265
+ pointer-events: none;
266
+ transition: opacity var(--speed);
267
+
268
+ &.is-visible {
269
+ opacity: 0.6;
270
+ pointer-events: all;
271
+ }
272
+
273
+ &.is-swiping {
274
+ transition: none;
275
+ }
276
+
277
+ @media (min-width: 1024px) {
278
+ display: none;
279
+ }
280
+ }
281
+ }
282
+ </style>
@@ -66,10 +66,7 @@
66
66
  </section>
67
67
  </ToastRoot>
68
68
 
69
- <ToastViewport
70
- class="toast-viewport"
71
- :class="[position]"
72
- />
69
+ <ToastViewport class="toast-viewport" />
73
70
  </ToastProvider>
74
71
 
75
72
  </template>
@@ -94,12 +91,10 @@ withDefaults(
94
91
  defineProps<{
95
92
  duration?: number
96
93
  swipeDirection?: 'right' | 'left' | 'up' | 'down'
97
- position?: 'bottom-right' | 'bottom-left' | 'top-right' | 'top-left'
98
94
  }>(),
99
95
  {
100
96
  duration: 5_000,
101
97
  swipeDirection: 'right',
102
- position: 'bottom-right',
103
98
  },
104
99
  )
105
100
 
@@ -114,6 +109,7 @@ const onClose = (id: string) => {
114
109
  @layer components {
115
110
  :deep(.toast-viewport) {
116
111
  position: fixed;
112
+ inset: var(--toast-inset);
117
113
  z-index: var(--z-index-toast);
118
114
  display: flex;
119
115
  flex-direction: column;
@@ -123,26 +119,6 @@ const onClose = (id: string) => {
123
119
  list-style: none;
124
120
  max-width: 100vw;
125
121
  outline: none;
126
-
127
- &.bottom-right {
128
- bottom: 0;
129
- right: 0;
130
- }
131
-
132
- &.bottom-left {
133
- bottom: 0;
134
- left: 0;
135
- }
136
-
137
- &.top-right {
138
- top: 0;
139
- right: 0;
140
- }
141
-
142
- &.top-left {
143
- top: 0;
144
- left: 0;
145
- }
146
122
  }
147
123
 
148
124
  :deep(.toast) {
package/src/index.ts CHANGED
@@ -1,6 +1,8 @@
1
1
  // Components
2
2
  export { default as Actions } from './base/components/Actions.vue'
3
3
  export { default as Alert } from './base/components/Alert.vue'
4
+ export { default as AppShell } from './base/components/AppShell.vue'
5
+ export { default as BottomNav } from './base/components/BottomNav.vue'
4
6
  export { default as Button } from './base/components/Button.vue'
5
7
  export { default as Calendar } from './base/components/Calendar.vue'
6
8
  export { default as Card } from './base/components/Card.vue'
@@ -34,6 +36,7 @@ export { default as Loading } from './base/components/Loading.vue'
34
36
  export { default as Opepicon } from './base/components/Opepicon.vue'
35
37
  export { default as Popover } from './base/components/Popover.vue'
36
38
  export { default as Progress } from './base/components/Progress.vue'
39
+ export { default as Sidebar } from './base/components/Sidebar.vue'
37
40
  export { default as Tag } from './base/components/Tag.vue'
38
41
  export { default as Tags } from './base/components/Tags.vue'
39
42
  export { default as Toasts } from './base/components/Toasts.vue'