@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,103 @@
|
|
|
1
|
+
import { Box, Stack, Text, useTheme } from '@ankhorage/surface';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
|
|
4
|
+
import { Button } from '../button';
|
|
5
|
+
import type { TabItem, TabsProps } from './types';
|
|
6
|
+
|
|
7
|
+
export function Tabs<TValue extends string = string>({
|
|
8
|
+
value,
|
|
9
|
+
items,
|
|
10
|
+
onValueChange,
|
|
11
|
+
variant = 'underline',
|
|
12
|
+
size = 'm',
|
|
13
|
+
disabled: tabsDisabled,
|
|
14
|
+
testID,
|
|
15
|
+
}: TabsProps<TValue>) {
|
|
16
|
+
const { theme } = useTheme();
|
|
17
|
+
|
|
18
|
+
const renderTab = (item: TabItem<TValue>) => {
|
|
19
|
+
const isActive = item.value === value;
|
|
20
|
+
const isDisabled = tabsDisabled ?? item.disabled;
|
|
21
|
+
|
|
22
|
+
if (variant === 'segmented') {
|
|
23
|
+
return (
|
|
24
|
+
<Button
|
|
25
|
+
key={item.value}
|
|
26
|
+
emphasis={isActive ? 'solid' : 'ghost'}
|
|
27
|
+
tone="neutral"
|
|
28
|
+
size={size}
|
|
29
|
+
disabled={isDisabled}
|
|
30
|
+
onPress={() => onValueChange(item.value)}
|
|
31
|
+
leadingIcon={item.icon}
|
|
32
|
+
testID={item.testID}
|
|
33
|
+
>
|
|
34
|
+
{item.label}
|
|
35
|
+
{item.badge}
|
|
36
|
+
</Button>
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (variant === 'pill') {
|
|
41
|
+
return (
|
|
42
|
+
<Button
|
|
43
|
+
key={item.value}
|
|
44
|
+
emphasis={isActive ? 'soft' : 'ghost'}
|
|
45
|
+
tone={isActive ? 'primary' : 'neutral'}
|
|
46
|
+
size={size}
|
|
47
|
+
disabled={isDisabled}
|
|
48
|
+
onPress={() => onValueChange(item.value)}
|
|
49
|
+
leadingIcon={item.icon}
|
|
50
|
+
testID={item.testID}
|
|
51
|
+
>
|
|
52
|
+
{item.label}
|
|
53
|
+
{item.badge}
|
|
54
|
+
</Button>
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Default: 'underline'
|
|
59
|
+
return (
|
|
60
|
+
<Box
|
|
61
|
+
key={item.value}
|
|
62
|
+
borderColor={isActive ? theme.colors.primary : 'transparent'}
|
|
63
|
+
pb="xs"
|
|
64
|
+
style={{
|
|
65
|
+
borderBottomWidth: 2,
|
|
66
|
+
}}
|
|
67
|
+
>
|
|
68
|
+
<Button
|
|
69
|
+
emphasis="ghost"
|
|
70
|
+
tone="neutral"
|
|
71
|
+
size={size}
|
|
72
|
+
disabled={isDisabled}
|
|
73
|
+
onPress={() => onValueChange(item.value)}
|
|
74
|
+
leadingIcon={item.icon}
|
|
75
|
+
testID={item.testID}
|
|
76
|
+
>
|
|
77
|
+
<Text
|
|
78
|
+
color={isActive ? theme.colors.primary : undefined}
|
|
79
|
+
tone={isActive ? undefined : 'muted'}
|
|
80
|
+
weight={isActive ? 'semiBold' : 'regular'}
|
|
81
|
+
>
|
|
82
|
+
{item.label}
|
|
83
|
+
</Text>
|
|
84
|
+
{item.badge}
|
|
85
|
+
</Button>
|
|
86
|
+
</Box>
|
|
87
|
+
);
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
return (
|
|
91
|
+
<Stack
|
|
92
|
+
direction="row"
|
|
93
|
+
gap={variant === 'segmented' ? 'none' : 'm'}
|
|
94
|
+
align="center"
|
|
95
|
+
testID={testID}
|
|
96
|
+
p={variant === 'segmented' ? 'xxs' : 'none'}
|
|
97
|
+
bg={variant === 'segmented' ? 'subtle' : 'transparent'}
|
|
98
|
+
radius={variant === 'segmented' ? 'm' : 'none'}
|
|
99
|
+
>
|
|
100
|
+
{items.map(renderTab)}
|
|
101
|
+
</Stack>
|
|
102
|
+
);
|
|
103
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { ButtonIconSpec } from '@ankhorage/surface';
|
|
2
|
+
import type { ReactNode } from 'react';
|
|
3
|
+
|
|
4
|
+
import type { ZoraControlSize } from '../../internal/recipes';
|
|
5
|
+
|
|
6
|
+
export interface TabItem<TValue extends string = string> {
|
|
7
|
+
value: TValue;
|
|
8
|
+
label: ReactNode;
|
|
9
|
+
icon?: ButtonIconSpec;
|
|
10
|
+
badge?: ReactNode;
|
|
11
|
+
disabled?: boolean;
|
|
12
|
+
testID?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export type TabsVariant = 'underline' | 'pill' | 'segmented';
|
|
16
|
+
|
|
17
|
+
export interface TabsProps<TValue extends string = string> {
|
|
18
|
+
value: TValue;
|
|
19
|
+
items: readonly TabItem<TValue>[];
|
|
20
|
+
onValueChange: (value: TValue) => void;
|
|
21
|
+
variant?: TabsVariant;
|
|
22
|
+
size?: ZoraControlSize;
|
|
23
|
+
disabled?: boolean;
|
|
24
|
+
testID?: string;
|
|
25
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { Icon, Textarea as SurfaceTextarea, useTheme } from '@ankhorage/surface';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
|
|
4
|
+
import { resolveIconSize } from '../../internal/recipes';
|
|
5
|
+
import type { TextareaProps } from './types';
|
|
6
|
+
|
|
7
|
+
export function Textarea({ size = 'l', leadingIcon, trailingIcon, ...props }: TextareaProps) {
|
|
8
|
+
const { theme } = useTheme();
|
|
9
|
+
const iconSize = resolveIconSize(size);
|
|
10
|
+
const iconColor = theme.semantics.content.muted;
|
|
11
|
+
|
|
12
|
+
return (
|
|
13
|
+
<SurfaceTextarea
|
|
14
|
+
{...props}
|
|
15
|
+
leadingAccessory={
|
|
16
|
+
leadingIcon ? (
|
|
17
|
+
<Icon
|
|
18
|
+
color={iconColor}
|
|
19
|
+
name={leadingIcon.name}
|
|
20
|
+
provider={leadingIcon.provider}
|
|
21
|
+
size={iconSize}
|
|
22
|
+
/>
|
|
23
|
+
) : undefined
|
|
24
|
+
}
|
|
25
|
+
size={size}
|
|
26
|
+
trailingAccessory={
|
|
27
|
+
trailingIcon ? (
|
|
28
|
+
<Icon
|
|
29
|
+
color={iconColor}
|
|
30
|
+
name={trailingIcon.name}
|
|
31
|
+
provider={trailingIcon.provider}
|
|
32
|
+
size={iconSize}
|
|
33
|
+
/>
|
|
34
|
+
) : undefined
|
|
35
|
+
}
|
|
36
|
+
/>
|
|
37
|
+
);
|
|
38
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { ButtonIconSpec, TextareaProps as SurfaceTextareaProps } from '@ankhorage/surface';
|
|
2
|
+
|
|
3
|
+
import type { ZoraControlSize } from '../../internal/recipes';
|
|
4
|
+
|
|
5
|
+
export interface TextareaProps extends Omit<
|
|
6
|
+
SurfaceTextareaProps,
|
|
7
|
+
'leadingAccessory' | 'size' | 'trailingAccessory'
|
|
8
|
+
> {
|
|
9
|
+
size?: ZoraControlSize;
|
|
10
|
+
leadingIcon?: ButtonIconSpec;
|
|
11
|
+
trailingIcon?: ButtonIconSpec;
|
|
12
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { Stack } from '@ankhorage/surface';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
|
|
4
|
+
import { Card } from '../card';
|
|
5
|
+
import type { ToolbarProps } from './types';
|
|
6
|
+
|
|
7
|
+
export function Toolbar({
|
|
8
|
+
children,
|
|
9
|
+
position = 'inline',
|
|
10
|
+
floating = false,
|
|
11
|
+
compact = true,
|
|
12
|
+
testID,
|
|
13
|
+
}: ToolbarProps) {
|
|
14
|
+
const isFixed = position === 'top' || position === 'bottom';
|
|
15
|
+
|
|
16
|
+
return (
|
|
17
|
+
<Card
|
|
18
|
+
compact={compact}
|
|
19
|
+
tone={floating ? 'default' : 'subtle'}
|
|
20
|
+
testID={testID}
|
|
21
|
+
style={
|
|
22
|
+
isFixed
|
|
23
|
+
? {
|
|
24
|
+
position: 'absolute',
|
|
25
|
+
left: 0,
|
|
26
|
+
right: 0,
|
|
27
|
+
[position]: 0,
|
|
28
|
+
zIndex: 10,
|
|
29
|
+
}
|
|
30
|
+
: undefined
|
|
31
|
+
}
|
|
32
|
+
>
|
|
33
|
+
<Stack direction="row" gap="s" align="center">
|
|
34
|
+
{children}
|
|
35
|
+
</Stack>
|
|
36
|
+
</Card>
|
|
37
|
+
);
|
|
38
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
3
|
+
import { IconButton } from '../icon-button';
|
|
4
|
+
import type { ToolbarActionProps } from './types';
|
|
5
|
+
|
|
6
|
+
export function ToolbarAction({ active, ...props }: ToolbarActionProps) {
|
|
7
|
+
return (
|
|
8
|
+
<IconButton
|
|
9
|
+
{...props}
|
|
10
|
+
emphasis={active ? 'soft' : 'ghost'}
|
|
11
|
+
tone={active ? 'primary' : 'neutral'}
|
|
12
|
+
size="m"
|
|
13
|
+
/>
|
|
14
|
+
);
|
|
15
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { ButtonIconSpec } from '@ankhorage/surface';
|
|
2
|
+
import type React from 'react';
|
|
3
|
+
|
|
4
|
+
export type ToolbarPosition = 'top' | 'bottom' | 'inline';
|
|
5
|
+
|
|
6
|
+
export interface ToolbarProps {
|
|
7
|
+
children?: React.ReactNode;
|
|
8
|
+
position?: ToolbarPosition;
|
|
9
|
+
floating?: boolean;
|
|
10
|
+
compact?: boolean;
|
|
11
|
+
testID?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface ToolbarActionProps {
|
|
15
|
+
label: string;
|
|
16
|
+
icon: ButtonIconSpec;
|
|
17
|
+
active?: boolean;
|
|
18
|
+
disabled?: boolean;
|
|
19
|
+
onPress?: () => void;
|
|
20
|
+
testID?: string;
|
|
21
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
export type { BadgeProps } from './components/badge';
|
|
2
|
+
export { Badge } from './components/badge';
|
|
3
|
+
export type { ButtonProps } from './components/button';
|
|
4
|
+
export { Button } from './components/button';
|
|
5
|
+
export type { CardProps } from './components/card';
|
|
6
|
+
export { Card } from './components/card';
|
|
7
|
+
export type { DrawerProps } from './components/drawer';
|
|
8
|
+
export { Drawer } from './components/drawer';
|
|
9
|
+
export type { IconProps } from './components/icon';
|
|
10
|
+
export { Icon } from './components/icon';
|
|
11
|
+
export type { IconButtonProps } from './components/icon-button';
|
|
12
|
+
export { IconButton } from './components/icon-button';
|
|
13
|
+
export type { InputProps } from './components/input';
|
|
14
|
+
export { Input } from './components/input';
|
|
15
|
+
export type { ModalProps } from './components/modal';
|
|
16
|
+
export { Modal } from './components/modal';
|
|
17
|
+
export type { SelectOption, SelectProps } from './components/select';
|
|
18
|
+
export { Select } from './components/select';
|
|
19
|
+
export type { TabItem, TabsProps } from './components/tabs';
|
|
20
|
+
export { Tabs } from './components/tabs';
|
|
21
|
+
export type { TextareaProps } from './components/textarea';
|
|
22
|
+
export { Textarea } from './components/textarea';
|
|
23
|
+
export type { ToolbarActionProps, ToolbarProps } from './components/toolbar';
|
|
24
|
+
export { Toolbar, ToolbarAction } from './components/toolbar';
|
|
25
|
+
export type { AppShellProps } from './layout/app-shell';
|
|
26
|
+
export { AppShell } from './layout/app-shell';
|
|
27
|
+
export type { AuthLayoutProps } from './layout/auth-layout';
|
|
28
|
+
export { AuthLayout } from './layout/auth-layout';
|
|
29
|
+
export type { PageProps } from './layout/page';
|
|
30
|
+
export { Page } from './layout/page';
|
|
31
|
+
export type { PageHeaderProps } from './layout/page-header';
|
|
32
|
+
export { PageHeader } from './layout/page-header';
|
|
33
|
+
export type { PageSectionProps } from './layout/page-section';
|
|
34
|
+
export { PageSection } from './layout/page-section';
|
|
35
|
+
export type { SettingsLayoutProps } from './layout/settings-layout';
|
|
36
|
+
export { SettingsLayout } from './layout/settings-layout';
|
|
37
|
+
export type { SidebarLayoutProps } from './layout/sidebar-layout';
|
|
38
|
+
export { SidebarLayout } from './layout/sidebar-layout';
|
|
39
|
+
export type { TopbarLayoutProps } from './layout/topbar-layout';
|
|
40
|
+
export { TopbarLayout } from './layout/topbar-layout';
|
|
41
|
+
export type {
|
|
42
|
+
CollectionEditorProps,
|
|
43
|
+
CollectionEditorRenderItemProps,
|
|
44
|
+
} from './patterns/collection-editor';
|
|
45
|
+
export { CollectionEditor } from './patterns/collection-editor';
|
|
46
|
+
export type { ConfirmDialogProps } from './patterns/confirm-dialog';
|
|
47
|
+
export { ConfirmDialog } from './patterns/confirm-dialog';
|
|
48
|
+
export type { DisclosureSectionProps } from './patterns/disclosure-section';
|
|
49
|
+
export { DisclosureSection } from './patterns/disclosure-section';
|
|
50
|
+
export type { EmptyStateAction, EmptyStateProps } from './patterns/empty-state';
|
|
51
|
+
export { EmptyState } from './patterns/empty-state';
|
|
52
|
+
export type { FormFieldProps } from './patterns/form-field';
|
|
53
|
+
export { FormField } from './patterns/form-field';
|
|
54
|
+
export type { InspectorFieldProps } from './patterns/inspector-field';
|
|
55
|
+
export { InspectorField } from './patterns/inspector-field';
|
|
56
|
+
export type { NoticeProps } from './patterns/notice';
|
|
57
|
+
export { Notice } from './patterns/notice';
|
|
58
|
+
export type { PanelProps } from './patterns/panel';
|
|
59
|
+
export { Panel } from './patterns/panel';
|
|
60
|
+
export type { ResponsivePanelProps } from './patterns/responsive-panel';
|
|
61
|
+
export { ResponsivePanel } from './patterns/responsive-panel';
|
|
62
|
+
export type { SectionHeaderProps } from './patterns/section-header';
|
|
63
|
+
export { SectionHeader } from './patterns/section-header';
|
|
64
|
+
export type { SettingsRowProps } from './patterns/settings-row';
|
|
65
|
+
export { SettingsRow } from './patterns/settings-row';
|
|
66
|
+
export type { SwitchFieldProps } from './patterns/switch-field';
|
|
67
|
+
export { SwitchField } from './patterns/switch-field';
|
|
68
|
+
export type { PaletteItemProps, TileGridProps } from './patterns/tile-grid';
|
|
69
|
+
export { PaletteItem, TileGrid } from './patterns/tile-grid';
|
|
70
|
+
export type { TreeItemNode, TreeItemRenderProps, TreeViewProps } from './patterns/tree-view';
|
|
71
|
+
export { TreeItem, TreeView } from './patterns/tree-view';
|
|
72
|
+
export * from './theme';
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export function deepMerge<T extends object>(target: T, source: Partial<T>): T {
|
|
2
|
+
const result = { ...target };
|
|
3
|
+
|
|
4
|
+
(Object.keys(source) as (keyof T)[]).forEach((key) => {
|
|
5
|
+
const sourceValue = source[key];
|
|
6
|
+
const targetValue = target[key];
|
|
7
|
+
|
|
8
|
+
if (
|
|
9
|
+
sourceValue &&
|
|
10
|
+
typeof sourceValue === 'object' &&
|
|
11
|
+
!Array.isArray(sourceValue) &&
|
|
12
|
+
targetValue &&
|
|
13
|
+
typeof targetValue === 'object' &&
|
|
14
|
+
!Array.isArray(targetValue)
|
|
15
|
+
) {
|
|
16
|
+
result[key] = deepMerge(targetValue as object, sourceValue as object) as T[keyof T];
|
|
17
|
+
} else if (sourceValue !== undefined) {
|
|
18
|
+
result[key] = sourceValue as T[keyof T];
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
return result;
|
|
23
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
resolveBadgeRecipe,
|
|
5
|
+
resolveButtonRecipe,
|
|
6
|
+
resolveCardVariant,
|
|
7
|
+
resolveDialogWidth,
|
|
8
|
+
resolveIconSize,
|
|
9
|
+
resolvePageMaxWidth,
|
|
10
|
+
} from './recipes';
|
|
11
|
+
|
|
12
|
+
describe('recipes', () => {
|
|
13
|
+
test('maps button defaults to a more opinionated large solid primary button', () => {
|
|
14
|
+
expect(resolveButtonRecipe({})).toEqual({
|
|
15
|
+
size: 'l',
|
|
16
|
+
tone: 'primary',
|
|
17
|
+
variant: 'solid',
|
|
18
|
+
});
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test('maps badge defaults to a soft medium primary badge', () => {
|
|
22
|
+
expect(resolveBadgeRecipe({})).toEqual({
|
|
23
|
+
size: 'm',
|
|
24
|
+
tone: 'primary',
|
|
25
|
+
variant: 'soft',
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test('resolves elevated cards by default', () => {
|
|
30
|
+
expect(resolveCardVariant()).toBe('raised');
|
|
31
|
+
expect(resolveCardVariant('outline')).toBe('outline');
|
|
32
|
+
expect(resolveCardVariant('subtle')).toBe('subtle');
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test('keeps width presets stable for layouts and dialogs', () => {
|
|
36
|
+
expect(resolveDialogWidth('narrow')).toBeLessThan(resolveDialogWidth('default'));
|
|
37
|
+
expect(resolveDialogWidth('wide')).toBeGreaterThanOrEqual(resolveDialogWidth('default'));
|
|
38
|
+
expect(resolvePageMaxWidth('wide')).toBeGreaterThan(resolvePageMaxWidth('default'));
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test('keeps icon sizes aligned with control sizes', () => {
|
|
42
|
+
expect(resolveIconSize('s')).toBe(16);
|
|
43
|
+
expect(resolveIconSize('m')).toBe(18);
|
|
44
|
+
expect(resolveIconSize('l')).toBe(20);
|
|
45
|
+
});
|
|
46
|
+
});
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
BadgeProps as SurfaceBadgeProps,
|
|
3
|
+
ButtonProps as SurfaceButtonProps,
|
|
4
|
+
CardProps as SurfaceCardProps,
|
|
5
|
+
} from '@ankhorage/surface';
|
|
6
|
+
|
|
7
|
+
export type ZoraTone = NonNullable<SurfaceButtonProps['tone']>;
|
|
8
|
+
export type ZoraEmphasis = NonNullable<SurfaceButtonProps['variant']>;
|
|
9
|
+
export type ZoraControlSize = NonNullable<SurfaceButtonProps['size']>;
|
|
10
|
+
export type ZoraBadgeEmphasis = NonNullable<SurfaceBadgeProps['variant']>;
|
|
11
|
+
export type ZoraCardTone = 'default' | 'subtle' | 'outline';
|
|
12
|
+
export type ZoraContentWidth = 'narrow' | 'default' | 'wide';
|
|
13
|
+
|
|
14
|
+
export function resolveButtonRecipe({
|
|
15
|
+
tone = 'primary',
|
|
16
|
+
emphasis = 'solid',
|
|
17
|
+
size = 'l',
|
|
18
|
+
}: {
|
|
19
|
+
tone?: ZoraTone;
|
|
20
|
+
emphasis?: ZoraEmphasis;
|
|
21
|
+
size?: ZoraControlSize;
|
|
22
|
+
}): Pick<SurfaceButtonProps, 'size' | 'tone' | 'variant'> {
|
|
23
|
+
return {
|
|
24
|
+
size,
|
|
25
|
+
tone,
|
|
26
|
+
variant: emphasis,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function resolveBadgeRecipe({
|
|
31
|
+
tone = 'primary',
|
|
32
|
+
emphasis = 'soft',
|
|
33
|
+
size = 'm',
|
|
34
|
+
}: {
|
|
35
|
+
tone?: ZoraTone;
|
|
36
|
+
emphasis?: ZoraBadgeEmphasis;
|
|
37
|
+
size?: ZoraControlSize;
|
|
38
|
+
}): Pick<SurfaceBadgeProps, 'size' | 'tone' | 'variant'> {
|
|
39
|
+
return {
|
|
40
|
+
size,
|
|
41
|
+
tone,
|
|
42
|
+
variant: emphasis,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function resolveCardVariant(tone: ZoraCardTone = 'default'): SurfaceCardProps['variant'] {
|
|
47
|
+
switch (tone) {
|
|
48
|
+
case 'outline':
|
|
49
|
+
return 'outline';
|
|
50
|
+
case 'subtle':
|
|
51
|
+
return 'subtle';
|
|
52
|
+
case 'default':
|
|
53
|
+
default:
|
|
54
|
+
return 'raised';
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function resolveDialogWidth(width: ZoraContentWidth = 'default'): number {
|
|
59
|
+
switch (width) {
|
|
60
|
+
case 'narrow':
|
|
61
|
+
return 420;
|
|
62
|
+
case 'wide':
|
|
63
|
+
return 560;
|
|
64
|
+
case 'default':
|
|
65
|
+
default:
|
|
66
|
+
return 520;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function resolvePageMaxWidth(width: ZoraContentWidth = 'default'): number {
|
|
71
|
+
switch (width) {
|
|
72
|
+
case 'narrow':
|
|
73
|
+
return 760;
|
|
74
|
+
case 'wide':
|
|
75
|
+
return 1280;
|
|
76
|
+
case 'default':
|
|
77
|
+
default:
|
|
78
|
+
return 1040;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function resolveIconSize(size: ZoraControlSize = 'l'): number {
|
|
83
|
+
switch (size) {
|
|
84
|
+
case 's':
|
|
85
|
+
return 16;
|
|
86
|
+
case 'm':
|
|
87
|
+
return 18;
|
|
88
|
+
case 'l':
|
|
89
|
+
default:
|
|
90
|
+
return 20;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { Box, Stack } from '@ankhorage/surface';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
|
|
4
|
+
import type { AppShellProps } from './types';
|
|
5
|
+
|
|
6
|
+
export function AppShell({ children, topbar, testID }: AppShellProps) {
|
|
7
|
+
return (
|
|
8
|
+
<Box bg="background" flex={1} testID={testID}>
|
|
9
|
+
<Stack gap="none">
|
|
10
|
+
{topbar ? <Box>{topbar}</Box> : null}
|
|
11
|
+
<Box flex={1}>{children}</Box>
|
|
12
|
+
</Stack>
|
|
13
|
+
</Box>
|
|
14
|
+
);
|
|
15
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { Center, Stack } from '@ankhorage/surface';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
|
|
4
|
+
import { Card } from '../../components/card';
|
|
5
|
+
import type { AuthLayoutProps } from './types';
|
|
6
|
+
|
|
7
|
+
export function AuthLayout({
|
|
8
|
+
title,
|
|
9
|
+
description,
|
|
10
|
+
eyebrow,
|
|
11
|
+
children,
|
|
12
|
+
footer,
|
|
13
|
+
testID,
|
|
14
|
+
}: AuthLayoutProps) {
|
|
15
|
+
return (
|
|
16
|
+
<Center py="xl" testID={testID}>
|
|
17
|
+
<Card
|
|
18
|
+
compact
|
|
19
|
+
description={description}
|
|
20
|
+
eyebrow={eyebrow}
|
|
21
|
+
footer={footer}
|
|
22
|
+
title={title}
|
|
23
|
+
tone="default"
|
|
24
|
+
>
|
|
25
|
+
<Stack gap="m">{children}</Stack>
|
|
26
|
+
</Card>
|
|
27
|
+
</Center>
|
|
28
|
+
);
|
|
29
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { Container, Stack } from '@ankhorage/surface';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
|
|
4
|
+
import { resolvePageMaxWidth } from '../../internal/recipes';
|
|
5
|
+
import type { PageProps } from './types';
|
|
6
|
+
|
|
7
|
+
export function Page({ children, header, footer, width = 'default', testID }: PageProps) {
|
|
8
|
+
return (
|
|
9
|
+
<Container maxWidth={resolvePageMaxWidth(width)} py="xl" testID={testID}>
|
|
10
|
+
<Stack gap="l">
|
|
11
|
+
{header}
|
|
12
|
+
{children}
|
|
13
|
+
{footer}
|
|
14
|
+
</Stack>
|
|
15
|
+
</Container>
|
|
16
|
+
);
|
|
17
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type React from 'react';
|
|
2
|
+
|
|
3
|
+
import type { ZoraContentWidth } from '../../internal/recipes';
|
|
4
|
+
|
|
5
|
+
export interface PageProps {
|
|
6
|
+
children?: React.ReactNode;
|
|
7
|
+
header?: React.ReactNode;
|
|
8
|
+
footer?: React.ReactNode;
|
|
9
|
+
width?: ZoraContentWidth;
|
|
10
|
+
testID?: string;
|
|
11
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { Box, Heading, Stack, Text } from '@ankhorage/surface';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
|
|
4
|
+
import type { PageHeaderProps } from './types';
|
|
5
|
+
|
|
6
|
+
export function PageHeader({
|
|
7
|
+
title,
|
|
8
|
+
description,
|
|
9
|
+
eyebrow,
|
|
10
|
+
actions,
|
|
11
|
+
meta,
|
|
12
|
+
testID,
|
|
13
|
+
}: PageHeaderProps) {
|
|
14
|
+
return (
|
|
15
|
+
<Stack
|
|
16
|
+
align={{ base: 'flex-start', md: 'center' }}
|
|
17
|
+
direction={{ base: 'column', md: 'row' }}
|
|
18
|
+
gap="l"
|
|
19
|
+
justify="space-between"
|
|
20
|
+
testID={testID}
|
|
21
|
+
>
|
|
22
|
+
<Box flex={1}>
|
|
23
|
+
<Stack gap="s">
|
|
24
|
+
{eyebrow ? (
|
|
25
|
+
<Text tone="muted" variant="caption" weight="semiBold">
|
|
26
|
+
{eyebrow}
|
|
27
|
+
</Text>
|
|
28
|
+
) : null}
|
|
29
|
+
<Heading level={1}>{title}</Heading>
|
|
30
|
+
{description ? (
|
|
31
|
+
<Text tone="muted" variant="body">
|
|
32
|
+
{description}
|
|
33
|
+
</Text>
|
|
34
|
+
) : null}
|
|
35
|
+
{meta ? <Box pt="xs">{meta}</Box> : null}
|
|
36
|
+
</Stack>
|
|
37
|
+
</Box>
|
|
38
|
+
{actions ? <Box>{actions}</Box> : null}
|
|
39
|
+
</Stack>
|
|
40
|
+
);
|
|
41
|
+
}
|