@stacksjs/components 0.2.216 → 0.2.218
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/FilterChip-h896vrxm.stx +50 -0
- package/dist/FilterChips-7pnzycfd.stx +41 -0
- package/dist/ListRow-v45ymhcn.stx +98 -0
- package/dist/NavBar-wcb6f2d3.stx +63 -0
- package/dist/SectionCard-r73hvvbc.stx +42 -0
- package/dist/TabBar-8143650x.stx +53 -0
- package/dist/TabBarItem-hd2r2ayy.stx +55 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +12 -12
- package/dist/ui/mobile/index.d.ts +64 -0
- package/package.json +3 -3
- package/src/index.ts +2 -0
- package/src/ui/mobile/FilterChip.stx +50 -0
- package/src/ui/mobile/FilterChips.stx +41 -0
- package/src/ui/mobile/ListRow.stx +98 -0
- package/src/ui/mobile/NavBar.stx +63 -0
- package/src/ui/mobile/SectionCard.stx +42 -0
- package/src/ui/mobile/TabBar.stx +53 -0
- package/src/ui/mobile/TabBarItem.stx +55 -0
- package/src/ui/mobile/index.ts +80 -0
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
<script server>
|
|
2
|
+
/** One pill. See `FilterChips` for why a menu chip is not a toggle chip. */
|
|
3
|
+
export const label = $props.label || ''
|
|
4
|
+
export const href = $props.href || '#'
|
|
5
|
+
export const active = $props.active || false
|
|
6
|
+
/** Opens a picker rather than toggling in place — draws a chevron. */
|
|
7
|
+
export const menu = $props.menu || false
|
|
8
|
+
export const count = $props.count || ''
|
|
9
|
+
|
|
10
|
+
export const hasCount = count !== '' && count !== 0 && count !== '0'
|
|
11
|
+
|
|
12
|
+
/*
|
|
13
|
+
* `shrink-0`, because the row scrolls.
|
|
14
|
+
*
|
|
15
|
+
* Without it flexbox distributes the overflow by squeezing every chip, and a
|
|
16
|
+
* row of filters that cannot overflow instead renders as a row of ellipses.
|
|
17
|
+
*/
|
|
18
|
+
export const chipClasses = `
|
|
19
|
+
inline-flex shrink-0 items-center gap-1.5 whitespace-nowrap rounded-full border px-4 py-2
|
|
20
|
+
text-sm font-medium transition-colors
|
|
21
|
+
${active
|
|
22
|
+
? 'border-transparent bg-fg text-surface'
|
|
23
|
+
: 'border-line bg-surface-raised text-fg hover:bg-surface-sunken'}
|
|
24
|
+
`.replace(/\s+/g, ' ').trim()
|
|
25
|
+
</script>
|
|
26
|
+
|
|
27
|
+
<a
|
|
28
|
+
href="{{ href }}"
|
|
29
|
+
class="{{ chipClasses }}"
|
|
30
|
+
data-stx-filter-chip
|
|
31
|
+
@if (active)
|
|
32
|
+
aria-current="true"
|
|
33
|
+
@endif
|
|
34
|
+
>
|
|
35
|
+
<span>{{ label }}</span>
|
|
36
|
+
|
|
37
|
+
@if (hasCount)
|
|
38
|
+
<span class="rounded-full bg-surface-sunken px-1.5 text-xs font-semibold text-fg-muted">{{ count }}</span>
|
|
39
|
+
@endif
|
|
40
|
+
|
|
41
|
+
@if (menu)
|
|
42
|
+
{{--
|
|
43
|
+
The chevron is the whole difference between "this filters the list" and
|
|
44
|
+
"this opens a picker", so it is drawn rather than implied.
|
|
45
|
+
--}}
|
|
46
|
+
<svg class="h-4 w-4 opacity-60" viewBox="0 0 20 20" fill="none" aria-hidden="true">
|
|
47
|
+
<path d="M6 8l4 4 4-4" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" />
|
|
48
|
+
</svg>
|
|
49
|
+
@endif
|
|
50
|
+
</a>
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
<script server>
|
|
2
|
+
/**
|
|
3
|
+
* The row of pills under a screen title.
|
|
4
|
+
*
|
|
5
|
+
* Two kinds of chip live in the same row and they are not the same control. A
|
|
6
|
+
* **toggle** is on or off — "Unread". A **menu** opens something and carries a
|
|
7
|
+
* chevron — "Repository". Rendering both as bare pills, which is the tempting
|
|
8
|
+
* shortcut, means a reader cannot tell which of them will change the list in
|
|
9
|
+
* place and which will cover the screen with a picker.
|
|
10
|
+
*
|
|
11
|
+
* `chips` is `{ label, href, active?, menu?, count? }`.
|
|
12
|
+
*/
|
|
13
|
+
export const chips = $props.chips || []
|
|
14
|
+
export const className = $props.className || ''
|
|
15
|
+
|
|
16
|
+
/*
|
|
17
|
+
* Scrolls horizontally, and says so by bleeding past the gutter.
|
|
18
|
+
*
|
|
19
|
+
* Filters overflow a phone's width the moment there are four of them with real
|
|
20
|
+
* words in. The negative margin plus matching padding lets the row run to both
|
|
21
|
+
* screen edges while its first chip still lines up with the title above it —
|
|
22
|
+
* a row that stops at the gutter looks complete when it is not.
|
|
23
|
+
*/
|
|
24
|
+
export const rowClasses = `
|
|
25
|
+
-mx-4 flex gap-2 overflow-x-auto px-4 pb-1
|
|
26
|
+
[scrollbar-width:none] [&::-webkit-scrollbar]:hidden
|
|
27
|
+
${className}
|
|
28
|
+
`.replace(/\s+/g, ' ').trim()
|
|
29
|
+
</script>
|
|
30
|
+
|
|
31
|
+
<div class="{{ rowClasses }}" data-stx-filter-chips role="group" aria-label="Filters">
|
|
32
|
+
@foreach (chips as chip)
|
|
33
|
+
<FilterChip
|
|
34
|
+
:label="chip.label"
|
|
35
|
+
:href="chip.href"
|
|
36
|
+
:active="chip.active"
|
|
37
|
+
:menu="chip.menu"
|
|
38
|
+
:count="chip.count"
|
|
39
|
+
/>
|
|
40
|
+
@endforeach
|
|
41
|
+
</div>
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
<script server>
|
|
2
|
+
/**
|
|
3
|
+
* The row every mobile list is made of.
|
|
4
|
+
*
|
|
5
|
+
* Read off what a dense list actually has to carry: a leading mark, a line of
|
|
6
|
+
* context above the title, the title itself over two lines, a line of detail
|
|
7
|
+
* under it, a timestamp at the top right, and a count at the bottom right.
|
|
8
|
+
* Six slots, because a list that can only do three ends up with applications
|
|
9
|
+
* inventing a seventh layout per screen.
|
|
10
|
+
*
|
|
11
|
+
* Everything except `title` is optional and nothing reserves space when absent,
|
|
12
|
+
* so the same component renders a two-line notification and a five-line issue
|
|
13
|
+
* without either looking like a version of the other.
|
|
14
|
+
*/
|
|
15
|
+
export const title = $props.title || ''
|
|
16
|
+
/** Small line above the title — a repository, an owner, a path. */
|
|
17
|
+
export const eyebrow = $props.eyebrow || ''
|
|
18
|
+
/** Small line below the title — an actor, a branch, a summary. */
|
|
19
|
+
export const detail = $props.detail || ''
|
|
20
|
+
/** Top right. A relative time, usually. */
|
|
21
|
+
export const meta = $props.meta || ''
|
|
22
|
+
/** Bottom right. A comment count, a diff stat, a status. */
|
|
23
|
+
export const trailing = $props.trailing || ''
|
|
24
|
+
/** Inline SVG for the leading column. */
|
|
25
|
+
export const icon = $props.icon || ''
|
|
26
|
+
/** Draws the unread dot in the leading column, under the icon. */
|
|
27
|
+
export const unread = $props.unread || false
|
|
28
|
+
export const href = $props.href || ''
|
|
29
|
+
export const className = $props.className || ''
|
|
30
|
+
|
|
31
|
+
export const interactive = href !== ''
|
|
32
|
+
export const tag = interactive ? 'a' : 'div'
|
|
33
|
+
|
|
34
|
+
export const rowClasses = `
|
|
35
|
+
flex w-full items-start gap-3 px-4 py-3 text-left
|
|
36
|
+
${interactive ? 'transition-colors active:bg-surface-sunken' : ''}
|
|
37
|
+
${className}
|
|
38
|
+
`.replace(/\s+/g, ' ').trim()
|
|
39
|
+
</script>
|
|
40
|
+
|
|
41
|
+
<{{ tag }}
|
|
42
|
+
class="{{ rowClasses }}"
|
|
43
|
+
data-stx-list-row
|
|
44
|
+
@if (interactive)
|
|
45
|
+
href="{{ href }}"
|
|
46
|
+
@endif
|
|
47
|
+
>
|
|
48
|
+
@if (icon || unread)
|
|
49
|
+
<span class="flex w-6 shrink-0 flex-col items-center gap-1.5 pt-0.5" aria-hidden="true">
|
|
50
|
+
{!! icon !!}
|
|
51
|
+
|
|
52
|
+
@if (unread)
|
|
53
|
+
{{--
|
|
54
|
+
A dot, not a bold title. Weight is already carrying the title's
|
|
55
|
+
hierarchy, and an unread list rendered entirely in bold has no
|
|
56
|
+
hierarchy left to spend.
|
|
57
|
+
--}}
|
|
58
|
+
<span class="h-2 w-2 rounded-full bg-accent"></span>
|
|
59
|
+
@endif
|
|
60
|
+
</span>
|
|
61
|
+
@endif
|
|
62
|
+
|
|
63
|
+
<span class="min-w-0 flex-1">
|
|
64
|
+
@if (eyebrow)
|
|
65
|
+
<span class="block truncate text-sm text-fg-muted">{{ eyebrow }}</span>
|
|
66
|
+
@endif
|
|
67
|
+
|
|
68
|
+
{{--
|
|
69
|
+
Two lines, then clipped. One line loses the end of nearly every issue
|
|
70
|
+
title; unbounded lets a single pasted stack trace push the rest of the
|
|
71
|
+
list off the screen.
|
|
72
|
+
--}}
|
|
73
|
+
<span class="block text-[15px] font-semibold leading-snug text-fg-strong [display:-webkit-box] [-webkit-box-orient:vertical] [-webkit-line-clamp:2] overflow-hidden">
|
|
74
|
+
{{ title }}
|
|
75
|
+
</span>
|
|
76
|
+
|
|
77
|
+
@if (detail)
|
|
78
|
+
<span class="mt-0.5 block truncate text-sm text-fg-muted">{{ detail }}</span>
|
|
79
|
+
@endif
|
|
80
|
+
</span>
|
|
81
|
+
|
|
82
|
+
@if (meta || trailing)
|
|
83
|
+
{{--
|
|
84
|
+
The right column is a column: timestamp at the top, count at the bottom,
|
|
85
|
+
both right-aligned. Putting them on one line makes the row's height
|
|
86
|
+
depend on which of the two is present.
|
|
87
|
+
--}}
|
|
88
|
+
<span class="flex shrink-0 flex-col items-end gap-2 pt-0.5">
|
|
89
|
+
@if (meta)
|
|
90
|
+
<span class="text-sm text-fg-muted">{{ meta }}</span>
|
|
91
|
+
@endif
|
|
92
|
+
|
|
93
|
+
@if (trailing)
|
|
94
|
+
<span class="rounded-md bg-surface-sunken px-1.5 py-0.5 text-xs font-medium text-fg-muted">{{ trailing }}</span>
|
|
95
|
+
@endif
|
|
96
|
+
</span>
|
|
97
|
+
@endif
|
|
98
|
+
</{{ tag }}>
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
<script server>
|
|
2
|
+
/**
|
|
3
|
+
* The top of a screen: a row of controls, then a large title under it.
|
|
4
|
+
*
|
|
5
|
+
* Two rows rather than one, because they answer different questions. The
|
|
6
|
+
* control row is where back and the screen's actions live and it stays the same
|
|
7
|
+
* height everywhere; the title is display-sized and belongs to the content, so
|
|
8
|
+
* it scrolls away while the controls do not.
|
|
9
|
+
*
|
|
10
|
+
* `title` may be empty — a screen pushed onto a stack usually wants its name in
|
|
11
|
+
* the small centre slot beside the back button instead, which is what
|
|
12
|
+
* `compactTitle` is.
|
|
13
|
+
*/
|
|
14
|
+
export const title = $props.title || ''
|
|
15
|
+
export const compactTitle = $props.compactTitle || ''
|
|
16
|
+
export const backHref = $props.backHref || ''
|
|
17
|
+
/** Markup for the right-hand side: search, add, an avatar. */
|
|
18
|
+
export const actions = $props.actions || ''
|
|
19
|
+
export const className = $props.className || ''
|
|
20
|
+
|
|
21
|
+
export const hasControls = backHref !== '' || compactTitle !== '' || actions !== ''
|
|
22
|
+
|
|
23
|
+
/*
|
|
24
|
+
* The safe-area inset is padding on the header itself.
|
|
25
|
+
*
|
|
26
|
+
* Not a spacer element above it: on a notched device the status bar sits over
|
|
27
|
+
* the top of the page, and a header that starts below a fixed-height spacer is
|
|
28
|
+
* right on exactly one device.
|
|
29
|
+
*/
|
|
30
|
+
export const headerStyle = 'padding-top: max(env(safe-area-inset-top, 0px), 12px);'
|
|
31
|
+
</script>
|
|
32
|
+
|
|
33
|
+
<header class="px-4 {{ className }}" style="{{ headerStyle }}" data-stx-nav-bar>
|
|
34
|
+
@if (hasControls)
|
|
35
|
+
<div class="flex h-11 items-center gap-2">
|
|
36
|
+
@if (backHref)
|
|
37
|
+
<a
|
|
38
|
+
href="{{ backHref }}"
|
|
39
|
+
class="flex h-9 w-9 shrink-0 items-center justify-center rounded-full bg-surface-raised text-fg"
|
|
40
|
+
aria-label="Back"
|
|
41
|
+
>
|
|
42
|
+
<svg class="h-5 w-5" viewBox="0 0 20 20" fill="none" aria-hidden="true">
|
|
43
|
+
<path d="M12 4l-6 6 6 6" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
|
|
44
|
+
</svg>
|
|
45
|
+
</a>
|
|
46
|
+
@endif
|
|
47
|
+
|
|
48
|
+
@if (compactTitle)
|
|
49
|
+
<span class="truncate text-[17px] font-semibold text-fg-strong">{{ compactTitle }}</span>
|
|
50
|
+
@endif
|
|
51
|
+
|
|
52
|
+
@if (actions)
|
|
53
|
+
{{-- Pushed right by the spacer rather than by `justify-between`, which
|
|
54
|
+
would centre a lone action when there is no back button. --}}
|
|
55
|
+
<span class="ml-auto flex items-center gap-2">{!! actions !!}</span>
|
|
56
|
+
@endif
|
|
57
|
+
</div>
|
|
58
|
+
@endif
|
|
59
|
+
|
|
60
|
+
@if (title)
|
|
61
|
+
<h1 class="pb-3 pt-1 text-[34px] font-bold leading-tight tracking-tight text-fg-strong">{{ title }}</h1>
|
|
62
|
+
@endif
|
|
63
|
+
</header>
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
<script server>
|
|
2
|
+
/**
|
|
3
|
+
* A titled group of rows on a rounded card — the "My Work" pattern.
|
|
4
|
+
*
|
|
5
|
+
* The title sits *outside* the card. That is the whole convention: the card is
|
|
6
|
+
* the group, the heading names it, and putting the heading inside makes the
|
|
7
|
+
* first row look like a header and every subsequent group look like a
|
|
8
|
+
* continuation of the one above it.
|
|
9
|
+
*
|
|
10
|
+
* Dividers are drawn by the card rather than by the rows, via
|
|
11
|
+
* `divide-y`, so a row does not have to know whether it is last — which is how
|
|
12
|
+
* a trailing hairline ends up under the final item of every list.
|
|
13
|
+
*/
|
|
14
|
+
export const title = $props.title || ''
|
|
15
|
+
/** Right-aligned control beside the title — "See all", a menu glyph. */
|
|
16
|
+
export const action = $props.action || ''
|
|
17
|
+
export const actionHref = $props.actionHref || ''
|
|
18
|
+
export const className = $props.className || ''
|
|
19
|
+
|
|
20
|
+
export const cardClasses = `
|
|
21
|
+
overflow-hidden rounded-2xl border border-line bg-surface-raised
|
|
22
|
+
divide-y divide-line
|
|
23
|
+
`.replace(/\s+/g, ' ').trim()
|
|
24
|
+
</script>
|
|
25
|
+
|
|
26
|
+
<section class="{{ className }}" data-stx-section-card>
|
|
27
|
+
@if (title || action)
|
|
28
|
+
<header class="flex items-baseline justify-between px-1 pb-2">
|
|
29
|
+
@if (title)
|
|
30
|
+
<h2 class="text-2xl font-bold tracking-tight text-fg-strong">{{ title }}</h2>
|
|
31
|
+
@endif
|
|
32
|
+
|
|
33
|
+
@if (action)
|
|
34
|
+
<a href="{{ actionHref }}" class="text-sm font-medium text-accent">{{ action }}</a>
|
|
35
|
+
@endif
|
|
36
|
+
</header>
|
|
37
|
+
@endif
|
|
38
|
+
|
|
39
|
+
<div class="{{ cardClasses }}">
|
|
40
|
+
<slot />
|
|
41
|
+
</div>
|
|
42
|
+
</section>
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
<script server>
|
|
2
|
+
/**
|
|
3
|
+
* The bottom tab bar — the one control a phone app is navigated by.
|
|
4
|
+
*
|
|
5
|
+
* Floating rather than flush: a rounded bar inset from the window edges with
|
|
6
|
+
* the content scrolling *under* it, which is what every current iOS app does
|
|
7
|
+
* and what the safe area at the bottom of a modern iPhone is shaped for. A bar
|
|
8
|
+
* pinned flush to the bottom either eats the home indicator or leaves a band of
|
|
9
|
+
* dead colour beneath itself.
|
|
10
|
+
*
|
|
11
|
+
* `items` is the whole bar. Each is `{ label, href, icon, active?, badge? }`,
|
|
12
|
+
* where `icon` is inline SVG or any markup — the bar does not own an icon set,
|
|
13
|
+
* because an application's icons are its own and a component that ships them
|
|
14
|
+
* makes every consumer look like the component.
|
|
15
|
+
*/
|
|
16
|
+
export const items = $props.items || []
|
|
17
|
+
export const className = $props.className || ''
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* `env(safe-area-inset-bottom)` with a floor, not instead of one.
|
|
21
|
+
*
|
|
22
|
+
* On a device with a home indicator the inset is real and the bar must clear
|
|
23
|
+
* it. On one without — an SE, a simulator in a small window, a desktop browser
|
|
24
|
+
* looking at the mobile layout — the inset is `0px` and a bar with no floor
|
|
25
|
+
* sits on the very edge of the screen. `max()` is the only spelling that gets
|
|
26
|
+
* both right.
|
|
27
|
+
*/
|
|
28
|
+
export const barStyle = 'bottom: max(env(safe-area-inset-bottom, 0px), 12px);'
|
|
29
|
+
|
|
30
|
+
export const barClasses = `
|
|
31
|
+
fixed inset-x-0 z-40 mx-auto flex w-max max-w-[calc(100%-24px)] items-stretch gap-1
|
|
32
|
+
rounded-full border border-line bg-surface-raised/90 px-2 py-1.5
|
|
33
|
+
shadow-lg backdrop-blur-xl
|
|
34
|
+
${className}
|
|
35
|
+
`.replace(/\s+/g, ' ').trim()
|
|
36
|
+
</script>
|
|
37
|
+
|
|
38
|
+
{{--
|
|
39
|
+
A `nav` with a label, because this is the primary navigation of the
|
|
40
|
+
application and a screen reader landing on an unlabelled group of five links
|
|
41
|
+
has no way to know that.
|
|
42
|
+
--}}
|
|
43
|
+
<nav class="{{ barClasses }}" style="{{ barStyle }}" aria-label="Primary" data-stx-tab-bar>
|
|
44
|
+
@foreach (items as item)
|
|
45
|
+
<TabBarItem
|
|
46
|
+
:label="item.label"
|
|
47
|
+
:href="item.href"
|
|
48
|
+
:icon="item.icon"
|
|
49
|
+
:active="item.active"
|
|
50
|
+
:badge="item.badge"
|
|
51
|
+
/>
|
|
52
|
+
@endforeach
|
|
53
|
+
</nav>
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
<script server>
|
|
2
|
+
/**
|
|
3
|
+
* One destination in the bottom bar.
|
|
4
|
+
*
|
|
5
|
+
* Icon over label, and the label always present. Icon-only bars test well with
|
|
6
|
+
* the people who designed them and badly with everyone else: a glyph is only
|
|
7
|
+
* unambiguous once you already know what the app calls that screen.
|
|
8
|
+
*/
|
|
9
|
+
export const label = $props.label || ''
|
|
10
|
+
export const href = $props.href || '#'
|
|
11
|
+
export const icon = $props.icon || ''
|
|
12
|
+
export const active = $props.active || false
|
|
13
|
+
/** A count, or any short string. Zero and empty are not rendered. */
|
|
14
|
+
export const badge = $props.badge || ''
|
|
15
|
+
|
|
16
|
+
export const hasBadge = badge !== '' && badge !== 0 && badge !== '0'
|
|
17
|
+
|
|
18
|
+
/*
|
|
19
|
+
* The active state is a filled pill behind the item rather than a colour
|
|
20
|
+
* change alone. Colour alone is the one distinction that disappears for the
|
|
21
|
+
* ~8% of men with a red-green deficiency and for anyone in bright sunlight,
|
|
22
|
+
* which is most of the time a phone is used outdoors.
|
|
23
|
+
*/
|
|
24
|
+
export const itemClasses = `
|
|
25
|
+
relative flex min-w-[68px] flex-col items-center gap-0.5 rounded-full px-4 py-1.5
|
|
26
|
+
text-[11px] font-medium leading-tight transition-colors
|
|
27
|
+
${active ? 'bg-surface-sunken text-accent' : 'text-fg-muted hover:text-fg'}
|
|
28
|
+
`.replace(/\s+/g, ' ').trim()
|
|
29
|
+
</script>
|
|
30
|
+
|
|
31
|
+
<a
|
|
32
|
+
href="{{ href }}"
|
|
33
|
+
class="{{ itemClasses }}"
|
|
34
|
+
data-stx-tab-bar-item
|
|
35
|
+
@if (active)
|
|
36
|
+
aria-current="page"
|
|
37
|
+
@endif
|
|
38
|
+
>
|
|
39
|
+
<span class="relative flex h-6 w-6 items-center justify-center" aria-hidden="true">
|
|
40
|
+
{!! icon !!}
|
|
41
|
+
|
|
42
|
+
@if (hasBadge)
|
|
43
|
+
{{--
|
|
44
|
+
The badge sits on the icon rather than after the label: it has to
|
|
45
|
+
survive the label being two words wide, and anchoring it to the glyph
|
|
46
|
+
keeps every tab's badge in the same place.
|
|
47
|
+
--}}
|
|
48
|
+
<span class="absolute -right-2 -top-1 min-w-[16px] rounded-full bg-danger px-1 text-[10px] font-semibold leading-4 text-white">
|
|
49
|
+
{{ badge }}
|
|
50
|
+
</span>
|
|
51
|
+
@endif
|
|
52
|
+
</span>
|
|
53
|
+
|
|
54
|
+
<span>{{ label }}</span>
|
|
55
|
+
</a>
|
package/dist/index.d.ts
CHANGED
|
@@ -56,6 +56,8 @@ export * from './ui/form/index';
|
|
|
56
56
|
export * from './ui/image/index';
|
|
57
57
|
export * from './ui/input/index';
|
|
58
58
|
export * from './ui/listbox/index';
|
|
59
|
+
// The phone primitives: a bottom tab bar, filter pills, dense list rows.
|
|
60
|
+
export * from './ui/mobile/index';
|
|
59
61
|
export * from './ui/navigator/index';
|
|
60
62
|
export * from './ui/notification/index';
|
|
61
63
|
export * from './ui/pagination/index';
|