@ankhorage/zora 0.3.6 → 0.3.7
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/CHANGELOG.md +6 -0
- package/README.md +0 -90
- package/package.json +5 -1
- package/src/components/badge/Badge.tsx +19 -0
- package/src/components/badge/index.ts +2 -0
- package/src/components/badge/types.ts +14 -0
- package/src/components/button/Button.tsx +13 -0
- package/src/components/button/index.ts +2 -0
- package/src/components/button/types.ts +16 -0
- package/src/components/card/Card.tsx +65 -0
- package/src/components/card/index.ts +2 -0
- package/src/components/card/types.ts +15 -0
- package/src/components/drawer/Drawer.tsx +27 -0
- package/src/components/drawer/index.ts +2 -0
- package/src/components/drawer/types.ts +12 -0
- package/src/components/icon/Icon.tsx +8 -0
- package/src/components/icon/index.ts +2 -0
- package/src/components/icon-button/IconButton.tsx +27 -0
- package/src/components/icon-button/index.ts +2 -0
- package/src/components/icon-button/types.ts +15 -0
- package/src/components/input/Input.tsx +38 -0
- package/src/components/input/index.ts +2 -0
- package/src/components/input/types.ts +12 -0
- package/src/components/modal/Modal.tsx +37 -0
- package/src/components/modal/index.ts +2 -0
- package/src/components/modal/types.ts +15 -0
- package/src/components/select/Select.tsx +49 -0
- package/src/components/select/index.ts +2 -0
- package/src/components/select/types.ts +14 -0
- package/src/components/tabs/Tabs.tsx +103 -0
- package/src/components/tabs/index.ts +2 -0
- package/src/components/tabs/types.ts +25 -0
- package/src/components/textarea/Textarea.tsx +38 -0
- package/src/components/textarea/index.ts +2 -0
- package/src/components/textarea/types.ts +12 -0
- package/src/components/toolbar/Toolbar.tsx +38 -0
- package/src/components/toolbar/ToolbarAction.tsx +15 -0
- package/src/components/toolbar/index.ts +3 -0
- package/src/components/toolbar/types.ts +21 -0
- package/src/index.ts +72 -0
- package/src/internal/deepMerge.ts +23 -0
- package/src/internal/recipes.test.ts +46 -0
- package/src/internal/recipes.ts +92 -0
- package/src/layout/app-shell/AppShell.tsx +15 -0
- package/src/layout/app-shell/index.ts +2 -0
- package/src/layout/app-shell/types.ts +7 -0
- package/src/layout/auth-layout/AuthLayout.tsx +29 -0
- package/src/layout/auth-layout/index.ts +2 -0
- package/src/layout/auth-layout/types.ts +10 -0
- package/src/layout/page/Page.tsx +17 -0
- package/src/layout/page/index.ts +2 -0
- package/src/layout/page/types.ts +11 -0
- package/src/layout/page-header/PageHeader.tsx +41 -0
- package/src/layout/page-header/index.ts +2 -0
- package/src/layout/page-header/types.ts +10 -0
- package/src/layout/page-section/PageSection.tsx +14 -0
- package/src/layout/page-section/index.ts +2 -0
- package/src/layout/page-section/types.ts +9 -0
- package/src/layout/settings-layout/SettingsLayout.tsx +26 -0
- package/src/layout/settings-layout/index.ts +2 -0
- package/src/layout/settings-layout/types.ts +10 -0
- package/src/layout/sidebar-layout/SidebarLayout.tsx +23 -0
- package/src/layout/sidebar-layout/index.ts +2 -0
- package/src/layout/sidebar-layout/types.ts +10 -0
- package/src/layout/topbar-layout/TopbarLayout.tsx +14 -0
- package/src/layout/topbar-layout/index.ts +2 -0
- package/src/layout/topbar-layout/types.ts +8 -0
- package/src/patterns/collection-editor/CollectionEditor.tsx +100 -0
- package/src/patterns/collection-editor/index.ts +2 -0
- package/src/patterns/collection-editor/types.ts +25 -0
- package/src/patterns/confirm-dialog/ConfirmDialog.tsx +46 -0
- package/src/patterns/confirm-dialog/index.ts +2 -0
- package/src/patterns/confirm-dialog/types.ts +19 -0
- package/src/patterns/disclosure-section/DisclosureSection.tsx +61 -0
- package/src/patterns/disclosure-section/index.ts +2 -0
- package/src/patterns/disclosure-section/types.ts +15 -0
- package/src/patterns/empty-state/EmptyState.tsx +53 -0
- package/src/patterns/empty-state/index.ts +2 -0
- package/src/patterns/empty-state/types.ts +20 -0
- package/src/patterns/form-field/FormField.tsx +27 -0
- package/src/patterns/form-field/index.ts +2 -0
- package/src/patterns/form-field/types.ts +11 -0
- package/src/patterns/inspector-field/InspectorField.tsx +16 -0
- package/src/patterns/inspector-field/index.ts +2 -0
- package/src/patterns/inspector-field/types.ts +15 -0
- package/src/patterns/notice/Notice.tsx +30 -0
- package/src/patterns/notice/index.ts +2 -0
- package/src/patterns/notice/types.ts +12 -0
- package/src/patterns/panel/Panel.tsx +8 -0
- package/src/patterns/panel/index.ts +2 -0
- package/src/patterns/panel/types.ts +15 -0
- package/src/patterns/responsive-panel/ResponsivePanel.tsx +70 -0
- package/src/patterns/responsive-panel/index.ts +2 -0
- package/src/patterns/responsive-panel/types.ts +20 -0
- package/src/patterns/section-header/SectionHeader.tsx +39 -0
- package/src/patterns/section-header/index.ts +2 -0
- package/src/patterns/section-header/types.ts +9 -0
- package/src/patterns/settings-row/SettingsRow.tsx +40 -0
- package/src/patterns/settings-row/index.ts +2 -0
- package/src/patterns/settings-row/types.ts +27 -0
- package/src/patterns/switch-field/SwitchField.tsx +24 -0
- package/src/patterns/switch-field/index.ts +2 -0
- package/src/patterns/switch-field/types.ts +10 -0
- package/src/patterns/tile-grid/PaletteItem.tsx +49 -0
- package/src/patterns/tile-grid/TileGrid.tsx +44 -0
- package/src/patterns/tile-grid/index.ts +3 -0
- package/src/patterns/tile-grid/types.ts +20 -0
- package/src/patterns/tree-view/TreeItem.tsx +86 -0
- package/src/patterns/tree-view/TreeView.tsx +50 -0
- package/src/patterns/tree-view/index.ts +3 -0
- package/src/patterns/tree-view/types.ts +31 -0
- package/src/theme/ZoraProvider.tsx +22 -0
- package/src/theme/createZoraTheme.test.ts +25 -0
- package/src/theme/createZoraTheme.ts +10 -0
- package/src/theme/index.ts +6 -0
- package/src/theme/useZoraTheme.ts +5 -0
- package/src/theme/zoraTheme.ts +16 -0
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
3
|
+
import { Drawer } from '../../components/drawer';
|
|
4
|
+
import { Modal } from '../../components/modal';
|
|
5
|
+
import { Panel } from '../panel';
|
|
6
|
+
import type { ResponsivePanelProps } from './types';
|
|
7
|
+
|
|
8
|
+
export function ResponsivePanel({
|
|
9
|
+
title,
|
|
10
|
+
description,
|
|
11
|
+
actions,
|
|
12
|
+
footer,
|
|
13
|
+
children,
|
|
14
|
+
open,
|
|
15
|
+
onOpenChange,
|
|
16
|
+
side = 'right',
|
|
17
|
+
desktopMode = 'inline',
|
|
18
|
+
mobileMode = 'drawer',
|
|
19
|
+
compact = true,
|
|
20
|
+
testID,
|
|
21
|
+
}: ResponsivePanelProps) {
|
|
22
|
+
if (!open) return null;
|
|
23
|
+
|
|
24
|
+
// For now, we assume desktopMode determines the rendering.
|
|
25
|
+
// In a real app, this would react to window size.
|
|
26
|
+
if (desktopMode === 'floating') {
|
|
27
|
+
if (mobileMode === 'modal') {
|
|
28
|
+
return (
|
|
29
|
+
<Modal
|
|
30
|
+
description={description}
|
|
31
|
+
footer={footer}
|
|
32
|
+
onDismiss={() => onOpenChange(false)}
|
|
33
|
+
testID={testID}
|
|
34
|
+
title={title}
|
|
35
|
+
visible={open}
|
|
36
|
+
>
|
|
37
|
+
{children}
|
|
38
|
+
</Modal>
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return (
|
|
43
|
+
<Drawer
|
|
44
|
+
description={description}
|
|
45
|
+
footer={footer}
|
|
46
|
+
onDismiss={() => onOpenChange(false)}
|
|
47
|
+
position={side}
|
|
48
|
+
testID={testID}
|
|
49
|
+
title={title}
|
|
50
|
+
visible={open}
|
|
51
|
+
>
|
|
52
|
+
{children}
|
|
53
|
+
</Drawer>
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// default: inline -> Panel
|
|
58
|
+
return (
|
|
59
|
+
<Panel
|
|
60
|
+
actions={actions}
|
|
61
|
+
compact={compact}
|
|
62
|
+
description={description}
|
|
63
|
+
footer={footer}
|
|
64
|
+
testID={testID}
|
|
65
|
+
title={title}
|
|
66
|
+
>
|
|
67
|
+
{children}
|
|
68
|
+
</Panel>
|
|
69
|
+
);
|
|
70
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type React from 'react';
|
|
2
|
+
|
|
3
|
+
export type ResponsivePanelSide = 'left' | 'right';
|
|
4
|
+
export type ResponsivePanelDesktopMode = 'inline' | 'floating';
|
|
5
|
+
export type ResponsivePanelMobileMode = 'drawer' | 'modal';
|
|
6
|
+
|
|
7
|
+
export interface ResponsivePanelProps {
|
|
8
|
+
title?: React.ReactNode;
|
|
9
|
+
description?: React.ReactNode;
|
|
10
|
+
actions?: React.ReactNode;
|
|
11
|
+
footer?: React.ReactNode;
|
|
12
|
+
children?: React.ReactNode;
|
|
13
|
+
open: boolean;
|
|
14
|
+
onOpenChange: (open: boolean) => void;
|
|
15
|
+
side?: ResponsivePanelSide;
|
|
16
|
+
desktopMode?: ResponsivePanelDesktopMode;
|
|
17
|
+
mobileMode?: ResponsivePanelMobileMode;
|
|
18
|
+
compact?: boolean;
|
|
19
|
+
testID?: string;
|
|
20
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { Box, Heading, Stack, Text } from '@ankhorage/surface';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
|
|
4
|
+
import type { SectionHeaderProps } from './types';
|
|
5
|
+
|
|
6
|
+
export function SectionHeader({
|
|
7
|
+
title,
|
|
8
|
+
description,
|
|
9
|
+
eyebrow,
|
|
10
|
+
actions,
|
|
11
|
+
testID,
|
|
12
|
+
}: SectionHeaderProps) {
|
|
13
|
+
return (
|
|
14
|
+
<Stack
|
|
15
|
+
align={{ base: 'flex-start', md: 'center' }}
|
|
16
|
+
direction={{ base: 'column', md: 'row' }}
|
|
17
|
+
gap="m"
|
|
18
|
+
justify="space-between"
|
|
19
|
+
testID={testID}
|
|
20
|
+
>
|
|
21
|
+
<Box flex={1}>
|
|
22
|
+
<Stack gap="xs">
|
|
23
|
+
{eyebrow ? (
|
|
24
|
+
<Text tone="muted" variant="caption" weight="semiBold">
|
|
25
|
+
{eyebrow}
|
|
26
|
+
</Text>
|
|
27
|
+
) : null}
|
|
28
|
+
<Heading level={3}>{title}</Heading>
|
|
29
|
+
{description ? (
|
|
30
|
+
<Text tone="muted" variant="bodySmall">
|
|
31
|
+
{description}
|
|
32
|
+
</Text>
|
|
33
|
+
) : null}
|
|
34
|
+
</Stack>
|
|
35
|
+
</Box>
|
|
36
|
+
{actions ? <Box>{actions}</Box> : null}
|
|
37
|
+
</Stack>
|
|
38
|
+
);
|
|
39
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { Box, Text } from '@ankhorage/surface';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
|
|
4
|
+
import { Card } from '../../components/card';
|
|
5
|
+
import type { SettingsRowProps } from './types';
|
|
6
|
+
|
|
7
|
+
export function SettingsRow({
|
|
8
|
+
title,
|
|
9
|
+
description,
|
|
10
|
+
meta,
|
|
11
|
+
control,
|
|
12
|
+
onPress,
|
|
13
|
+
disabled = false,
|
|
14
|
+
testID,
|
|
15
|
+
}: SettingsRowProps) {
|
|
16
|
+
// Prevent nested interactive elements:
|
|
17
|
+
// If a control is present (likely contains buttons), the row itself must not be clickable
|
|
18
|
+
const isInteractive = Boolean(onPress) && !control;
|
|
19
|
+
|
|
20
|
+
return (
|
|
21
|
+
<Card
|
|
22
|
+
compact
|
|
23
|
+
actions={control}
|
|
24
|
+
description={description}
|
|
25
|
+
disabled={disabled}
|
|
26
|
+
onPress={isInteractive ? onPress : undefined}
|
|
27
|
+
testID={testID}
|
|
28
|
+
title={title}
|
|
29
|
+
tone="subtle"
|
|
30
|
+
>
|
|
31
|
+
{meta ? (
|
|
32
|
+
<Box pt="xs">
|
|
33
|
+
<Text tone="muted" variant="caption">
|
|
34
|
+
{meta}
|
|
35
|
+
</Text>
|
|
36
|
+
</Box>
|
|
37
|
+
) : null}
|
|
38
|
+
</Card>
|
|
39
|
+
);
|
|
40
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type React from 'react';
|
|
2
|
+
|
|
3
|
+
interface SettingsRowBaseProps {
|
|
4
|
+
title: React.ReactNode;
|
|
5
|
+
description?: React.ReactNode;
|
|
6
|
+
meta?: React.ReactNode;
|
|
7
|
+
disabled?: boolean;
|
|
8
|
+
testID?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
interface SettingsRowPressableProps {
|
|
12
|
+
onPress: () => void;
|
|
13
|
+
control?: never;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface SettingsRowControlledProps {
|
|
17
|
+
control: React.ReactNode;
|
|
18
|
+
onPress?: never;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
interface SettingsRowStaticProps {
|
|
22
|
+
control?: never;
|
|
23
|
+
onPress?: never;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export type SettingsRowProps = SettingsRowBaseProps &
|
|
27
|
+
(SettingsRowPressableProps | SettingsRowControlledProps | SettingsRowStaticProps);
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { Switch } from '@ankhorage/surface';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
|
|
4
|
+
import { SettingsRow } from '../settings-row';
|
|
5
|
+
import type { SwitchFieldProps } from './types';
|
|
6
|
+
|
|
7
|
+
export function SwitchField({
|
|
8
|
+
label,
|
|
9
|
+
description,
|
|
10
|
+
value,
|
|
11
|
+
onValueChange,
|
|
12
|
+
disabled,
|
|
13
|
+
testID,
|
|
14
|
+
}: SwitchFieldProps) {
|
|
15
|
+
return (
|
|
16
|
+
<SettingsRow
|
|
17
|
+
title={label}
|
|
18
|
+
description={description}
|
|
19
|
+
disabled={disabled}
|
|
20
|
+
testID={testID}
|
|
21
|
+
control={<Switch checked={value} onCheckedChange={onValueChange} disabled={disabled} />}
|
|
22
|
+
/>
|
|
23
|
+
);
|
|
24
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { Box, Heading, Text, useTheme } from '@ankhorage/surface';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
|
|
4
|
+
import { Card } from '../../components/card';
|
|
5
|
+
import type { PaletteItemProps } from './types';
|
|
6
|
+
|
|
7
|
+
export function PaletteItem({
|
|
8
|
+
title,
|
|
9
|
+
description,
|
|
10
|
+
icon,
|
|
11
|
+
badge,
|
|
12
|
+
selected,
|
|
13
|
+
disabled,
|
|
14
|
+
onPress,
|
|
15
|
+
testID,
|
|
16
|
+
}: PaletteItemProps) {
|
|
17
|
+
const { theme } = useTheme();
|
|
18
|
+
|
|
19
|
+
return (
|
|
20
|
+
<Card
|
|
21
|
+
compact
|
|
22
|
+
disabled={disabled}
|
|
23
|
+
onPress={onPress}
|
|
24
|
+
testID={testID}
|
|
25
|
+
tone={selected ? 'default' : 'subtle'}
|
|
26
|
+
style={
|
|
27
|
+
selected
|
|
28
|
+
? {
|
|
29
|
+
borderColor: theme.colors.primary,
|
|
30
|
+
borderWidth: 2,
|
|
31
|
+
}
|
|
32
|
+
: undefined
|
|
33
|
+
}
|
|
34
|
+
>
|
|
35
|
+
<Box p="xs" style={{ alignItems: 'center' }}>
|
|
36
|
+
{icon ? <Box pb="s">{/* Icon spec here */}</Box> : null}
|
|
37
|
+
<Heading level={5} align="center">
|
|
38
|
+
{title}
|
|
39
|
+
</Heading>
|
|
40
|
+
{description ? (
|
|
41
|
+
<Text align="center" tone="muted" variant="caption">
|
|
42
|
+
{description}
|
|
43
|
+
</Text>
|
|
44
|
+
) : null}
|
|
45
|
+
{badge ? <Box pt="xs">{badge}</Box> : null}
|
|
46
|
+
</Box>
|
|
47
|
+
</Card>
|
|
48
|
+
);
|
|
49
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { Box } from '@ankhorage/surface';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
|
|
4
|
+
import type { TileGridProps } from './types';
|
|
5
|
+
|
|
6
|
+
export function TileGrid({
|
|
7
|
+
children,
|
|
8
|
+
columns = 'responsive',
|
|
9
|
+
compact = false,
|
|
10
|
+
testID,
|
|
11
|
+
}: TileGridProps) {
|
|
12
|
+
return (
|
|
13
|
+
<Box
|
|
14
|
+
testID={testID}
|
|
15
|
+
style={{
|
|
16
|
+
flexDirection: 'row',
|
|
17
|
+
flexWrap: 'wrap',
|
|
18
|
+
gap: compact ? 8 : 16,
|
|
19
|
+
}}
|
|
20
|
+
>
|
|
21
|
+
{React.Children.map(children, (child) => {
|
|
22
|
+
if (!child) return null;
|
|
23
|
+
|
|
24
|
+
return (
|
|
25
|
+
<Box
|
|
26
|
+
style={
|
|
27
|
+
columns === 'responsive'
|
|
28
|
+
? {
|
|
29
|
+
flexBasis: '30%',
|
|
30
|
+
flexGrow: 1,
|
|
31
|
+
minWidth: 120,
|
|
32
|
+
}
|
|
33
|
+
: {
|
|
34
|
+
width: `${100 / columns}%`,
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
>
|
|
38
|
+
{child}
|
|
39
|
+
</Box>
|
|
40
|
+
);
|
|
41
|
+
})}
|
|
42
|
+
</Box>
|
|
43
|
+
);
|
|
44
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { ButtonIconSpec } from '@ankhorage/surface';
|
|
2
|
+
import type { ReactNode } from 'react';
|
|
3
|
+
|
|
4
|
+
export interface TileGridProps {
|
|
5
|
+
children?: ReactNode;
|
|
6
|
+
columns?: number | 'responsive';
|
|
7
|
+
compact?: boolean;
|
|
8
|
+
testID?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface PaletteItemProps {
|
|
12
|
+
title: ReactNode;
|
|
13
|
+
description?: ReactNode;
|
|
14
|
+
icon?: ButtonIconSpec;
|
|
15
|
+
badge?: ReactNode;
|
|
16
|
+
selected?: boolean;
|
|
17
|
+
disabled?: boolean;
|
|
18
|
+
onPress?: () => void;
|
|
19
|
+
testID?: string;
|
|
20
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { Box, Stack } from '@ankhorage/surface';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
|
|
4
|
+
import { IconButton } from '../../components/icon-button';
|
|
5
|
+
import { SettingsRow } from '../settings-row';
|
|
6
|
+
import type { TreeItemNode, TreeItemRenderProps } from './types';
|
|
7
|
+
|
|
8
|
+
interface TreeItemProps<TId extends string = string> {
|
|
9
|
+
node: TreeItemNode<TId>;
|
|
10
|
+
depth: number;
|
|
11
|
+
selectedId?: TId;
|
|
12
|
+
expandedIds: readonly TId[];
|
|
13
|
+
onSelect?: (id: TId) => void;
|
|
14
|
+
onToggleExpand: (id: TId) => void;
|
|
15
|
+
renderItem?: (props: TreeItemRenderProps<TId>) => React.ReactNode;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function TreeItem<TId extends string = string>({
|
|
19
|
+
node,
|
|
20
|
+
depth,
|
|
21
|
+
selectedId,
|
|
22
|
+
expandedIds,
|
|
23
|
+
onSelect,
|
|
24
|
+
onToggleExpand,
|
|
25
|
+
renderItem,
|
|
26
|
+
}: TreeItemProps<TId>) {
|
|
27
|
+
const hasChildren = node.children !== undefined && node.children.length > 0;
|
|
28
|
+
const isExpanded = expandedIds.includes(node.id);
|
|
29
|
+
const isSelected = selectedId === node.id;
|
|
30
|
+
|
|
31
|
+
const renderContent = () => {
|
|
32
|
+
if (renderItem) {
|
|
33
|
+
return renderItem({
|
|
34
|
+
node,
|
|
35
|
+
depth,
|
|
36
|
+
selected: isSelected,
|
|
37
|
+
expanded: isExpanded,
|
|
38
|
+
hasChildren,
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return (
|
|
43
|
+
<SettingsRow
|
|
44
|
+
title={node.label}
|
|
45
|
+
control={
|
|
46
|
+
<Stack direction="row" gap="xs" align="center">
|
|
47
|
+
{node.actions}
|
|
48
|
+
{hasChildren ? (
|
|
49
|
+
<IconButton
|
|
50
|
+
icon={{ name: isExpanded ? 'chevron-down-outline' : 'chevron-forward-outline' }}
|
|
51
|
+
label={isExpanded ? 'Collapse' : 'Expand'}
|
|
52
|
+
onPress={() => onToggleExpand(node.id)}
|
|
53
|
+
size="s"
|
|
54
|
+
emphasis="ghost"
|
|
55
|
+
/>
|
|
56
|
+
) : null}
|
|
57
|
+
</Stack>
|
|
58
|
+
}
|
|
59
|
+
meta={node.meta}
|
|
60
|
+
disabled={node.disabled}
|
|
61
|
+
/>
|
|
62
|
+
);
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
return (
|
|
66
|
+
<Box>
|
|
67
|
+
<Box style={{ paddingLeft: depth * 16 }}>{renderContent()}</Box>
|
|
68
|
+
{hasChildren && isExpanded ? (
|
|
69
|
+
<Box>
|
|
70
|
+
{node.children?.map((child) => (
|
|
71
|
+
<TreeItem
|
|
72
|
+
key={child.id}
|
|
73
|
+
depth={depth + 1}
|
|
74
|
+
expandedIds={expandedIds}
|
|
75
|
+
node={child}
|
|
76
|
+
onSelect={onSelect}
|
|
77
|
+
onToggleExpand={onToggleExpand}
|
|
78
|
+
renderItem={renderItem}
|
|
79
|
+
selectedId={selectedId}
|
|
80
|
+
/>
|
|
81
|
+
))}
|
|
82
|
+
</Box>
|
|
83
|
+
) : null}
|
|
84
|
+
</Box>
|
|
85
|
+
);
|
|
86
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { Stack } from '@ankhorage/surface';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
|
|
4
|
+
import { TreeItem } from './TreeItem';
|
|
5
|
+
import type { TreeViewProps } from './types';
|
|
6
|
+
|
|
7
|
+
export function TreeView<TId extends string = string>({
|
|
8
|
+
nodes,
|
|
9
|
+
selectedId,
|
|
10
|
+
expandedIds: controlledExpandedIds,
|
|
11
|
+
defaultExpandedIds,
|
|
12
|
+
onSelect,
|
|
13
|
+
onExpandedChange,
|
|
14
|
+
renderItem,
|
|
15
|
+
testID,
|
|
16
|
+
}: TreeViewProps<TId>) {
|
|
17
|
+
const [internalExpandedIds, setInternalExpandedIds] = React.useState<readonly TId[]>(
|
|
18
|
+
defaultExpandedIds ?? [],
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
const isControlled = controlledExpandedIds !== undefined;
|
|
22
|
+
const expandedIds = isControlled ? controlledExpandedIds : internalExpandedIds;
|
|
23
|
+
|
|
24
|
+
const handleToggleExpand = (id: TId) => {
|
|
25
|
+
const isExpanded = expandedIds.includes(id);
|
|
26
|
+
const newIds = isExpanded ? expandedIds.filter((eid) => eid !== id) : [...expandedIds, id];
|
|
27
|
+
|
|
28
|
+
if (!isControlled) {
|
|
29
|
+
setInternalExpandedIds(newIds);
|
|
30
|
+
}
|
|
31
|
+
onExpandedChange?.(newIds);
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
return (
|
|
35
|
+
<Stack gap="none" testID={testID}>
|
|
36
|
+
{nodes.map((node) => (
|
|
37
|
+
<TreeItem
|
|
38
|
+
key={node.id}
|
|
39
|
+
depth={0}
|
|
40
|
+
expandedIds={expandedIds}
|
|
41
|
+
node={node}
|
|
42
|
+
onSelect={onSelect}
|
|
43
|
+
onToggleExpand={handleToggleExpand}
|
|
44
|
+
renderItem={renderItem}
|
|
45
|
+
selectedId={selectedId}
|
|
46
|
+
/>
|
|
47
|
+
))}
|
|
48
|
+
</Stack>
|
|
49
|
+
);
|
|
50
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { ButtonIconSpec } from '@ankhorage/surface';
|
|
2
|
+
import type { ReactNode } from 'react';
|
|
3
|
+
|
|
4
|
+
export interface TreeItemNode<TId extends string = string> {
|
|
5
|
+
id: TId;
|
|
6
|
+
label: ReactNode;
|
|
7
|
+
icon?: ButtonIconSpec;
|
|
8
|
+
children?: readonly TreeItemNode<TId>[];
|
|
9
|
+
disabled?: boolean;
|
|
10
|
+
meta?: ReactNode;
|
|
11
|
+
actions?: ReactNode;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface TreeItemRenderProps<TId extends string = string> {
|
|
15
|
+
node: TreeItemNode<TId>;
|
|
16
|
+
depth: number;
|
|
17
|
+
selected: boolean;
|
|
18
|
+
expanded: boolean;
|
|
19
|
+
hasChildren: boolean;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface TreeViewProps<TId extends string = string> {
|
|
23
|
+
nodes: readonly TreeItemNode<TId>[];
|
|
24
|
+
selectedId?: TId;
|
|
25
|
+
expandedIds?: readonly TId[];
|
|
26
|
+
defaultExpandedIds?: readonly TId[];
|
|
27
|
+
onSelect?: (id: TId) => void;
|
|
28
|
+
onExpandedChange?: (ids: readonly TId[]) => void;
|
|
29
|
+
renderItem?: (props: TreeItemRenderProps<TId>) => ReactNode;
|
|
30
|
+
testID?: string;
|
|
31
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { ResponsiveProvider, ThemeProvider } from '@ankhorage/surface';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
|
|
4
|
+
import { createZoraTheme, type ZoraThemeOverride } from './createZoraTheme';
|
|
5
|
+
|
|
6
|
+
export interface ZoraProviderProps {
|
|
7
|
+
children: React.ReactNode;
|
|
8
|
+
initialConfig?: ZoraThemeOverride;
|
|
9
|
+
initialMode?: 'light' | 'dark';
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function ZoraProvider({
|
|
13
|
+
children,
|
|
14
|
+
initialConfig,
|
|
15
|
+
initialMode = 'light',
|
|
16
|
+
}: ZoraProviderProps) {
|
|
17
|
+
return (
|
|
18
|
+
<ThemeProvider initialConfig={createZoraTheme(initialConfig)} initialMode={initialMode}>
|
|
19
|
+
<ResponsiveProvider>{children}</ResponsiveProvider>
|
|
20
|
+
</ThemeProvider>
|
|
21
|
+
);
|
|
22
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test';
|
|
2
|
+
|
|
3
|
+
import { createZoraTheme } from './createZoraTheme';
|
|
4
|
+
|
|
5
|
+
describe('createZoraTheme', () => {
|
|
6
|
+
test('keeps the default preset stable', () => {
|
|
7
|
+
const theme = createZoraTheme();
|
|
8
|
+
|
|
9
|
+
expect(theme.id).toBe('zora');
|
|
10
|
+
expect(theme.light.primaryColor).toBe('#0f766e');
|
|
11
|
+
expect(theme.dark.primaryColor).toBe('#2dd4bf');
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
test('merges overrides without dropping the preset identity', () => {
|
|
15
|
+
const theme = createZoraTheme({
|
|
16
|
+
light: {
|
|
17
|
+
primaryColor: '#1d4ed8',
|
|
18
|
+
},
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
expect(theme.id).toBe('zora');
|
|
22
|
+
expect(theme.light.primaryColor).toBe('#1d4ed8');
|
|
23
|
+
expect(theme.dark.primaryColor).toBe('#2dd4bf');
|
|
24
|
+
});
|
|
25
|
+
});
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { ThemeConfig } from '@ankhorage/surface';
|
|
2
|
+
|
|
3
|
+
import { deepMerge } from '../internal/deepMerge';
|
|
4
|
+
import { zoraTheme } from './zoraTheme';
|
|
5
|
+
|
|
6
|
+
export type ZoraThemeOverride = Partial<ThemeConfig>;
|
|
7
|
+
|
|
8
|
+
export function createZoraTheme(overrides: ZoraThemeOverride = {}): ThemeConfig {
|
|
9
|
+
return deepMerge(zoraTheme, overrides);
|
|
10
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export type { ZoraThemeOverride } from './createZoraTheme';
|
|
2
|
+
export { createZoraTheme } from './createZoraTheme';
|
|
3
|
+
export { useZoraTheme } from './useZoraTheme';
|
|
4
|
+
export type { ZoraProviderProps } from './ZoraProvider';
|
|
5
|
+
export { ZoraProvider } from './ZoraProvider';
|
|
6
|
+
export { zoraTheme } from './zoraTheme';
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { ThemeConfig } from '@ankhorage/surface';
|
|
2
|
+
|
|
3
|
+
export const zoraTheme: ThemeConfig = {
|
|
4
|
+
id: 'zora',
|
|
5
|
+
name: 'ZORA',
|
|
6
|
+
light: {
|
|
7
|
+
primaryColor: '#0f766e',
|
|
8
|
+
harmony: 'analogous',
|
|
9
|
+
systemTone: 'jewel',
|
|
10
|
+
},
|
|
11
|
+
dark: {
|
|
12
|
+
primaryColor: '#2dd4bf',
|
|
13
|
+
harmony: 'analogous',
|
|
14
|
+
systemTone: 'jewel',
|
|
15
|
+
},
|
|
16
|
+
};
|