@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,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,2 @@
1
+ export * from './ResponsivePanel';
2
+ export * from './types';
@@ -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,2 @@
1
+ export { SectionHeader } from './SectionHeader';
2
+ export type { SectionHeaderProps } from './types';
@@ -0,0 +1,9 @@
1
+ import type React from 'react';
2
+
3
+ export interface SectionHeaderProps {
4
+ title: React.ReactNode;
5
+ description?: React.ReactNode;
6
+ eyebrow?: React.ReactNode;
7
+ actions?: React.ReactNode;
8
+ testID?: string;
9
+ }
@@ -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,2 @@
1
+ export { SettingsRow } from './SettingsRow';
2
+ export type { SettingsRowProps } from './types';
@@ -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,2 @@
1
+ export * from './SwitchField';
2
+ export * from './types';
@@ -0,0 +1,10 @@
1
+ import type { ReactNode } from 'react';
2
+
3
+ export interface SwitchFieldProps {
4
+ label: ReactNode;
5
+ description?: ReactNode;
6
+ value: boolean;
7
+ onValueChange: (value: boolean) => void;
8
+ disabled?: boolean;
9
+ testID?: string;
10
+ }
@@ -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,3 @@
1
+ export * from './PaletteItem';
2
+ export * from './TileGrid';
3
+ export * from './types';
@@ -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,3 @@
1
+ export * from './TreeItem';
2
+ export * from './TreeView';
3
+ export * from './types';
@@ -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,5 @@
1
+ import { useTheme } from '@ankhorage/surface';
2
+
3
+ export function useZoraTheme() {
4
+ return useTheme();
5
+ }
@@ -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
+ };