@stacksjs/defaults 0.75.97 → 0.75.99

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.
@@ -1,40 +1,167 @@
1
1
  <script client>
2
- import { haptics } from '@stacksjs/mobile'
2
+ import type { SheetDetent } from '@stacksjs/mobile'
3
+ import { coverTabBar, liftToBody, lockBackground, observeSheetDrag, presentPageAsCard, trapFocus } from '@stacksjs/mobile'
3
4
 
4
5
  /**
5
6
  * A bottom sheet: a task that belongs to this screen, raised over it.
6
7
  *
7
- * Bind it with `v-model:open`. It opens with a light haptic tap, closes from
8
- * its backdrop, its Done button or Escape, and keeps its content clear of the
9
- * home indicator.
8
+ * Bind it with `v-model:open`. It closes from its backdrop, its Done button,
9
+ * Escape, or a drag down from its grabber or header (a flick, or past half
10
+ * way; a shorter drag springs back). While it is up the page behind it is
11
+ * inert and still, and focus stays inside it.
12
+ *
13
+ * `detents="medium large"` makes it a full-height sheet that rests at half
14
+ * the screen or all of it and is dragged between them, the first named being
15
+ * where it opens. At large the page behind shrinks back into a card, as iOS
16
+ * presents a page sheet. Without `detents` it is as tall as its content.
17
+ *
18
+ * As on iOS it covers the tab bar: in the app the native bar steps aside while
19
+ * it is up. It belongs to the screen it was opened on, so leaving that screen
20
+ * (a link from outside, a tab switch) closes it rather than carrying it over
21
+ * the next one.
10
22
  */
11
23
  const emit = defineEmits()
12
24
  const open = useModel('open', { default: false })
13
25
  const title = useReactiveProp<string>('title', '')
14
26
  const doneLabel = useReactiveProp<string>('doneLabel', 'Done')
27
+ const detentsProp = useReactiveProp<SheetDetent[] | string>('detents', '')
28
+
29
+ const detents = derived<SheetDetent[]>(() => {
30
+ const raw = detentsProp()
31
+ const list = Array.isArray(raw) ? raw : String(raw || '').split(/[\s,]+/)
32
+ const valid = list.filter((value): value is SheetDetent => value === 'medium' || value === 'large')
33
+ return valid.length ? valid : ['large']
34
+ })
35
+ const hasDetents = derived(() => Boolean(detentsProp()) && detentsProp().length > 0)
36
+ const detent = state<SheetDetent>('large')
37
+
38
+ const sheetRoot = useRef('nativeSheet')
39
+ const panel = useRef('nativeSheetPanel')
40
+ const grabber = useRef('nativeSheetGrabber')
41
+ const header = useRef('nativeSheetHeader')
42
+
43
+ let releaseBackground: (() => void) | null = null
44
+ let putBack: (() => void) | null = null
45
+ let putBackTimer: ReturnType<typeof setTimeout> | null = null
46
+ let previousFocus: HTMLElement | null = null
47
+ let carded = false
48
+ let stopDrag: (() => void) | null = null
49
+ let openedOn = ''
50
+
51
+ function currentPath(): string {
52
+ return root()?.ownerDocument.defaultView?.location.pathname ?? ''
53
+ }
54
+
55
+ function root(): HTMLElement | null {
56
+ return sheetRoot.current as HTMLElement | null
57
+ }
58
+
59
+ function present(): void {
60
+ const element = root()
61
+ if (!element || releaseBackground) return
62
+ if (putBackTimer) clearTimeout(putBackTimer)
63
+ putBackTimer = null
64
+ detent.set(detents()[0] ?? 'large')
65
+ previousFocus = element.ownerDocument.activeElement as HTMLElement | null
66
+ // Out of the page's stacking contexts and transforms (the card below
67
+ // scales the page), and everything else made inert behind it.
68
+ putBack ??= liftToBody(element)
69
+ releaseBackground = lockBackground(element)
70
+ openedOn = currentPath()
71
+ coverTabBar(element, true)
72
+ void nextTick(() => (panel.current as HTMLElement | null)?.focus())
73
+ }
74
+
75
+ function dismissed(): void {
76
+ const element = root()
77
+ if (element) coverTabBar(element, false)
78
+ releaseBackground?.()
79
+ releaseBackground = null
80
+ previousFocus?.focus?.()
81
+ previousFocus = null
82
+ // Back where it was written once it has slid away.
83
+ putBackTimer = setTimeout(() => {
84
+ putBackTimer = null
85
+ putBack?.()
86
+ putBack = null
87
+ }, 420)
88
+ }
15
89
 
16
90
  let wasOpen = false
17
91
  effect(() => {
18
92
  const isOpen = open()
19
- if (isOpen && !wasOpen) void haptics.impact('light')
93
+ if (isOpen && !wasOpen) present()
94
+ else if (!isOpen && wasOpen) dismissed()
20
95
  wasOpen = isOpen
21
96
  })
22
97
 
98
+ // The page behind becomes a card while a full-height sheet is at large. Only
99
+ // this sheet's own change is applied, so a closed sheet never undoes an open
100
+ // one's card.
101
+ effect(() => {
102
+ const element = root()
103
+ const want = open() && hasDetents() && detent() === 'large'
104
+ if (!element || want === carded) return
105
+ carded = want
106
+ presentPageAsCard(element.ownerDocument, want)
107
+ })
108
+
23
109
  function close(): void {
24
110
  open.set(false)
25
111
  emit('close')
26
112
  }
27
113
 
114
+ onMount(() => {
115
+ const sheetPanel = panel.current as HTMLElement | null
116
+ if (!sheetPanel) return
117
+ const handles = [grabber.current, header.current].filter(Boolean) as HTMLElement[]
118
+ stopDrag = observeSheetDrag({
119
+ panel: sheetPanel,
120
+ handles,
121
+ detents: () => detents(),
122
+ detent: () => detent(),
123
+ onSettle: (to) => {
124
+ if (to === 'dismiss') close()
125
+ else detent.set(to)
126
+ },
127
+ })
128
+ if (open()) present()
129
+ })
130
+
131
+ onDestroy(() => {
132
+ stopDrag?.()
133
+ releaseBackground?.()
134
+ const element = root()
135
+ if (element && open()) coverTabBar(element, false)
136
+ if (carded && element) presentPageAsCard(element.ownerDocument, false)
137
+ if (putBackTimer) clearTimeout(putBackTimer)
138
+ putBack?.()
139
+ })
140
+
141
+ // The router moved to another screen while this one's sheet was up.
142
+ useEventListener('stx:navigate', (event: CustomEvent<{ url?: string }>) => {
143
+ if (!open()) return
144
+ const location = root()?.ownerDocument.defaultView?.location
145
+ const to = event.detail?.url ? new URL(event.detail.url, location?.href).pathname : currentPath()
146
+ if (to !== openedOn) close()
147
+ })
148
+
28
149
  useEventListener('keydown', (event: KeyboardEvent) => {
29
- if (event.key === 'Escape' && open()) close()
150
+ if (!open()) return
151
+ if (event.key === 'Escape') close()
152
+ else if (panel.current) trapFocus(panel.current as HTMLElement, event)
30
153
  })
154
+
155
+ const sheetClass = derived(() => `${open() ? 'is-open' : ''}${hasDetents() ? ' has-detents' : ''}${detent() === 'medium' ? ' is-medium' : ''}`.trim())
31
156
  </script>
32
157
 
33
- <div class="native-sheet" :class="open() ? 'is-open' : ''" :aria-hidden="open() ? 'false' : 'true'">
158
+ <div ref="nativeSheet" class="native-sheet" :class="sheetClass()" :aria-hidden="open() ? 'false' : 'true'" data-native-sheet>
34
159
  <div class="native-sheet-backdrop" @click="close()"></div>
35
- <section class="native-sheet-panel" role="dialog" aria-modal="true" :aria-label="title()">
36
- <span aria-hidden="true" class="native-sheet-grabber"></span>
37
- <header class="native-sheet-header">
160
+ <section ref="nativeSheetPanel" class="native-sheet-panel" role="dialog" aria-modal="true" :aria-label="title()" tabindex="-1">
161
+ <div ref="nativeSheetGrabber" class="native-sheet-grabber-area">
162
+ <span aria-hidden="true" class="native-sheet-grabber"></span>
163
+ </div>
164
+ <header ref="nativeSheetHeader" class="native-sheet-header">
38
165
  <h2 class="native-sheet-title">{{ title() }}</h2>
39
166
  <button type="button" class="native-sheet-done" @click="close()">{{ doneLabel() }}</button>
40
167
  </header>
@@ -51,6 +178,8 @@ useEventListener('keydown', (event: KeyboardEvent) => {
51
178
  z-index: 70;
52
179
  visibility: hidden;
53
180
  pointer-events: none;
181
+ font-size: var(--native-font-size, 17px);
182
+ -webkit-tap-highlight-color: transparent;
54
183
  }
55
184
 
56
185
  .native-sheet.is-open { visibility: visible; pointer-events: auto; }
@@ -63,7 +192,8 @@ useEventListener('keydown', (event: KeyboardEvent) => {
63
192
  transition: opacity 240ms ease;
64
193
  }
65
194
 
66
- .native-sheet.is-open .native-sheet-backdrop { opacity: 1; }
195
+ /* Dragged, the dimming follows the panel (--native-sheet-presence, 1 to 0). */
196
+ .native-sheet.is-open .native-sheet-backdrop { opacity: var(--native-sheet-presence, 1); }
67
197
 
68
198
  .native-sheet-panel {
69
199
  position: absolute;
@@ -75,20 +205,41 @@ useEventListener('keydown', (event: KeyboardEvent) => {
75
205
  max-height: calc(100dvh - env(safe-area-inset-top, 0px) - 1rem);
76
206
  padding-bottom: env(safe-area-inset-bottom, 0px);
77
207
  border-radius: 1.25rem 1.25rem 0 0;
208
+ outline: none;
78
209
  background: var(--native-sheet-background, rgb(248 250 252));
79
210
  box-shadow: 0 -12px 40px rgb(2 6 23 / 0.18);
80
211
  transform: translateY(105%);
81
- transition: transform 320ms cubic-bezier(0.32, 0.72, 0, 1);
212
+ transition: transform 380ms cubic-bezier(0.32, 0.72, 0, 1);
213
+ touch-action: pan-y;
82
214
  }
83
215
 
84
216
  .dark .native-sheet-panel { background: var(--native-sheet-background-dark, rgb(15 23 42)); }
85
217
  .native-sheet.is-open .native-sheet-panel { transform: none; }
86
218
 
219
+ /* Following the finger: no easing between where it was and where it is. */
220
+ .native-sheet-panel.is-dragging { transition: none; }
221
+
222
+ /* A sheet with detents is full height, and rests with half the screen's
223
+ worth showing at medium. */
224
+ .native-sheet.has-detents .native-sheet-panel {
225
+ height: calc(100dvh - env(safe-area-inset-top, 0px) - 0.625rem);
226
+ max-height: none;
227
+ }
228
+
229
+ .native-sheet.is-open.is-medium .native-sheet-panel { transform: translateY(calc(100% - 50dvh)); }
230
+
231
+ /* The grabber's area is a handle, generous for a thumb. */
232
+ .native-sheet-grabber-area {
233
+ display: flex;
234
+ justify-content: center;
235
+ padding: 0.4rem 0 0.15rem;
236
+ touch-action: none;
237
+ cursor: grab;
238
+ }
239
+
87
240
  .native-sheet-grabber {
88
- align-self: center;
89
241
  width: 2.25rem;
90
242
  height: 0.3rem;
91
- margin-top: 0.4rem;
92
243
  border-radius: 999px;
93
244
  background: rgb(148 163 184 / 0.55);
94
245
  }
@@ -99,17 +250,19 @@ useEventListener('keydown', (event: KeyboardEvent) => {
99
250
  align-items: center;
100
251
  justify-content: space-between;
101
252
  padding: 0.5rem 1rem 0.25rem;
253
+ touch-action: none;
102
254
  }
103
255
 
104
- .native-sheet-title { font-size: 1.0625rem; font-weight: 600; }
256
+ .native-sheet-title { font-size: 1em; font-weight: 600; }
105
257
 
106
258
  .native-sheet-done {
107
259
  min-height: 2.75rem;
108
260
  border: 0;
109
261
  color: var(--native-accent, rgb(37 99 235));
110
262
  background: transparent;
111
- font-size: 1.0625rem;
263
+ font-size: 1em;
112
264
  font-weight: 600;
265
+ touch-action: manipulation;
113
266
  }
114
267
 
115
268
  .dark .native-sheet-done { color: var(--native-accent-dark, rgb(96 165 250)); }
@@ -120,10 +273,33 @@ useEventListener('keydown', (event: KeyboardEvent) => {
120
273
  overscroll-behavior: contain;
121
274
  }
122
275
 
276
+ .native-sheet.has-detents .native-sheet-body { flex: 1; }
277
+
278
+ /* The page behind a full-height sheet at large: pressed back into a card
279
+ with rounded corners, centred on what was in view, on black. The
280
+ content scales rather than the shell, whose fixed tab bar a transform
281
+ would unpin. */
282
+ .native-app-content {
283
+ transition: transform 380ms cubic-bezier(0.32, 0.72, 0, 1), border-radius 380ms cubic-bezier(0.32, 0.72, 0, 1);
284
+ }
285
+
286
+ :root[data-native-sheet-card] body { background: #000; }
287
+
288
+ :root[data-native-sheet-card] .native-app-content {
289
+ overflow: hidden;
290
+ border-radius: 0.75rem;
291
+ transform: scale(0.93);
292
+ transform-origin: 50% var(--native-sheet-card-origin, 50vh);
293
+ }
294
+
123
295
  @media (prefers-reduced-motion: reduce) {
124
- .native-sheet-backdrop, .native-sheet-panel { transition: none; }
296
+ .native-sheet-backdrop, .native-sheet-panel, .native-app-content { transition: none; }
297
+ :root[data-native-sheet-card] .native-app-content { transform: none; border-radius: 0; }
125
298
  }
126
299
 
300
+ :root[data-native-reduce-motion] .native-sheet-panel { transition: none; }
301
+ :root[data-native-reduce-motion][data-native-sheet-card] .native-app-content { transform: none; border-radius: 0; }
302
+
127
303
  /* The router names the page's <main> for view transitions, which makes it a
128
304
  stacking context: a sheet inside it stacked under the tab bar whatever its
129
305
  z-index. While a sheet is open <main> gives the name up. Nothing navigates
@@ -0,0 +1,121 @@
1
+ <script server>
2
+ interface NativeSymbolProps {
3
+ /** The SF Symbol, e.g. `chevron.backward`. */
4
+ name: string
5
+ /** The Iconify class drawn where there are no SF Symbols (a browser, an older shell). */
6
+ icon?: string
7
+ /** The point size; the surrounding text's size when left out. */
8
+ size?: number
9
+ weight?: 'ultralight' | 'thin' | 'light' | 'regular' | 'medium' | 'semibold' | 'bold' | 'heavy' | 'black'
10
+ /** What the symbol says, for a symbol that stands alone. Decorative when left out. */
11
+ label?: string
12
+ class?: string
13
+ }
14
+
15
+ // The caller's classes are rendered here too, so the symbol has its size
16
+ // from the first paint rather than once the page has run.
17
+ const { name = '', icon = '', label = '', class: extraClass = '' } = defineProps<NativeSymbolProps>()
18
+
19
+ // The common symbols' Iconify stand-ins, written out here so the stylesheet
20
+ // generator sees every class. Anything else names its own with `icon`.
21
+ const standIns: Record<string, string> = {
22
+ 'chevron.backward': 'i-lucide-chevron-left',
23
+ 'chevron.left': 'i-lucide-chevron-left',
24
+ 'chevron.forward': 'i-lucide-chevron-right',
25
+ 'chevron.right': 'i-lucide-chevron-right',
26
+ 'chevron.down': 'i-lucide-chevron-down',
27
+ 'chevron.up': 'i-lucide-chevron-up',
28
+ 'xmark': 'i-lucide-x',
29
+ 'plus': 'i-lucide-plus',
30
+ 'minus': 'i-lucide-minus',
31
+ 'checkmark': 'i-lucide-check',
32
+ 'ellipsis': 'i-lucide-ellipsis',
33
+ 'ellipsis.circle': 'i-lucide-circle-ellipsis',
34
+ 'square.and.arrow.up': 'i-lucide-share',
35
+ 'trash': 'i-lucide-trash-2',
36
+ 'pencil': 'i-lucide-pencil',
37
+ 'doc.on.doc': 'i-lucide-copy',
38
+ 'link': 'i-lucide-link',
39
+ 'magnifyingglass': 'i-lucide-search',
40
+ 'gearshape': 'i-lucide-settings',
41
+ 'person.crop.circle': 'i-lucide-circle-user',
42
+ 'bell': 'i-lucide-bell',
43
+ 'heart': 'i-lucide-heart',
44
+ 'star': 'i-lucide-star',
45
+ 'bookmark': 'i-lucide-bookmark',
46
+ 'calendar': 'i-lucide-calendar',
47
+ 'house': 'i-lucide-house',
48
+ 'info.circle': 'i-lucide-info',
49
+ 'arrow.clockwise': 'i-lucide-rotate-cw',
50
+ 'play.fill': 'i-lucide-play',
51
+ 'pause.fill': 'i-lucide-pause',
52
+ }
53
+ const standIn = icon || standIns[name] || 'i-lucide-circle'
54
+ const classes = ['native-symbol', standIn, extraClass].filter(Boolean).join(' ')
55
+ </script>
56
+
57
+ <script client>
58
+ import type { SymbolOptions } from '@stacksjs/mobile'
59
+ import { symbols } from '@stacksjs/mobile'
60
+
61
+ /**
62
+ * An SF Symbol, drawn by the phone (`craft.symbols.image`) and painted in the
63
+ * text colour like any icon, so it follows the theme and the tint. Until the
64
+ * shell has drawn it, and wherever it cannot, the Iconify stand-in shows.
65
+ * Drawn symbols are kept in memory, so the next screen's bar has its chevron
66
+ * from the first frame.
67
+ */
68
+ const symbolName = useReactiveProp<string>('name', '')
69
+ const size = useReactiveProp<number>('size', 0)
70
+ const weight = useReactiveProp<SymbolOptions['weight']>('weight', 'regular')
71
+ const className = useReactiveProp<string>('class', '')
72
+ const image = state<string | null>(null)
73
+
74
+ effect(() => {
75
+ const name = symbolName()
76
+ if (!name) return
77
+ const options: SymbolOptions = { pointSize: Number(size()) || 17, weight: weight() }
78
+ const known = symbols.cached(name, options)
79
+ if (known !== undefined) {
80
+ image.set(known)
81
+ return
82
+ }
83
+ void symbols.image(name, options).then((url) => {
84
+ if (symbolName() === name) image.set(url)
85
+ })
86
+ })
87
+
88
+ // One class binding: the drawn symbol and the caller's classes together.
89
+ const symbolClass = derived(() => `${image() ? 'is-symbol ' : ''}${className()}`.trim())
90
+ const symbolStyle = derived(() => {
91
+ const px = Number(size()) || 0
92
+ return `${image() ? `--native-symbol:url("${image()}");` : ''}${px ? `font-size:${px}px;` : ''}`
93
+ })
94
+ </script>
95
+
96
+ @if(label)
97
+ <i class="{{ classes }}" :class="symbolClass()" :style="symbolStyle()" role="img" aria-label="{{ label }}" data-native-symbol="{{ name }}"></i>
98
+ @else
99
+ <i class="{{ classes }}" :class="symbolClass()" :style="symbolStyle()" aria-hidden="true" data-native-symbol="{{ name }}"></i>
100
+ @endif
101
+
102
+ <style>
103
+ /* stx's box around the component steps aside, so the symbol sits in the
104
+ line (or the flex row) it was written in. */
105
+ [data-stx-scope]:has(> .native-symbol) { display: contents; }
106
+
107
+ .native-symbol {
108
+ display: inline-block;
109
+ flex-shrink: 0;
110
+ width: 1em;
111
+ height: 1em;
112
+ vertical-align: -0.125em;
113
+ }
114
+
115
+ /* The drawn symbol as a mask over the text colour, in place of the stand-in. */
116
+ .native-symbol.is-symbol {
117
+ background-color: currentColor;
118
+ -webkit-mask: var(--native-symbol) center / contain no-repeat;
119
+ mask: var(--native-symbol) center / contain no-repeat;
120
+ }
121
+ </style>
@@ -7,24 +7,36 @@ const { label = 'App navigation' } = defineProps<NativeTabBarProps>()
7
7
  </script>
8
8
 
9
9
  <script client>
10
- import { haptics, mirrorTabBar } from '@stacksjs/mobile'
10
+ import { handleTabReselect, haptics, mirrorTabBar } from '@stacksjs/mobile'
11
11
 
12
12
  // Inside Craft's iOS shell the phone draws this bar natively (Liquid Glass on
13
13
  // iOS 26 and later) and these links stay as its model: the router keeps the
14
14
  // current one marked, and a native tap comes back as a click on one of them.
15
+ //
16
+ // The tabs are the stx router's (`data-stx-nav="tab"` on each link): each
17
+ // keeps its own screens and history, a switch adds no back entry, and
18
+ // choosing the current tab again scrolls it to the top, then pops it to its
19
+ // root, announcing `stx:tabreselect`. A native tap reaches the router's
20
+ // selectTab(). handleTabReselect covers a router that predates tabs.
15
21
  const bar = useRef('nativeTabBar')
16
22
  let mirrored = false
17
23
  let stopMirror: (() => void) | null = null
24
+ let stopReselect: (() => void) | null = null
18
25
  onMount(() => {
19
26
  const nav = bar.current as HTMLElement | null
20
27
  if (!nav) return
21
28
  stopMirror = mirrorTabBar(nav)
22
29
  mirrored = stopMirror !== null
30
+ stopReselect = handleTabReselect(nav)
31
+ })
32
+ onDestroy(() => {
33
+ stopMirror?.()
34
+ stopReselect?.()
23
35
  })
24
- onDestroy(() => stopMirror?.())
25
36
 
26
37
  function selectTab(): void {
27
- // The native bar has already played its own.
38
+ // Only the page's own bar: the phone's native bar plays its own haptic,
39
+ // and a tap on it comes back without one.
28
40
  if (!mirrored) void haptics.selection()
29
41
  }
30
42
  </script>
@@ -32,7 +32,10 @@ const hasBadge = badge !== '' && badge !== 0 && badge !== '0'
32
32
  swapped without the router's cross-fade, which iOS never draws between
33
33
  tabs (it read as a quarter-second lag on every tap). `data-native-tab`
34
34
  and `data-native-tab-symbol` describe the tab to the phone's native bar
35
- (@stacksjs/mobile mirrorTabBar). -->
35
+ (@stacksjs/mobile mirrorTabBar). `data-stx-nav="tab"`: the stx router
36
+ treats it as a tab, with its own retained screens and history, no back
37
+ entry for a switch, and re-selection scrolling to the top, then popping
38
+ to the tab's root. -->
36
39
  <StxLink
37
40
  to="{{ href }}"
38
41
  aria-label="{{ label }}"
@@ -42,6 +45,7 @@ const hasBadge = badge !== '' && badge !== 0 && badge !== '0'
42
45
  draggable="false"
43
46
  data-stx-prefetch="eager"
44
47
  data-stx-transition="none"
48
+ data-stx-nav="tab"
45
49
  data-native-tab="{{ label }}"
46
50
  data-native-tab-symbol="{{ symbol }}"
47
51
  >
@@ -12,7 +12,9 @@ export { default as DashboardNavbar } from './Dashboard/Navbar.stx'
12
12
  export * from './Dashboard'
13
13
  export { default as Audio } from './Audio.stx'
14
14
  export { default as Image } from './Image.stx'
15
+ export { default as NativeActionSheet } from './NativeActionSheet.stx'
15
16
  export { default as NativeAppShell } from './NativeAppShell.stx'
17
+ export { default as NativeContextMenu } from './NativeContextMenu.stx'
16
18
  export { default as NativeHealthButton } from './NativeHealthButton.stx'
17
19
  export { default as NativeNavBar } from './NativeNavBar.stx'
18
20
  export { default as NativeNetworkBanner } from './NativeNetworkBanner.stx'
@@ -22,6 +24,7 @@ export { default as NativePullToRefresh } from './NativePullToRefresh.stx'
22
24
  export { default as NativeSegmentedControl } from './NativeSegmentedControl.stx'
23
25
  export { default as NativeShareButton } from './NativeShareButton.stx'
24
26
  export { default as NativeSheet } from './NativeSheet.stx'
27
+ export { default as NativeSymbol } from './NativeSymbol.stx'
25
28
  export { default as NativeTabBar } from './NativeTabBar.stx'
26
29
  export { default as NativeTabItem } from './NativeTabItem.stx'
27
30
  export { default as Video } from './Video.stx'