@multiplatform.one/components 7.79.0 → 7.80.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/dist/cjs/appShell/AppShell.cjs +432 -0
- package/dist/cjs/appShell/AppShell.native.js +499 -0
- package/dist/cjs/appShell/AppShell.native.js.map +1 -0
- package/dist/cjs/appShell/index.cjs +1 -0
- package/dist/cjs/appShell/index.native.js +1 -0
- package/dist/cjs/appShell/index.native.js.map +1 -1
- package/dist/cjs/layouts/page.cjs +3 -0
- package/dist/cjs/layouts/page.native.js +3 -0
- package/dist/cjs/layouts/page.native.js.map +1 -1
- package/dist/cjs/layouts/screenScroll.cjs +55 -0
- package/dist/cjs/layouts/screenScroll.native.js +59 -0
- package/dist/cjs/layouts/screenScroll.native.js.map +1 -1
- package/dist/cjs/layouts/sidebar.cjs +46 -9
- package/dist/cjs/layouts/sidebar.native.js +49 -9
- package/dist/cjs/layouts/sidebar.native.js.map +1 -1
- package/dist/esm/Accordion/index.native.js.map +1 -1
- package/dist/esm/AnimateHeight/index.native.js.map +1 -1
- package/dist/esm/Avatar/getInitials.native.js.map +1 -1
- package/dist/esm/Avatar/index.native.js.map +1 -1
- package/dist/esm/Badge/index.native.js.map +1 -1
- package/dist/esm/Button.native.js.map +1 -1
- package/dist/esm/Carousel/index.native.js.map +1 -1
- package/dist/esm/Chip/index.native.js.map +1 -1
- package/dist/esm/Collapsible.native.js.map +1 -1
- package/dist/esm/ContextMenu/index.native.js.map +1 -1
- package/dist/esm/CopyField/CopyMenuButton.native.js.map +1 -1
- package/dist/esm/CopyField/index.native.js.map +1 -1
- package/dist/esm/Dialog.native.js.map +1 -1
- package/dist/esm/Diff/diff.native.js.map +1 -1
- package/dist/esm/Diff/index.native.js.map +1 -1
- package/dist/esm/DotIndicator/index.native.js.map +1 -1
- package/dist/esm/DropdownMenu/index.native.js.map +1 -1
- package/dist/esm/DropdownMenu/menuSettle.native.js.map +1 -1
- package/dist/esm/ElapsedProgress/index.native.js.map +1 -1
- package/dist/esm/ErrorBoundary/index.native.js.map +1 -1
- package/dist/esm/Heading.native.js.map +1 -1
- package/dist/esm/Kbd/index.native.js.map +1 -1
- package/dist/esm/Kbd/keys.native.js.map +1 -1
- package/dist/esm/Lightbox/MediaControl.native.js.map +1 -1
- package/dist/esm/Lightbox/index.gnome.native.js.map +1 -1
- package/dist/esm/Lightbox/shared.native.js.map +1 -1
- package/dist/esm/Lightbox/useLightbox.native.js.map +1 -1
- package/dist/esm/MachineLiteral/index.native.js.map +1 -1
- package/dist/esm/Material/index.native.js.map +1 -1
- package/dist/esm/Meter/index.native.js.map +1 -1
- package/dist/esm/Meter/meter.native.js.map +1 -1
- package/dist/esm/OverflowRow/index.native.js.map +1 -1
- package/dist/esm/Pagination.native.js.map +1 -1
- package/dist/esm/Popover.native.js.map +1 -1
- package/dist/esm/ProgressSteps/index.native.js.map +1 -1
- package/dist/esm/Quote/index.native.js.map +1 -1
- package/dist/esm/SchemeToggle/index.native.js.map +1 -1
- package/dist/esm/ScrollView/revealFocusedField.native.js.map +1 -1
- package/dist/esm/Separator/index.native.js.map +1 -1
- package/dist/esm/Sheet.native.js.map +1 -1
- package/dist/esm/SheetModal/SheetHeader.native.js.map +1 -1
- package/dist/esm/SheetModal/SheetNavigation.native.js.map +1 -1
- package/dist/esm/SheetModal/detents.native.js.map +1 -1
- package/dist/esm/SheetModal/useSheetFrameFocus.native.js.map +1 -1
- package/dist/esm/Skeleton/index.native.js.map +1 -1
- package/dist/esm/SkipLink/index.native.js.map +1 -1
- package/dist/esm/SortableList/SortableHandle.native.js.map +1 -1
- package/dist/esm/SortableList/index.native.js.map +1 -1
- package/dist/esm/SortableList/useSortableRows.native.js.map +1 -1
- package/dist/esm/SortableList/useUserOrder.native.js.map +1 -1
- package/dist/esm/Tabs/index.native.js.map +1 -1
- package/dist/esm/Text.native.js.map +1 -1
- package/dist/esm/ThemePresetPicker/index.native.js.map +1 -1
- package/dist/esm/Toast/index.native.js.map +1 -1
- package/dist/esm/Toast/store.native.js.map +1 -1
- package/dist/esm/Tooltip/nativePress.native.js.map +1 -1
- package/dist/esm/Tooltip/tooltipShared.native.js.map +1 -1
- package/dist/esm/TreeSelect/index.native.js.map +1 -1
- package/dist/esm/TreeSelect/treeSelectModel.native.js.map +1 -1
- package/dist/esm/Video/PlayerChrome.native.js.map +1 -1
- package/dist/esm/Video/VideoFrame.native.js.map +1 -1
- package/dist/esm/Video/types.native.js.map +1 -1
- package/dist/esm/Wheel/index.native.js.map +1 -1
- package/dist/esm/Wizard/index.native.js.map +1 -1
- package/dist/esm/actions/ActionBar.native.js.map +1 -1
- package/dist/esm/actions/DangerSection.native.js.map +1 -1
- package/dist/esm/actions/DestructiveAction.native.js.map +1 -1
- package/dist/esm/actions/HeaderActions.native.js.map +1 -1
- package/dist/esm/actions/StandardAction.native.js.map +1 -1
- package/dist/esm/actions/devWarn.native.js.map +1 -1
- package/dist/esm/actions/index.native.js.map +1 -1
- package/dist/esm/actions/severity.native.js.map +1 -1
- package/dist/esm/appShell/AppShell.mjs +406 -0
- package/dist/esm/appShell/AppShell.mjs.map +1 -0
- package/dist/esm/appShell/AppShell.native.js +470 -0
- package/dist/esm/appShell/AppShell.native.js.map +1 -0
- package/dist/esm/appShell/Dock.native.js.map +1 -1
- package/dist/esm/appShell/MpoTabHost.native.js.map +1 -1
- package/dist/esm/appShell/ScopeMenu.native.js.map +1 -1
- package/dist/esm/appShell/SettingsSplit.native.js.map +1 -1
- package/dist/esm/appShell/Springboard.native.js.map +1 -1
- package/dist/esm/appShell/SystemBar.native.js.map +1 -1
- package/dist/esm/appShell/TabBar.native.js.map +1 -1
- package/dist/esm/appShell/appTile.native.js.map +1 -1
- package/dist/esm/appShell/bottomChrome.native.js.map +1 -1
- package/dist/esm/appShell/index.mjs +1 -0
- package/dist/esm/appShell/index.mjs.map +1 -1
- package/dist/esm/appShell/index.native.js +1 -0
- package/dist/esm/appShell/index.native.js.map +1 -1
- package/dist/esm/charts/Axis.native.js.map +1 -1
- package/dist/esm/charts/BarChart.native.js.map +1 -1
- package/dist/esm/charts/ChartDataView.native.js.map +1 -1
- package/dist/esm/charts/ChartSurface.native.js.map +1 -1
- package/dist/esm/charts/LineChart.native.js.map +1 -1
- package/dist/esm/charts/PieChart.native.js.map +1 -1
- package/dist/esm/charts/Sparkline.native.js.map +1 -1
- package/dist/esm/charts/accessibility.native.js.map +1 -1
- package/dist/esm/charts/chartHeight.native.js.map +1 -1
- package/dist/esm/charts/composeChart.native.js.map +1 -1
- package/dist/esm/charts/index.native.js.map +1 -1
- package/dist/esm/charts/math.native.js.map +1 -1
- package/dist/esm/charts/series.native.js.map +1 -1
- package/dist/esm/charts/svg.native.js.map +1 -1
- package/dist/esm/charts/types.native.js.map +1 -1
- package/dist/esm/charts/useChartTheme.native.js.map +1 -1
- package/dist/esm/code/Code/index.native.js.map +1 -1
- package/dist/esm/code/CodeBlock/index.native.js.map +1 -1
- package/dist/esm/code/HighlightedCode/index.native.js.map +1 -1
- package/dist/esm/code/Highlighter/houseSyntaxTheme.native.js.map +1 -1
- package/dist/esm/code/Highlighter/index.native.js.map +1 -1
- package/dist/esm/code/Highlighter/namedGroupMatchAll.native.js.map +1 -1
- package/dist/esm/code/Pre/index.native.js.map +1 -1
- package/dist/esm/code/index.native.js.map +1 -1
- package/dist/esm/componentColors.native.js.map +1 -1
- package/dist/esm/feedback/ConfirmDialog.native.js.map +1 -1
- package/dist/esm/feedback/NotifyHost.native.js.map +1 -1
- package/dist/esm/feedback/copy.native.js.map +1 -1
- package/dist/esm/feedback/index.native.js.map +1 -1
- package/dist/esm/feedback/notify.native.js.map +1 -1
- package/dist/esm/feedback/route.native.js.map +1 -1
- package/dist/esm/feedback/store.native.js.map +1 -1
- package/dist/esm/formLayouts/FormLayouts.native.js.map +1 -1
- package/dist/esm/formLayouts/PreferencePanel.native.js.map +1 -1
- package/dist/esm/formLayouts/index.native.js.map +1 -1
- package/dist/esm/hooks/index.native.js.map +1 -1
- package/dist/esm/hooks/useClipboard.native.js.map +1 -1
- package/dist/esm/hooks/useDocumentTitle.native.js.map +1 -1
- package/dist/esm/hooks/useHoldCue.native.js.map +1 -1
- package/dist/esm/icons/CheckRegular.native.js.map +1 -1
- package/dist/esm/icons/MinusRegular.native.js.map +1 -1
- package/dist/esm/icons/index.native.js.map +1 -1
- package/dist/esm/icons/lucide.native.js.map +1 -1
- package/dist/esm/icons/themed.native.js.map +1 -1
- package/dist/esm/images/Image/ImageFrame.native.js.map +1 -1
- package/dist/esm/images/Image/framing.native.js.map +1 -1
- package/dist/esm/images/Image/mediaRadius.native.js.map +1 -1
- package/dist/esm/images/Image/shared.native.js.map +1 -1
- package/dist/esm/images/Image/types.native.js.map +1 -1
- package/dist/esm/images/index.native.js.map +1 -1
- package/dist/esm/index.native.js.map +1 -1
- package/dist/esm/knobImmuneExports.native.js.map +1 -1
- package/dist/esm/layouts/AsyncBoundary.native.js.map +1 -1
- package/dist/esm/layouts/Debug/index.native.js.map +1 -1
- package/dist/esm/layouts/EmptyState.native.js.map +1 -1
- package/dist/esm/layouts/IdentityPlate.native.js.map +1 -1
- package/dist/esm/layouts/KeyboardAvoidingWrapper/behavior.native.js.map +1 -1
- package/dist/esm/layouts/PageTemplates.native.js.map +1 -1
- package/dist/esm/layouts/PaneScaffold.native.js.map +1 -1
- package/dist/esm/layouts/SafeAreaWrapper/insets.native.js.map +1 -1
- package/dist/esm/layouts/StatusDot.native.js.map +1 -1
- package/dist/esm/layouts/StatusGlyphStrip.native.js.map +1 -1
- package/dist/esm/layouts/ViewSwitcher.native.js.map +1 -1
- package/dist/esm/layouts/attachments.native.js.map +1 -1
- package/dist/esm/layouts/bottomAccessory.native.js.map +1 -1
- package/dist/esm/layouts/breadcrumbs.native.js.map +1 -1
- package/dist/esm/layouts/index.native.js.map +1 -1
- package/dist/esm/layouts/page.mjs +4 -2
- package/dist/esm/layouts/page.mjs.map +1 -1
- package/dist/esm/layouts/page.native.js +4 -2
- package/dist/esm/layouts/page.native.js.map +1 -1
- package/dist/esm/layouts/pageHeadingScope.native.js.map +1 -1
- package/dist/esm/layouts/screenScroll.mjs +53 -1
- package/dist/esm/layouts/screenScroll.mjs.map +1 -1
- package/dist/esm/layouts/screenScroll.native.js +57 -1
- package/dist/esm/layouts/screenScroll.native.js.map +1 -1
- package/dist/esm/layouts/shellSidebarSlot.native.js.map +1 -1
- package/dist/esm/layouts/sidebar.mjs +45 -11
- package/dist/esm/layouts/sidebar.mjs.map +1 -1
- package/dist/esm/layouts/sidebar.native.js +48 -11
- package/dist/esm/layouts/sidebar.native.js.map +1 -1
- package/dist/esm/layouts/tags.native.js.map +1 -1
- package/dist/esm/layouts/timeline.native.js.map +1 -1
- package/dist/esm/layouts/toolbar.native.js.map +1 -1
- package/dist/esm/shared/actionScan.native.js.map +1 -1
- package/dist/esm/shared/i18n.native.js.map +1 -1
- package/dist/esm/shared/restoreRetries.native.js.map +1 -1
- package/dist/esm/shared/t.native.js.map +1 -1
- package/dist/esm/shared/textRidesText.native.js.map +1 -1
- package/dist/esm/shared/useReturnFocusOnClose.native.js.map +1 -1
- package/dist/esm/surfaces.native.js.map +1 -1
- package/dist/esm/tamagui.native.js.map +1 -1
- package/dist/esm/testSetup.native.js.map +1 -1
- package/dist/esm/utils/setTinted.native.js.map +1 -1
- package/dist/esm/utils/unwrapText.native.js.map +1 -1
- package/dist/esm/views/Calendar.native.js.map +1 -1
- package/dist/esm/views/Dashboard.native.js.map +1 -1
- package/dist/esm/views/Gantt.native.js.map +1 -1
- package/dist/esm/views/GanttDateGrip.native.js.map +1 -1
- package/dist/esm/views/ImageGrid.native.js.map +1 -1
- package/dist/esm/views/List.native.js.map +1 -1
- package/dist/esm/views/ListGroup.native.js.map +1 -1
- package/dist/esm/views/ListRow.native.js.map +1 -1
- package/dist/esm/views/Matrix.native.js.map +1 -1
- package/dist/esm/views/ReportBuilder.native.js.map +1 -1
- package/dist/esm/views/ShortcutCard.native.js.map +1 -1
- package/dist/esm/views/TreeView.native.js.map +1 -1
- package/dist/esm/views/calendarMath.native.js.map +1 -1
- package/dist/esm/views/ganttIntervals.native.js.map +1 -1
- package/dist/esm/views/ganttMath.native.js.map +1 -1
- package/dist/esm/views/index.native.js.map +1 -1
- package/dist/esm/views/kanbanDnd.native.js.map +1 -1
- package/dist/esm/views/matrixMath.native.js.map +1 -1
- package/dist/esm/views/sameNameScope.native.js.map +1 -1
- package/dist/esm/views/treeViewDnd.native.js.map +1 -1
- package/dist/esm/views/treeViewLayout.native.js.map +1 -1
- package/dist/esm/views/treeViewMotion.native.js.map +1 -1
- package/dist/esm/views/treeViewReveal.native.js.map +1 -1
- package/dist/esm/views/usePageFlowLoadMore.native.js.map +1 -1
- package/dist/esm/views/widgetChrome.native.js.map +1 -1
- package/dist/jsx/appShell/AppShell.mjs +406 -0
- package/dist/jsx/appShell/AppShell.mjs.map +1 -0
- package/dist/jsx/appShell/AppShell.native.js +499 -0
- package/dist/jsx/appShell/AppShell.native.js.map +1 -0
- package/dist/jsx/appShell/index.mjs +1 -0
- package/dist/jsx/appShell/index.mjs.map +1 -1
- package/dist/jsx/appShell/index.native.js +1 -0
- package/dist/jsx/appShell/index.native.js.map +1 -1
- package/dist/jsx/feedback/NotifyHost.native.js +1 -1
- package/dist/jsx/formLayouts/PreferencePanel.native.js +1 -1
- package/dist/jsx/index.native.js +1 -1
- package/dist/jsx/layouts/AsyncBoundary.native.js +1 -1
- package/dist/jsx/layouts/AsyncBoundary.native.js.map +1 -1
- package/dist/jsx/layouts/page.mjs +4 -2
- package/dist/jsx/layouts/page.mjs.map +1 -1
- package/dist/jsx/layouts/page.native.js +3 -0
- package/dist/jsx/layouts/page.native.js.map +1 -1
- package/dist/jsx/layouts/screenScroll.mjs +53 -1
- package/dist/jsx/layouts/screenScroll.mjs.map +1 -1
- package/dist/jsx/layouts/screenScroll.native.js +59 -0
- package/dist/jsx/layouts/screenScroll.native.js.map +1 -1
- package/dist/jsx/layouts/sidebar.mjs +45 -11
- package/dist/jsx/layouts/sidebar.mjs.map +1 -1
- package/dist/jsx/layouts/sidebar.native.js +49 -9
- package/dist/jsx/layouts/sidebar.native.js.map +1 -1
- package/package.json +9 -9
- package/src/appShell/AppShell.spec.tsx +259 -0
- package/src/appShell/AppShell.stories.tsx +112 -0
- package/src/appShell/AppShell.tsx +526 -0
- package/src/appShell/index.ts +1 -0
- package/src/layouts/Sidebar.spec.tsx +31 -0
- package/src/layouts/page.tsx +8 -0
- package/src/layouts/screenScroll.tsx +71 -0
- package/src/layouts/sidebar.tsx +83 -5
- package/types/appShell/AppShell.d.ts +47 -0
- package/types/appShell/AppShell.d.ts.map +1 -0
- package/types/appShell/index.d.ts +1 -0
- package/types/appShell/index.d.ts.map +1 -1
- package/types/layouts/page.d.ts +5 -0
- package/types/layouts/page.d.ts.map +1 -1
- package/types/layouts/screenScroll.d.ts +24 -0
- package/types/layouts/screenScroll.d.ts.map +1 -1
- package/types/layouts/sidebar.d.ts +31 -1
- package/types/layouts/sidebar.d.ts.map +1 -1
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react-native-web-vite";
|
|
2
|
+
import {
|
|
3
|
+
GearIcon,
|
|
4
|
+
HouseIcon,
|
|
5
|
+
LightningIcon,
|
|
6
|
+
SparkleIcon,
|
|
7
|
+
SquaresFourIcon,
|
|
8
|
+
StackIcon,
|
|
9
|
+
} from "@phosphor-icons/react";
|
|
10
|
+
import { useState } from "react";
|
|
11
|
+
import { XStack, YStack } from "tamagui";
|
|
12
|
+
import { Button } from "@multiplatform.one/forms";
|
|
13
|
+
import { useResolvedKnobs } from "@multiplatform.one/theme";
|
|
14
|
+
import { PageBarButton, PageHeader, PageSection, Screen } from "../layouts/page";
|
|
15
|
+
import { Pagination } from "../Pagination";
|
|
16
|
+
import { Text } from "../Text";
|
|
17
|
+
import { AppShell, type AppShellDestination } from "./AppShell";
|
|
18
|
+
|
|
19
|
+
const destinations: AppShellDestination[] = [
|
|
20
|
+
{ key: "/", label: "Home", icon: HouseIcon },
|
|
21
|
+
{ key: "/pokemon", label: "Pokemon", icon: SparkleIcon },
|
|
22
|
+
{ key: "/moves", label: "Moves", icon: LightningIcon },
|
|
23
|
+
{ key: "/backoffice", label: "Backoffice", icon: SquaresFourIcon },
|
|
24
|
+
{ key: "/settings", label: "Settings", icon: GearIcon, placement: "more" },
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
function Mark() {
|
|
28
|
+
const { knobProps } = useResolvedKnobs();
|
|
29
|
+
return (
|
|
30
|
+
<YStack
|
|
31
|
+
width="$2"
|
|
32
|
+
height="$2"
|
|
33
|
+
borderRadius="$3"
|
|
34
|
+
backgroundColor="$color4"
|
|
35
|
+
alignItems="center"
|
|
36
|
+
justifyContent="center"
|
|
37
|
+
>
|
|
38
|
+
<StackIcon size={knobProps.controlIcon.width} weight="bold" />
|
|
39
|
+
</YStack>
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function DemoPage({ path, open }: { path: string; open: (path: string) => void }) {
|
|
44
|
+
const pushed = path.split("/").filter(Boolean).length > 1;
|
|
45
|
+
const name = destinations.find((item) => path.startsWith(item.key) && item.key !== "/");
|
|
46
|
+
const title = pushed ? "Pikachu" : (name?.label ?? "Home");
|
|
47
|
+
return (
|
|
48
|
+
<Screen>
|
|
49
|
+
<PageHeader title={title} subtitle={pushed ? "#025 · Electric" : "1,025 entries"} />
|
|
50
|
+
<PageSection title="Recent">
|
|
51
|
+
{Array.from({ length: 14 }, (_, index) => (
|
|
52
|
+
<XStack key={index} alignItems="center" minHeight="$4">
|
|
53
|
+
<Text flexGrow={1}>Row {index + 1}</Text>
|
|
54
|
+
{pushed ? null : (
|
|
55
|
+
<Button
|
|
56
|
+
size="$3"
|
|
57
|
+
chromeless
|
|
58
|
+
onPress={() => open(`${path === "/" ? "" : path}/pikachu`)}
|
|
59
|
+
>
|
|
60
|
+
Open
|
|
61
|
+
</Button>
|
|
62
|
+
)}
|
|
63
|
+
</XStack>
|
|
64
|
+
))}
|
|
65
|
+
</PageSection>
|
|
66
|
+
</Screen>
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function Demo({ initialPath, accessory }: { initialPath: string; accessory?: boolean }) {
|
|
71
|
+
const [path, setPath] = useState(initialPath);
|
|
72
|
+
return (
|
|
73
|
+
<AppShell
|
|
74
|
+
destinations={destinations}
|
|
75
|
+
path={path}
|
|
76
|
+
onNavigate={setPath}
|
|
77
|
+
onBack={setPath}
|
|
78
|
+
title="multiplatform.one"
|
|
79
|
+
logo={<Mark />}
|
|
80
|
+
actions={<PageBarButton aria-label="Theme" icon={GearIcon} edge="end" />}
|
|
81
|
+
accessory={accessory ? <Pagination total={41} defaultPage={1} variant="dots" /> : undefined}
|
|
82
|
+
ownChrome={["/backoffice"]}
|
|
83
|
+
>
|
|
84
|
+
<DemoPage path={path} open={setPath} />
|
|
85
|
+
</AppShell>
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* The composed app frame (LC-175). Size class follows the viewport: below
|
|
91
|
+
* `md` the phone frame (nav bar, content, bottom accessory, tab bar), above
|
|
92
|
+
* it the split frame (sidebar of destinations beside the content column).
|
|
93
|
+
*/
|
|
94
|
+
const meta: Meta<typeof AppShell> = {
|
|
95
|
+
title: "Components/AppShell",
|
|
96
|
+
component: AppShell,
|
|
97
|
+
parameters: { layout: "fullscreen", status: { type: "beta" } },
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
export default meta;
|
|
101
|
+
type Story = StoryObj<typeof AppShell>;
|
|
102
|
+
|
|
103
|
+
/** A tab root: the mark leads, the large title sits in the page, tabs below. */
|
|
104
|
+
export const TabRoot: Story = { render: () => <Demo initialPath="/pokemon" /> };
|
|
105
|
+
|
|
106
|
+
/** A pushed route: tinted back to the destination, its tab still selected. */
|
|
107
|
+
export const Pushed: Story = { render: () => <Demo initialPath="/pokemon/pikachu" /> };
|
|
108
|
+
|
|
109
|
+
/** A page that hands the shell a bottom accessory: the tab bar floats it above itself. */
|
|
110
|
+
export const WithAccessory: Story = {
|
|
111
|
+
render: () => <Demo initialPath="/pokemon" accessory />,
|
|
112
|
+
};
|
|
@@ -0,0 +1,526 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AppShell — the one composed app frame every MPO app mounts at its root
|
|
3
|
+
* (LC-175 MPO-ONLY-REUSE). An app declares its destinations and hands over
|
|
4
|
+
* the current path; the shell draws the rest:
|
|
5
|
+
*
|
|
6
|
+
* - compact (phone): the nav bar on top (UINavigationBar: tinted back on a
|
|
7
|
+
* pushed route, the page's large title collapsing into it, trailing
|
|
8
|
+
* actions), the routed content, and the `tab` destinations in TabHost
|
|
9
|
+
* (FZ-8105): the system tab bar where the binary has one, the floating
|
|
10
|
+
* MPO TabBar (LC-150) elsewhere. The bar publishes the height it covers,
|
|
11
|
+
* which a Screen pads its content by. `more` destinations open from a
|
|
12
|
+
* pull-down in the bar.
|
|
13
|
+
* - regular (tablet, desktop): MPO's Sidebar on the translucent chrome
|
|
14
|
+
* material with the app's name, every destination and the app's actions
|
|
15
|
+
* at its foot (UISplitViewController's sidebar), the content centred at a
|
|
16
|
+
* readable width, and a page's panel stacked under the sidebar's rows
|
|
17
|
+
* (LC-101 ONE-SIDEBAR).
|
|
18
|
+
*
|
|
19
|
+
* A route listed in `ownChrome` mounts its own full chrome (a desk), so the
|
|
20
|
+
* shell draws nothing around it. A page whose `Screen` pins its own top bar
|
|
21
|
+
* (LC-157) owns the nav bar on a phone, so the shell's bar steps aside there.
|
|
22
|
+
*/
|
|
23
|
+
import { zIndex } from "@multiplatform.one/forms";
|
|
24
|
+
import { isWeb } from "@multiplatform.one/platform";
|
|
25
|
+
import {
|
|
26
|
+
MIN_PRESS_TARGET,
|
|
27
|
+
Surface,
|
|
28
|
+
hairline,
|
|
29
|
+
transitionProps,
|
|
30
|
+
useLayoutSizeClass,
|
|
31
|
+
useResolvedKnobs,
|
|
32
|
+
} from "@multiplatform.one/theme";
|
|
33
|
+
import { ListIcon } from "@phosphor-icons/react";
|
|
34
|
+
import { createElement, useEffect, useMemo, useState, type ReactNode } from "react";
|
|
35
|
+
import type { LayoutChangeEvent } from "react-native";
|
|
36
|
+
import { SizableText, XStack, YStack } from "tamagui";
|
|
37
|
+
import { componentColors } from "../componentColors";
|
|
38
|
+
import { DropdownMenu, type DropdownMenuEntry } from "../DropdownMenu";
|
|
39
|
+
import { PageBackButton, PageBarButton, READABLE_MAX_WIDTH } from "../layouts/page";
|
|
40
|
+
import { ShellScrollReportScope, useShellScrollReports } from "../layouts/screenScroll";
|
|
41
|
+
import {
|
|
42
|
+
ShellSidebarSlotProvider,
|
|
43
|
+
useShellSidebarSlotState,
|
|
44
|
+
type ShellSidebarPanel,
|
|
45
|
+
} from "../layouts/shellSidebarSlot";
|
|
46
|
+
import { BottomAccessory } from "../layouts/bottomAccessory";
|
|
47
|
+
import { Sidebar } from "../layouts/sidebar";
|
|
48
|
+
import { useTranslation } from "../shared/i18n";
|
|
49
|
+
import type { TabBarIcon, TabBarItem } from "./TabBar";
|
|
50
|
+
import { TabHost } from "./TabHost";
|
|
51
|
+
import { useBottomChromeInset, useFloatingBarMounted } from "./bottomChrome";
|
|
52
|
+
|
|
53
|
+
export interface AppShellDestination {
|
|
54
|
+
/** The destination's route; a path under it keeps it selected. */
|
|
55
|
+
key: string;
|
|
56
|
+
/** One short word: it is the tab caption and the back label. */
|
|
57
|
+
label: string;
|
|
58
|
+
icon: TabBarIcon;
|
|
59
|
+
/** The SF Symbol the iOS system tab bar draws for this destination. */
|
|
60
|
+
nativeIcon?: TabBarItem["nativeIcon"];
|
|
61
|
+
/**
|
|
62
|
+
* `tab` (default) sits in the phone's tab bar; `more` opens from the bar's
|
|
63
|
+
* pull-down. Every destination sits in the regular-width sidebar.
|
|
64
|
+
*/
|
|
65
|
+
placement?: "tab" | "more";
|
|
66
|
+
/** Runs instead of `onNavigate(key)`, e.g. an external link. */
|
|
67
|
+
onPress?: () => void;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export interface AppShellProps {
|
|
71
|
+
destinations: readonly AppShellDestination[];
|
|
72
|
+
/** The current route path. */
|
|
73
|
+
path: string;
|
|
74
|
+
onNavigate: (key: string) => void;
|
|
75
|
+
/** The app's name: the sidebar's title and the home control's label. */
|
|
76
|
+
title: string;
|
|
77
|
+
/** The app's mark, leading the bar on a phone and the name in the sidebar. */
|
|
78
|
+
logo?: ReactNode;
|
|
79
|
+
/** Pressing the name or mark. Default: the first destination. */
|
|
80
|
+
onTitlePress?: () => void;
|
|
81
|
+
/** Trailing bar actions (theme, account). */
|
|
82
|
+
actions?: ReactNode;
|
|
83
|
+
/** Shows a tinted back on a route pushed below its destination (its key). */
|
|
84
|
+
onBack?: (destinationKey: string) => void;
|
|
85
|
+
/** The phone's bottom accessory (LC-174), handed to the tab bar or pinned to the bottom edge. */
|
|
86
|
+
accessory?: ReactNode;
|
|
87
|
+
/** Route prefixes that mount their own chrome; the shell draws none there. */
|
|
88
|
+
ownChrome?: readonly string[];
|
|
89
|
+
children?: ReactNode;
|
|
90
|
+
testID?: string;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function underKey(path: string, key: string): boolean {
|
|
94
|
+
if (key === "/") return path === "/";
|
|
95
|
+
return path === key || path.startsWith(`${key}/`);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** The destination that owns `path` (the longest key it sits under). */
|
|
99
|
+
export function appShellRoute(
|
|
100
|
+
destinations: readonly AppShellDestination[],
|
|
101
|
+
path: string,
|
|
102
|
+
): { destination: AppShellDestination | undefined; pushed: boolean } {
|
|
103
|
+
const normal = path.length > 1 ? path.replace(/\/+$/, "") : path;
|
|
104
|
+
let destination: AppShellDestination | undefined;
|
|
105
|
+
for (const candidate of destinations) {
|
|
106
|
+
if (candidate.onPress || !underKey(normal, candidate.key)) continue;
|
|
107
|
+
if (!destination || candidate.key.length > destination.key.length) destination = candidate;
|
|
108
|
+
}
|
|
109
|
+
return { destination, pushed: Boolean(destination && normal !== destination.key) };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** Web: the document scrolls under the sticky bar; native reports via Screen. */
|
|
113
|
+
function useDocumentScrolled(enabled: boolean): boolean {
|
|
114
|
+
const [scrolled, setScrolled] = useState(false);
|
|
115
|
+
useEffect(() => {
|
|
116
|
+
if (!enabled || typeof document === "undefined") return;
|
|
117
|
+
const sync = () => setScrolled(window.scrollY > 1);
|
|
118
|
+
sync();
|
|
119
|
+
window.addEventListener("scroll", sync, { passive: true });
|
|
120
|
+
return () => window.removeEventListener("scroll", sync);
|
|
121
|
+
}, [enabled]);
|
|
122
|
+
return scrolled;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const sticky = (edge: "top" | "bottom") =>
|
|
126
|
+
isWeb ? { position: "sticky" as const, [edge]: 0, zIndex: zIndex.stickyHeader } : null;
|
|
127
|
+
|
|
128
|
+
function HomeControl({
|
|
129
|
+
title,
|
|
130
|
+
onPress,
|
|
131
|
+
children,
|
|
132
|
+
}: {
|
|
133
|
+
title: string;
|
|
134
|
+
onPress: () => void;
|
|
135
|
+
children: ReactNode;
|
|
136
|
+
}) {
|
|
137
|
+
return (
|
|
138
|
+
<XStack
|
|
139
|
+
role="link"
|
|
140
|
+
aria-label={title}
|
|
141
|
+
alignItems="center"
|
|
142
|
+
minHeight={MIN_PRESS_TARGET}
|
|
143
|
+
flexShrink={1}
|
|
144
|
+
minWidth={0}
|
|
145
|
+
gap="$2"
|
|
146
|
+
cursor="pointer"
|
|
147
|
+
onPress={onPress}
|
|
148
|
+
data-testid="app-shell-home"
|
|
149
|
+
>
|
|
150
|
+
{children}
|
|
151
|
+
</XStack>
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
interface NavBarProps {
|
|
156
|
+
/** Centres the bar's row in the content column's readable width. */
|
|
157
|
+
maxWidth?: number;
|
|
158
|
+
leading: ReactNode;
|
|
159
|
+
trailing: ReactNode;
|
|
160
|
+
title: string | null;
|
|
161
|
+
showTitle: boolean;
|
|
162
|
+
scrolled: boolean;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* The shell's nav bar: one 44pt row on the page fill. Its hairline appears
|
|
167
|
+
* once content scrolls under it (scroll-edge appearance) and the inline
|
|
168
|
+
* title fades in once the page's large title has scrolled away, centred
|
|
169
|
+
* clear of the wider side group, both on the control motion role (LC-68).
|
|
170
|
+
*/
|
|
171
|
+
function AppShellNavBar({ maxWidth, leading, trailing, title, showTitle, scrolled }: NavBarProps) {
|
|
172
|
+
const { knobProps } = useResolvedKnobs({ component: "ScreenToolbar" });
|
|
173
|
+
const [sides, setSides] = useState({ leading: 0, trailing: 0 });
|
|
174
|
+
const measure = (side: "leading" | "trailing") => (event: LayoutChangeEvent) => {
|
|
175
|
+
const width = Math.ceil(event.nativeEvent.layout.width);
|
|
176
|
+
setSides((prev) => (prev[side] === width ? prev : { ...prev, [side]: width }));
|
|
177
|
+
};
|
|
178
|
+
const motion = transitionProps(knobProps.motion.control);
|
|
179
|
+
const clearance = Math.max(sides.leading, sides.trailing);
|
|
180
|
+
return (
|
|
181
|
+
<Surface bar>
|
|
182
|
+
<YStack
|
|
183
|
+
role="banner"
|
|
184
|
+
data-testid="app-shell-bar"
|
|
185
|
+
data-scroll-edge={scrolled ? "" : undefined}
|
|
186
|
+
backgroundColor="$background"
|
|
187
|
+
flexShrink={0}
|
|
188
|
+
{...hairline.bottom}
|
|
189
|
+
borderColor={(scrolled ? componentColors.divider : "transparent") as never}
|
|
190
|
+
{...motion}
|
|
191
|
+
{...sticky("top")}
|
|
192
|
+
>
|
|
193
|
+
<XStack
|
|
194
|
+
position="relative"
|
|
195
|
+
width="100%"
|
|
196
|
+
{...(maxWidth ? { maxWidth, alignSelf: "center" as const } : null)}
|
|
197
|
+
minHeight={MIN_PRESS_TARGET}
|
|
198
|
+
alignItems="center"
|
|
199
|
+
flexWrap="nowrap"
|
|
200
|
+
paddingHorizontal={knobProps.panelPadding.padding as "$4"}
|
|
201
|
+
{...knobProps.gap}
|
|
202
|
+
>
|
|
203
|
+
<XStack
|
|
204
|
+
alignItems="center"
|
|
205
|
+
flexShrink={1000}
|
|
206
|
+
minWidth={0}
|
|
207
|
+
{...knobProps.gap}
|
|
208
|
+
onLayout={measure("leading")}
|
|
209
|
+
>
|
|
210
|
+
{leading}
|
|
211
|
+
</XStack>
|
|
212
|
+
<XStack flexGrow={1} />
|
|
213
|
+
<XStack
|
|
214
|
+
alignItems="center"
|
|
215
|
+
flexShrink={1}
|
|
216
|
+
minWidth={0}
|
|
217
|
+
justifyContent="flex-end"
|
|
218
|
+
{...knobProps.gap}
|
|
219
|
+
onLayout={measure("trailing")}
|
|
220
|
+
>
|
|
221
|
+
{trailing}
|
|
222
|
+
</XStack>
|
|
223
|
+
{title ? (
|
|
224
|
+
<XStack
|
|
225
|
+
position="absolute"
|
|
226
|
+
top={0}
|
|
227
|
+
bottom={0}
|
|
228
|
+
left={clearance}
|
|
229
|
+
right={clearance}
|
|
230
|
+
alignItems="center"
|
|
231
|
+
justifyContent="center"
|
|
232
|
+
pointerEvents="none"
|
|
233
|
+
aria-hidden={!showTitle}
|
|
234
|
+
data-testid="app-shell-bar-title"
|
|
235
|
+
opacity={showTitle ? 1 : 0}
|
|
236
|
+
{...motion}
|
|
237
|
+
>
|
|
238
|
+
<SizableText {...knobProps.body} fontWeight="600" color="$color12" numberOfLines={1}>
|
|
239
|
+
{title}
|
|
240
|
+
</SizableText>
|
|
241
|
+
</XStack>
|
|
242
|
+
) : null}
|
|
243
|
+
</XStack>
|
|
244
|
+
</YStack>
|
|
245
|
+
</Surface>
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function StackedPanel({ panel }: { panel: ShellSidebarPanel }) {
|
|
250
|
+
const { knobProps } = useResolvedKnobs();
|
|
251
|
+
return (
|
|
252
|
+
<YStack
|
|
253
|
+
paddingHorizontal={knobProps.panelPadding.padding as "$4"}
|
|
254
|
+
{...knobProps.gap}
|
|
255
|
+
data-testid="app-shell-page-panel"
|
|
256
|
+
>
|
|
257
|
+
<SizableText
|
|
258
|
+
{...knobProps.body}
|
|
259
|
+
size="$2"
|
|
260
|
+
textTransform="uppercase"
|
|
261
|
+
color={knobProps.textAccentColor}
|
|
262
|
+
numberOfLines={1}
|
|
263
|
+
>
|
|
264
|
+
{panel.title}
|
|
265
|
+
</SizableText>
|
|
266
|
+
{panel.content}
|
|
267
|
+
</YStack>
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
export function AppShell({
|
|
272
|
+
destinations,
|
|
273
|
+
path,
|
|
274
|
+
onNavigate,
|
|
275
|
+
title,
|
|
276
|
+
logo,
|
|
277
|
+
onTitlePress,
|
|
278
|
+
actions,
|
|
279
|
+
onBack,
|
|
280
|
+
accessory,
|
|
281
|
+
ownChrome = [],
|
|
282
|
+
children,
|
|
283
|
+
testID = "app-shell",
|
|
284
|
+
}: AppShellProps) {
|
|
285
|
+
const { t } = useTranslation();
|
|
286
|
+
const { knobProps } = useResolvedKnobs();
|
|
287
|
+
const compact = useLayoutSizeClass() === "compact";
|
|
288
|
+
const { report, reporter } = useShellScrollReports();
|
|
289
|
+
const sidebarSlot = useShellSidebarSlotState();
|
|
290
|
+
const documentScrolled = useDocumentScrolled(isWeb);
|
|
291
|
+
const chromeInset = useBottomChromeInset();
|
|
292
|
+
const floatingBar = useFloatingBarMounted();
|
|
293
|
+
|
|
294
|
+
const { destination, pushed } = appShellRoute(destinations, path);
|
|
295
|
+
const ownsChrome = ownChrome.some((prefix) => underKey(path, prefix));
|
|
296
|
+
const go = (item: AppShellDestination) => (item.onPress ? item.onPress() : onNavigate(item.key));
|
|
297
|
+
const goHome = onTitlePress ?? (() => destinations[0] && go(destinations[0]));
|
|
298
|
+
|
|
299
|
+
const tabs = useMemo(
|
|
300
|
+
() => destinations.filter((item) => (item.placement ?? "tab") === "tab"),
|
|
301
|
+
[destinations],
|
|
302
|
+
);
|
|
303
|
+
const more = useMemo(
|
|
304
|
+
() => destinations.filter((item) => item.placement === "more"),
|
|
305
|
+
[destinations],
|
|
306
|
+
);
|
|
307
|
+
|
|
308
|
+
if (ownsChrome) {
|
|
309
|
+
return (
|
|
310
|
+
<YStack
|
|
311
|
+
flex={1}
|
|
312
|
+
minHeight={0}
|
|
313
|
+
backgroundColor="$background"
|
|
314
|
+
{...(isWeb ? { height: "100dvh" as never } : null)}
|
|
315
|
+
data-testid={testID}
|
|
316
|
+
data-chrome="own"
|
|
317
|
+
>
|
|
318
|
+
{children}
|
|
319
|
+
</YStack>
|
|
320
|
+
);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
const pageBar = Boolean(report?.ownTopBar);
|
|
324
|
+
const scrolled = isWeb ? documentScrolled : Boolean(report?.scrolled);
|
|
325
|
+
const largeTitle = report?.title ?? null;
|
|
326
|
+
const barTitle = largeTitle ?? destination?.label ?? null;
|
|
327
|
+
const showTitle = largeTitle ? Boolean(report?.collapsed) : Boolean(barTitle);
|
|
328
|
+
const back =
|
|
329
|
+
pushed && onBack && destination && !pageBar ? (
|
|
330
|
+
<PageBackButton label={destination.label} onPress={() => onBack(destination.key)} />
|
|
331
|
+
) : null;
|
|
332
|
+
const showBar = compact ? !pageBar : Boolean(back);
|
|
333
|
+
|
|
334
|
+
const moreMenu =
|
|
335
|
+
compact && more.length > 0 ? (
|
|
336
|
+
<DropdownMenu
|
|
337
|
+
placement="bottom-end"
|
|
338
|
+
items={more.map<DropdownMenuEntry>((item) => ({
|
|
339
|
+
key: item.key,
|
|
340
|
+
label: item.label,
|
|
341
|
+
icon: createElement(item.icon),
|
|
342
|
+
onSelect: () => go(item),
|
|
343
|
+
}))}
|
|
344
|
+
>
|
|
345
|
+
<PageBarButton
|
|
346
|
+
edge="end"
|
|
347
|
+
icon={ListIcon}
|
|
348
|
+
aria-label={t("More")}
|
|
349
|
+
data-testid="app-shell-more"
|
|
350
|
+
/>
|
|
351
|
+
</DropdownMenu>
|
|
352
|
+
) : null;
|
|
353
|
+
|
|
354
|
+
const bar = showBar ? (
|
|
355
|
+
<AppShellNavBar
|
|
356
|
+
leading={
|
|
357
|
+
back ??
|
|
358
|
+
(compact && logo ? (
|
|
359
|
+
<HomeControl title={title} onPress={goHome}>
|
|
360
|
+
{logo}
|
|
361
|
+
</HomeControl>
|
|
362
|
+
) : null)
|
|
363
|
+
}
|
|
364
|
+
trailing={
|
|
365
|
+
compact ? (
|
|
366
|
+
<>
|
|
367
|
+
{actions}
|
|
368
|
+
{moreMenu}
|
|
369
|
+
</>
|
|
370
|
+
) : null
|
|
371
|
+
}
|
|
372
|
+
maxWidth={compact ? undefined : READABLE_MAX_WIDTH}
|
|
373
|
+
title={barTitle}
|
|
374
|
+
showTitle={showTitle}
|
|
375
|
+
scrolled={scrolled}
|
|
376
|
+
/>
|
|
377
|
+
) : null;
|
|
378
|
+
|
|
379
|
+
const content = (
|
|
380
|
+
<YStack
|
|
381
|
+
role="main"
|
|
382
|
+
flex={1}
|
|
383
|
+
minWidth={0}
|
|
384
|
+
{...(isWeb ? null : { minHeight: 0 })}
|
|
385
|
+
data-testid="app-shell-content"
|
|
386
|
+
>
|
|
387
|
+
{compact ? (
|
|
388
|
+
children
|
|
389
|
+
) : (
|
|
390
|
+
<YStack
|
|
391
|
+
flex={1}
|
|
392
|
+
width="100%"
|
|
393
|
+
maxWidth={READABLE_MAX_WIDTH}
|
|
394
|
+
alignSelf="center"
|
|
395
|
+
{...(isWeb ? null : { minHeight: 0 })}
|
|
396
|
+
data-testid="app-shell-readable"
|
|
397
|
+
>
|
|
398
|
+
{children}
|
|
399
|
+
</YStack>
|
|
400
|
+
)}
|
|
401
|
+
</YStack>
|
|
402
|
+
);
|
|
403
|
+
|
|
404
|
+
const showTabs = compact && Boolean(destination && tabs.includes(destination));
|
|
405
|
+
const tabItems: TabBarItem[] = tabs.map(({ key, label, icon, nativeIcon }) => ({
|
|
406
|
+
key,
|
|
407
|
+
label,
|
|
408
|
+
icon,
|
|
409
|
+
nativeIcon,
|
|
410
|
+
}));
|
|
411
|
+
// The floating TabBar takes the accessory into its own capsule. Otherwise
|
|
412
|
+
// it renders in place, and on native it clears the system tab bar, which
|
|
413
|
+
// the content runs under.
|
|
414
|
+
const pageAccessory = accessory ? (
|
|
415
|
+
<YStack flexShrink={0} {...(isWeb || floatingBar ? null : { paddingBottom: chromeInset })}>
|
|
416
|
+
<BottomAccessory>{accessory}</BottomAccessory>
|
|
417
|
+
</YStack>
|
|
418
|
+
) : null;
|
|
419
|
+
|
|
420
|
+
const frame = compact ? (
|
|
421
|
+
<YStack
|
|
422
|
+
flex={1}
|
|
423
|
+
backgroundColor="$background"
|
|
424
|
+
{...(isWeb ? { minHeight: "100dvh" as never } : null)}
|
|
425
|
+
data-testid={testID}
|
|
426
|
+
data-size="compact"
|
|
427
|
+
>
|
|
428
|
+
{bar}
|
|
429
|
+
{tabItems.length > 0 ? (
|
|
430
|
+
<TabHost
|
|
431
|
+
items={tabItems}
|
|
432
|
+
activeKey={showTabs ? destination?.key : undefined}
|
|
433
|
+
onTabPress={(key) => {
|
|
434
|
+
const item = tabs.find((tab) => tab.key === key);
|
|
435
|
+
if (item) go(item);
|
|
436
|
+
}}
|
|
437
|
+
>
|
|
438
|
+
{content}
|
|
439
|
+
{pageAccessory}
|
|
440
|
+
</TabHost>
|
|
441
|
+
) : (
|
|
442
|
+
<>
|
|
443
|
+
{content}
|
|
444
|
+
{pageAccessory}
|
|
445
|
+
</>
|
|
446
|
+
)}
|
|
447
|
+
</YStack>
|
|
448
|
+
) : (
|
|
449
|
+
<XStack
|
|
450
|
+
flex={1}
|
|
451
|
+
backgroundColor="$background"
|
|
452
|
+
{...(isWeb ? { minHeight: "100dvh" as never } : null)}
|
|
453
|
+
data-testid={testID}
|
|
454
|
+
data-size="regular"
|
|
455
|
+
>
|
|
456
|
+
<YStack
|
|
457
|
+
flexShrink={0}
|
|
458
|
+
alignSelf="stretch"
|
|
459
|
+
{...(isWeb ? { position: "sticky" as const, top: 0, height: "100dvh" as never } : null)}
|
|
460
|
+
data-testid="app-shell-sidebar"
|
|
461
|
+
>
|
|
462
|
+
<Sidebar
|
|
463
|
+
material
|
|
464
|
+
aria-label={title}
|
|
465
|
+
header={
|
|
466
|
+
<XStack
|
|
467
|
+
paddingHorizontal={knobProps.panelPadding.padding as "$4"}
|
|
468
|
+
alignItems="center"
|
|
469
|
+
minHeight={MIN_PRESS_TARGET}
|
|
470
|
+
>
|
|
471
|
+
<HomeControl title={title} onPress={goHome}>
|
|
472
|
+
{logo}
|
|
473
|
+
<SizableText
|
|
474
|
+
{...knobProps.heading}
|
|
475
|
+
fontWeight="700"
|
|
476
|
+
color="$color12"
|
|
477
|
+
numberOfLines={1}
|
|
478
|
+
>
|
|
479
|
+
{title}
|
|
480
|
+
</SizableText>
|
|
481
|
+
</HomeControl>
|
|
482
|
+
</XStack>
|
|
483
|
+
}
|
|
484
|
+
footer={
|
|
485
|
+
actions ? (
|
|
486
|
+
<XStack
|
|
487
|
+
paddingHorizontal={knobProps.panelPadding.padding as "$4"}
|
|
488
|
+
alignItems="center"
|
|
489
|
+
minHeight={MIN_PRESS_TARGET}
|
|
490
|
+
flexWrap="wrap"
|
|
491
|
+
{...knobProps.gap}
|
|
492
|
+
data-testid="app-shell-sidebar-actions"
|
|
493
|
+
>
|
|
494
|
+
{actions}
|
|
495
|
+
</XStack>
|
|
496
|
+
) : undefined
|
|
497
|
+
}
|
|
498
|
+
sections={[
|
|
499
|
+
{
|
|
500
|
+
items: destinations.map((item) => ({
|
|
501
|
+
label: item.label,
|
|
502
|
+
value: item.key,
|
|
503
|
+
icon: createElement(item.icon),
|
|
504
|
+
active: item === destination,
|
|
505
|
+
onPress: () => go(item),
|
|
506
|
+
})),
|
|
507
|
+
},
|
|
508
|
+
]}
|
|
509
|
+
>
|
|
510
|
+
{sidebarSlot.panel ? <StackedPanel panel={sidebarSlot.panel} /> : null}
|
|
511
|
+
</Sidebar>
|
|
512
|
+
</YStack>
|
|
513
|
+
<YStack flex={1} minWidth={0}>
|
|
514
|
+
{bar}
|
|
515
|
+
{content}
|
|
516
|
+
{pageAccessory}
|
|
517
|
+
</YStack>
|
|
518
|
+
</XStack>
|
|
519
|
+
);
|
|
520
|
+
|
|
521
|
+
return (
|
|
522
|
+
<ShellSidebarSlotProvider value={compact ? null : sidebarSlot.value}>
|
|
523
|
+
<ShellScrollReportScope reporter={reporter}>{frame}</ShellScrollReportScope>
|
|
524
|
+
</ShellSidebarSlotProvider>
|
|
525
|
+
);
|
|
526
|
+
}
|
package/src/appShell/index.ts
CHANGED
|
@@ -120,3 +120,34 @@ describe("Sidebar footer", () => {
|
|
|
120
120
|
expect(result.getByTestId("rail").querySelector("[data-sidebar-footer]")).toBeNull();
|
|
121
121
|
});
|
|
122
122
|
});
|
|
123
|
+
|
|
124
|
+
describe("Sidebar on the shell material (FZ-7996)", () => {
|
|
125
|
+
const sections = [
|
|
126
|
+
{
|
|
127
|
+
items: [
|
|
128
|
+
{ value: "home", label: "Home", active: true, onPress: () => {} },
|
|
129
|
+
{ value: "pokemon", label: "Pokemon", onPress: () => {} },
|
|
130
|
+
{ value: "moves", label: "Moves", onPress: () => {} },
|
|
131
|
+
],
|
|
132
|
+
},
|
|
133
|
+
];
|
|
134
|
+
|
|
135
|
+
it("sets every row as a rounded pill with no rule between rows", () => {
|
|
136
|
+
const result = renderWithProviders(<Sidebar material sections={sections} />);
|
|
137
|
+
const rows = [...result.container.querySelectorAll("[data-sidebar-row]")];
|
|
138
|
+
expect(rows).toHaveLength(3);
|
|
139
|
+
for (const row of rows) {
|
|
140
|
+
expect(row.className).toMatch(/_btlr-/);
|
|
141
|
+
expect(row.className).toMatch(/_bbrr-/);
|
|
142
|
+
}
|
|
143
|
+
expect(rows[1]!.previousElementSibling).toBe(rows[0]);
|
|
144
|
+
expect(rows[2]!.previousElementSibling).toBe(rows[1]);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it("keeps the stacked group with rules between rows off the material", () => {
|
|
148
|
+
const result = renderWithProviders(<Sidebar sections={sections} />);
|
|
149
|
+
const rows = [...result.container.querySelectorAll("[data-sidebar-row]")];
|
|
150
|
+
expect(rows[1]!.previousElementSibling).not.toBe(rows[0]);
|
|
151
|
+
expect(rows[1]!.previousElementSibling?.hasAttribute("data-sidebar-row")).toBe(false);
|
|
152
|
+
});
|
|
153
|
+
});
|