@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.
Files changed (117) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/README.md +0 -90
  3. package/package.json +5 -1
  4. package/src/components/badge/Badge.tsx +19 -0
  5. package/src/components/badge/index.ts +2 -0
  6. package/src/components/badge/types.ts +14 -0
  7. package/src/components/button/Button.tsx +13 -0
  8. package/src/components/button/index.ts +2 -0
  9. package/src/components/button/types.ts +16 -0
  10. package/src/components/card/Card.tsx +65 -0
  11. package/src/components/card/index.ts +2 -0
  12. package/src/components/card/types.ts +15 -0
  13. package/src/components/drawer/Drawer.tsx +27 -0
  14. package/src/components/drawer/index.ts +2 -0
  15. package/src/components/drawer/types.ts +12 -0
  16. package/src/components/icon/Icon.tsx +8 -0
  17. package/src/components/icon/index.ts +2 -0
  18. package/src/components/icon-button/IconButton.tsx +27 -0
  19. package/src/components/icon-button/index.ts +2 -0
  20. package/src/components/icon-button/types.ts +15 -0
  21. package/src/components/input/Input.tsx +38 -0
  22. package/src/components/input/index.ts +2 -0
  23. package/src/components/input/types.ts +12 -0
  24. package/src/components/modal/Modal.tsx +37 -0
  25. package/src/components/modal/index.ts +2 -0
  26. package/src/components/modal/types.ts +15 -0
  27. package/src/components/select/Select.tsx +49 -0
  28. package/src/components/select/index.ts +2 -0
  29. package/src/components/select/types.ts +14 -0
  30. package/src/components/tabs/Tabs.tsx +103 -0
  31. package/src/components/tabs/index.ts +2 -0
  32. package/src/components/tabs/types.ts +25 -0
  33. package/src/components/textarea/Textarea.tsx +38 -0
  34. package/src/components/textarea/index.ts +2 -0
  35. package/src/components/textarea/types.ts +12 -0
  36. package/src/components/toolbar/Toolbar.tsx +38 -0
  37. package/src/components/toolbar/ToolbarAction.tsx +15 -0
  38. package/src/components/toolbar/index.ts +3 -0
  39. package/src/components/toolbar/types.ts +21 -0
  40. package/src/index.ts +72 -0
  41. package/src/internal/deepMerge.ts +23 -0
  42. package/src/internal/recipes.test.ts +46 -0
  43. package/src/internal/recipes.ts +92 -0
  44. package/src/layout/app-shell/AppShell.tsx +15 -0
  45. package/src/layout/app-shell/index.ts +2 -0
  46. package/src/layout/app-shell/types.ts +7 -0
  47. package/src/layout/auth-layout/AuthLayout.tsx +29 -0
  48. package/src/layout/auth-layout/index.ts +2 -0
  49. package/src/layout/auth-layout/types.ts +10 -0
  50. package/src/layout/page/Page.tsx +17 -0
  51. package/src/layout/page/index.ts +2 -0
  52. package/src/layout/page/types.ts +11 -0
  53. package/src/layout/page-header/PageHeader.tsx +41 -0
  54. package/src/layout/page-header/index.ts +2 -0
  55. package/src/layout/page-header/types.ts +10 -0
  56. package/src/layout/page-section/PageSection.tsx +14 -0
  57. package/src/layout/page-section/index.ts +2 -0
  58. package/src/layout/page-section/types.ts +9 -0
  59. package/src/layout/settings-layout/SettingsLayout.tsx +26 -0
  60. package/src/layout/settings-layout/index.ts +2 -0
  61. package/src/layout/settings-layout/types.ts +10 -0
  62. package/src/layout/sidebar-layout/SidebarLayout.tsx +23 -0
  63. package/src/layout/sidebar-layout/index.ts +2 -0
  64. package/src/layout/sidebar-layout/types.ts +10 -0
  65. package/src/layout/topbar-layout/TopbarLayout.tsx +14 -0
  66. package/src/layout/topbar-layout/index.ts +2 -0
  67. package/src/layout/topbar-layout/types.ts +8 -0
  68. package/src/patterns/collection-editor/CollectionEditor.tsx +100 -0
  69. package/src/patterns/collection-editor/index.ts +2 -0
  70. package/src/patterns/collection-editor/types.ts +25 -0
  71. package/src/patterns/confirm-dialog/ConfirmDialog.tsx +46 -0
  72. package/src/patterns/confirm-dialog/index.ts +2 -0
  73. package/src/patterns/confirm-dialog/types.ts +19 -0
  74. package/src/patterns/disclosure-section/DisclosureSection.tsx +61 -0
  75. package/src/patterns/disclosure-section/index.ts +2 -0
  76. package/src/patterns/disclosure-section/types.ts +15 -0
  77. package/src/patterns/empty-state/EmptyState.tsx +53 -0
  78. package/src/patterns/empty-state/index.ts +2 -0
  79. package/src/patterns/empty-state/types.ts +20 -0
  80. package/src/patterns/form-field/FormField.tsx +27 -0
  81. package/src/patterns/form-field/index.ts +2 -0
  82. package/src/patterns/form-field/types.ts +11 -0
  83. package/src/patterns/inspector-field/InspectorField.tsx +16 -0
  84. package/src/patterns/inspector-field/index.ts +2 -0
  85. package/src/patterns/inspector-field/types.ts +15 -0
  86. package/src/patterns/notice/Notice.tsx +30 -0
  87. package/src/patterns/notice/index.ts +2 -0
  88. package/src/patterns/notice/types.ts +12 -0
  89. package/src/patterns/panel/Panel.tsx +8 -0
  90. package/src/patterns/panel/index.ts +2 -0
  91. package/src/patterns/panel/types.ts +15 -0
  92. package/src/patterns/responsive-panel/ResponsivePanel.tsx +70 -0
  93. package/src/patterns/responsive-panel/index.ts +2 -0
  94. package/src/patterns/responsive-panel/types.ts +20 -0
  95. package/src/patterns/section-header/SectionHeader.tsx +39 -0
  96. package/src/patterns/section-header/index.ts +2 -0
  97. package/src/patterns/section-header/types.ts +9 -0
  98. package/src/patterns/settings-row/SettingsRow.tsx +40 -0
  99. package/src/patterns/settings-row/index.ts +2 -0
  100. package/src/patterns/settings-row/types.ts +27 -0
  101. package/src/patterns/switch-field/SwitchField.tsx +24 -0
  102. package/src/patterns/switch-field/index.ts +2 -0
  103. package/src/patterns/switch-field/types.ts +10 -0
  104. package/src/patterns/tile-grid/PaletteItem.tsx +49 -0
  105. package/src/patterns/tile-grid/TileGrid.tsx +44 -0
  106. package/src/patterns/tile-grid/index.ts +3 -0
  107. package/src/patterns/tile-grid/types.ts +20 -0
  108. package/src/patterns/tree-view/TreeItem.tsx +86 -0
  109. package/src/patterns/tree-view/TreeView.tsx +50 -0
  110. package/src/patterns/tree-view/index.ts +3 -0
  111. package/src/patterns/tree-view/types.ts +31 -0
  112. package/src/theme/ZoraProvider.tsx +22 -0
  113. package/src/theme/createZoraTheme.test.ts +25 -0
  114. package/src/theme/createZoraTheme.ts +10 -0
  115. package/src/theme/index.ts +6 -0
  116. package/src/theme/useZoraTheme.ts +5 -0
  117. 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,2 @@
1
+ export * from './Tabs';
2
+ export * from './types';
@@ -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,2 @@
1
+ export { Textarea } from './Textarea';
2
+ export type { TextareaProps } from './types';
@@ -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,3 @@
1
+ export * from './Toolbar';
2
+ export * from './ToolbarAction';
3
+ export * from './types';
@@ -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,2 @@
1
+ export { AppShell } from './AppShell';
2
+ export type { AppShellProps } from './types';
@@ -0,0 +1,7 @@
1
+ import type React from 'react';
2
+
3
+ export interface AppShellProps {
4
+ children?: React.ReactNode;
5
+ topbar?: React.ReactNode;
6
+ testID?: string;
7
+ }
@@ -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,2 @@
1
+ export { AuthLayout } from './AuthLayout';
2
+ export type { AuthLayoutProps } from './types';
@@ -0,0 +1,10 @@
1
+ import type React from 'react';
2
+
3
+ export interface AuthLayoutProps {
4
+ title?: React.ReactNode;
5
+ description?: React.ReactNode;
6
+ eyebrow?: React.ReactNode;
7
+ children?: React.ReactNode;
8
+ footer?: React.ReactNode;
9
+ testID?: string;
10
+ }
@@ -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,2 @@
1
+ export { Page } from './Page';
2
+ export type { PageProps } from './types';
@@ -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
+ }
@@ -0,0 +1,2 @@
1
+ export { PageHeader } from './PageHeader';
2
+ export type { PageHeaderProps } from './types';
@@ -0,0 +1,10 @@
1
+ import type React from 'react';
2
+
3
+ export interface PageHeaderProps {
4
+ title: React.ReactNode;
5
+ description?: React.ReactNode;
6
+ eyebrow?: React.ReactNode;
7
+ actions?: React.ReactNode;
8
+ meta?: React.ReactNode;
9
+ testID?: string;
10
+ }