@ankhorage/zora 3.2.0 → 3.3.2

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 (107) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/dist/components/card/meta.d.ts +1 -1
  3. package/dist/foundation/meta.d.ts +4 -4
  4. package/dist/index.d.ts +5 -1
  5. package/dist/index.d.ts.map +1 -1
  6. package/dist/index.js +2 -0
  7. package/dist/index.js.map +1 -1
  8. package/dist/layout/screen/meta.d.ts +1 -1
  9. package/dist/layout/screen-section/meta.d.ts +1 -1
  10. package/dist/metadata/allowedChildren.d.ts +4 -3
  11. package/dist/metadata/allowedChildren.d.ts.map +1 -1
  12. package/dist/metadata/allowedChildren.js +14 -0
  13. package/dist/metadata/allowedChildren.js.map +1 -1
  14. package/dist/metadata/bindableComponentMeta.d.ts +2 -2
  15. package/dist/metadata/componentMeta.d.ts.map +1 -1
  16. package/dist/metadata/componentMeta.js +4 -0
  17. package/dist/metadata/componentMeta.js.map +1 -1
  18. package/dist/metadata/index.d.ts +1 -1
  19. package/dist/metadata/index.d.ts.map +1 -1
  20. package/dist/metadata/index.js.map +1 -1
  21. package/dist/metadata/types.d.ts +6 -0
  22. package/dist/metadata/types.d.ts.map +1 -1
  23. package/dist/metadata/types.js.map +1 -1
  24. package/dist/patterns/content-rail/ContentRail.d.ts +10 -0
  25. package/dist/patterns/content-rail/ContentRail.d.ts.map +1 -0
  26. package/dist/patterns/content-rail/ContentRail.js +175 -0
  27. package/dist/patterns/content-rail/ContentRail.js.map +1 -0
  28. package/dist/patterns/content-rail/index.d.ts +3 -0
  29. package/dist/patterns/content-rail/index.d.ts.map +1 -0
  30. package/dist/patterns/content-rail/index.js +2 -0
  31. package/dist/patterns/content-rail/index.js.map +1 -0
  32. package/dist/patterns/content-rail/meta.d.ts +143 -0
  33. package/dist/patterns/content-rail/meta.d.ts.map +1 -0
  34. package/dist/patterns/content-rail/meta.js +130 -0
  35. package/dist/patterns/content-rail/meta.js.map +1 -0
  36. package/dist/patterns/content-rail/resolveContentRailItemWidth.d.ts +10 -0
  37. package/dist/patterns/content-rail/resolveContentRailItemWidth.d.ts.map +1 -0
  38. package/dist/patterns/content-rail/resolveContentRailItemWidth.js +25 -0
  39. package/dist/patterns/content-rail/resolveContentRailItemWidth.js.map +1 -0
  40. package/dist/patterns/content-rail/resolveContentRailPhysicalOffset.d.ts +6 -0
  41. package/dist/patterns/content-rail/resolveContentRailPhysicalOffset.d.ts.map +1 -0
  42. package/dist/patterns/content-rail/resolveContentRailPhysicalOffset.js +5 -0
  43. package/dist/patterns/content-rail/resolveContentRailPhysicalOffset.js.map +1 -0
  44. package/dist/patterns/content-rail/resolveContentRailState.d.ts +18 -0
  45. package/dist/patterns/content-rail/resolveContentRailState.d.ts.map +1 -0
  46. package/dist/patterns/content-rail/resolveContentRailState.js +21 -0
  47. package/dist/patterns/content-rail/resolveContentRailState.js.map +1 -0
  48. package/dist/patterns/content-rail/types.d.ts +31 -0
  49. package/dist/patterns/content-rail/types.d.ts.map +1 -0
  50. package/dist/patterns/content-rail/types.js +2 -0
  51. package/dist/patterns/content-rail/types.js.map +1 -0
  52. package/dist/patterns/content-rail/useContentRailReducedMotion.d.ts +3 -0
  53. package/dist/patterns/content-rail/useContentRailReducedMotion.d.ts.map +1 -0
  54. package/dist/patterns/content-rail/useContentRailReducedMotion.js +21 -0
  55. package/dist/patterns/content-rail/useContentRailReducedMotion.js.map +1 -0
  56. package/dist/patterns/message-bubble/meta.d.ts +1 -1
  57. package/dist/patterns/missing-element/MissingElement.d.ts +10 -0
  58. package/dist/patterns/missing-element/MissingElement.d.ts.map +1 -0
  59. package/dist/patterns/missing-element/MissingElement.js +41 -0
  60. package/dist/patterns/missing-element/MissingElement.js.map +1 -0
  61. package/dist/patterns/missing-element/index.d.ts +3 -0
  62. package/dist/patterns/missing-element/index.d.ts.map +1 -0
  63. package/dist/patterns/missing-element/index.js +2 -0
  64. package/dist/patterns/missing-element/index.js.map +1 -0
  65. package/dist/patterns/missing-element/meta.d.ts +68 -0
  66. package/dist/patterns/missing-element/meta.d.ts.map +1 -0
  67. package/dist/patterns/missing-element/meta.js +59 -0
  68. package/dist/patterns/missing-element/meta.js.map +1 -0
  69. package/dist/patterns/missing-element/types.d.ts +9 -0
  70. package/dist/patterns/missing-element/types.d.ts.map +1 -0
  71. package/dist/patterns/missing-element/types.js +2 -0
  72. package/dist/patterns/missing-element/types.js.map +1 -0
  73. package/dist/patterns/notice/meta.d.ts +1 -1
  74. package/dist/patterns/panel/meta.d.ts +1 -1
  75. package/dist/patterns/post-card/meta.d.ts +1 -1
  76. package/dist/patterns/reader/meta.d.ts +2 -2
  77. package/dist/patterns/scanner/meta.d.ts +2 -2
  78. package/dist/registry.d.ts.map +1 -1
  79. package/dist/registry.js +4 -0
  80. package/dist/registry.js.map +1 -1
  81. package/package.json +3 -3
  82. package/src/expo57Migration.test.ts +16 -1
  83. package/src/index.ts +13 -0
  84. package/src/interactionPolicy.test.ts +22 -0
  85. package/src/metadata/allowedChildren.ts +15 -0
  86. package/src/metadata/componentMeta.test.ts +50 -0
  87. package/src/metadata/componentMeta.ts +4 -0
  88. package/src/metadata/index.ts +1 -0
  89. package/src/metadata/types.ts +7 -0
  90. package/src/patterns/content-rail/ContentRail.test.ts +44 -0
  91. package/src/patterns/content-rail/ContentRail.tsx +245 -0
  92. package/src/patterns/content-rail/index.ts +10 -0
  93. package/src/patterns/content-rail/meta.ts +133 -0
  94. package/src/patterns/content-rail/resolveContentRailItemWidth.test.ts +77 -0
  95. package/src/patterns/content-rail/resolveContentRailItemWidth.ts +39 -0
  96. package/src/patterns/content-rail/resolveContentRailPhysicalOffset.test.ts +29 -0
  97. package/src/patterns/content-rail/resolveContentRailPhysicalOffset.ts +12 -0
  98. package/src/patterns/content-rail/resolveContentRailState.test.ts +68 -0
  99. package/src/patterns/content-rail/resolveContentRailState.ts +36 -0
  100. package/src/patterns/content-rail/types.ts +35 -0
  101. package/src/patterns/content-rail/useContentRailReducedMotion.ts +28 -0
  102. package/src/patterns/missing-element/MissingElement.test.ts +66 -0
  103. package/src/patterns/missing-element/MissingElement.tsx +73 -0
  104. package/src/patterns/missing-element/index.ts +2 -0
  105. package/src/patterns/missing-element/meta.ts +62 -0
  106. package/src/patterns/missing-element/types.ts +9 -0
  107. package/src/registry.ts +4 -0
@@ -0,0 +1,77 @@
1
+ import { describe, expect, test } from 'bun:test';
2
+
3
+ import { resolveContentRailItemWidth } from './resolveContentRailItemWidth';
4
+
5
+ describe('resolveContentRailItemWidth', () => {
6
+ test('keeps fixed item sizes within the available viewport', () => {
7
+ expect(
8
+ resolveContentRailItemWidth({
9
+ gap: 16,
10
+ itemCount: 4,
11
+ itemSize: 'regular',
12
+ padding: 16,
13
+ peek: 32,
14
+ viewportWidth: 500,
15
+ }),
16
+ ).toBe(200);
17
+ expect(
18
+ resolveContentRailItemWidth({
19
+ gap: 16,
20
+ itemCount: 4,
21
+ itemSize: 'wide',
22
+ padding: 16,
23
+ peek: 32,
24
+ viewportWidth: 240,
25
+ }),
26
+ ).toBe(208);
27
+ });
28
+
29
+ test('reserves a deterministic partial-next affordance at responsive breakpoints', () => {
30
+ expect(
31
+ resolveContentRailItemWidth({
32
+ gap: 16,
33
+ itemCount: 5,
34
+ itemSize: 'responsive',
35
+ padding: 16,
36
+ peek: 32,
37
+ viewportWidth: 500,
38
+ }),
39
+ ).toBe(202);
40
+ expect(
41
+ resolveContentRailItemWidth({
42
+ gap: 16,
43
+ itemCount: 5,
44
+ itemSize: 'responsive',
45
+ padding: 16,
46
+ peek: 32,
47
+ viewportWidth: 800,
48
+ }),
49
+ ).toBe(229);
50
+ });
51
+
52
+ test('removes the peek and distributes items predictably when content fits', () => {
53
+ expect(
54
+ resolveContentRailItemWidth({
55
+ gap: 16,
56
+ itemCount: 2,
57
+ itemSize: 'responsive',
58
+ padding: 16,
59
+ peek: 32,
60
+ viewportWidth: 800,
61
+ }),
62
+ ).toBe(280);
63
+ });
64
+
65
+ test('handles empty and unmeasured rails', () => {
66
+ expect(
67
+ resolveContentRailItemWidth({
68
+ gap: 16,
69
+ itemCount: 0,
70
+ itemSize: 'responsive',
71
+ padding: 16,
72
+ peek: 32,
73
+ viewportWidth: 500,
74
+ }),
75
+ ).toBe(0);
76
+ });
77
+ });
@@ -0,0 +1,39 @@
1
+ import type { ContentRailItemSize } from './types';
2
+
3
+ export function resolveContentRailItemWidth({
4
+ gap,
5
+ itemCount,
6
+ itemSize,
7
+ padding,
8
+ peek,
9
+ viewportWidth,
10
+ }: {
11
+ gap: number;
12
+ itemCount: number;
13
+ itemSize: ContentRailItemSize;
14
+ padding: number;
15
+ peek: number;
16
+ viewportWidth: number;
17
+ }): number {
18
+ const availableWidth = Math.max(0, viewportWidth - padding * 2);
19
+ if (availableWidth === 0 || itemCount === 0) return 0;
20
+
21
+ const fixedWidth = resolveFixedItemWidth(itemSize);
22
+ if (fixedWidth !== undefined) return Math.min(fixedWidth, availableWidth);
23
+
24
+ const slots = viewportWidth >= 1024 ? 4 : viewportWidth >= 768 ? 3 : viewportWidth >= 480 ? 2 : 1;
25
+ const fullItems = Math.min(slots, itemCount);
26
+ const overflows = itemCount > fullItems;
27
+ const resolvedPeek = overflows ? Math.min(Math.max(0, peek), availableWidth / 2) : 0;
28
+ const gaps = overflows ? fullItems : Math.max(0, fullItems - 1);
29
+ const resolved = (availableWidth - resolvedPeek - gap * gaps) / fullItems;
30
+
31
+ return Math.min(280, Math.max(Math.min(96, availableWidth), Math.floor(resolved)));
32
+ }
33
+
34
+ function resolveFixedItemWidth(itemSize: ContentRailItemSize): number | undefined {
35
+ if (itemSize === 'compact') return 144;
36
+ if (itemSize === 'regular') return 200;
37
+ if (itemSize === 'wide') return 280;
38
+ return undefined;
39
+ }
@@ -0,0 +1,29 @@
1
+ import { describe, expect, test } from 'bun:test';
2
+
3
+ import { resolveContentRailPhysicalOffset } from './resolveContentRailPhysicalOffset';
4
+
5
+ describe('resolveContentRailPhysicalOffset', () => {
6
+ test('keeps logical and physical offsets aligned for LTR content', () => {
7
+ expect(
8
+ resolveContentRailPhysicalOffset({ isRtl: false, logicalOffset: 216, maxOffset: 648 }),
9
+ ).toBe(216);
10
+ });
11
+
12
+ test('inverts offsets for RTL while preserving logical boundaries', () => {
13
+ expect(
14
+ resolveContentRailPhysicalOffset({ isRtl: true, logicalOffset: 0, maxOffset: 648 }),
15
+ ).toBe(648);
16
+ expect(
17
+ resolveContentRailPhysicalOffset({ isRtl: true, logicalOffset: 216, maxOffset: 648 }),
18
+ ).toBe(432);
19
+ expect(
20
+ resolveContentRailPhysicalOffset({ isRtl: true, logicalOffset: 648, maxOffset: 648 }),
21
+ ).toBe(0);
22
+ });
23
+
24
+ test('clamps offsets rather than wrapping', () => {
25
+ expect(
26
+ resolveContentRailPhysicalOffset({ isRtl: true, logicalOffset: 900, maxOffset: 648 }),
27
+ ).toBe(0);
28
+ });
29
+ });
@@ -0,0 +1,12 @@
1
+ export function resolveContentRailPhysicalOffset({
2
+ isRtl,
3
+ logicalOffset,
4
+ maxOffset,
5
+ }: {
6
+ isRtl: boolean;
7
+ logicalOffset: number;
8
+ maxOffset: number;
9
+ }): number {
10
+ const clampedOffset = Math.min(Math.max(0, logicalOffset), Math.max(0, maxOffset));
11
+ return isRtl ? Math.max(0, maxOffset) - clampedOffset : clampedOffset;
12
+ }
@@ -0,0 +1,68 @@
1
+ import { describe, expect, test } from 'bun:test';
2
+
3
+ import { resolveContentRailState } from './resolveContentRailState';
4
+
5
+ describe('resolveContentRailState', () => {
6
+ test('resolves overflow, visible items, and the nearest navigation anchor', () => {
7
+ expect(
8
+ resolveContentRailState({
9
+ contentWidth: 848,
10
+ gap: 16,
11
+ itemCount: 4,
12
+ itemWidth: 200,
13
+ logicalOffset: 216,
14
+ viewportWidth: 432,
15
+ }),
16
+ ).toEqual({
17
+ anchorIndex: 1,
18
+ canGoNext: true,
19
+ canGoPrevious: true,
20
+ firstVisibleIndex: 1,
21
+ lastVisibleIndex: 2,
22
+ maxOffset: 416,
23
+ offset: 216,
24
+ stride: 216,
25
+ });
26
+ });
27
+
28
+ test('clamps boundaries without wrapping', () => {
29
+ const start = resolveContentRailState({
30
+ contentWidth: 848,
31
+ gap: 16,
32
+ itemCount: 4,
33
+ itemWidth: 200,
34
+ logicalOffset: -100,
35
+ viewportWidth: 432,
36
+ });
37
+ const end = resolveContentRailState({
38
+ contentWidth: 848,
39
+ gap: 16,
40
+ itemCount: 4,
41
+ itemWidth: 200,
42
+ logicalOffset: 1000,
43
+ viewportWidth: 432,
44
+ });
45
+
46
+ expect(start).toMatchObject({ offset: 0, canGoPrevious: false, canGoNext: true });
47
+ expect(end).toMatchObject({ offset: 416, canGoPrevious: true, canGoNext: false });
48
+ });
49
+
50
+ test('degrades to a non-overflowing empty range', () => {
51
+ expect(
52
+ resolveContentRailState({
53
+ contentWidth: 0,
54
+ gap: 16,
55
+ itemCount: 0,
56
+ itemWidth: 0,
57
+ logicalOffset: 0,
58
+ viewportWidth: 432,
59
+ }),
60
+ ).toMatchObject({
61
+ canGoNext: false,
62
+ canGoPrevious: false,
63
+ firstVisibleIndex: -1,
64
+ lastVisibleIndex: -1,
65
+ maxOffset: 0,
66
+ });
67
+ });
68
+ });
@@ -0,0 +1,36 @@
1
+ export function resolveContentRailState({
2
+ contentWidth,
3
+ gap,
4
+ itemCount,
5
+ itemWidth,
6
+ logicalOffset,
7
+ viewportWidth,
8
+ }: {
9
+ contentWidth: number;
10
+ gap: number;
11
+ itemCount: number;
12
+ itemWidth: number;
13
+ logicalOffset: number;
14
+ viewportWidth: number;
15
+ }) {
16
+ const maxOffset = Math.max(0, contentWidth - viewportWidth);
17
+ const offset = Math.min(Math.max(0, logicalOffset), maxOffset);
18
+ const stride = itemWidth + gap;
19
+ const firstVisibleIndex = stride > 0 ? Math.min(itemCount - 1, Math.floor(offset / stride)) : 0;
20
+ const lastVisibleIndex =
21
+ stride > 0
22
+ ? Math.min(itemCount - 1, Math.max(0, Math.ceil((offset + viewportWidth) / stride) - 1))
23
+ : 0;
24
+ const anchorIndex = stride > 0 ? Math.round(offset / stride) : 0;
25
+
26
+ return {
27
+ anchorIndex: Math.min(Math.max(0, anchorIndex), Math.max(0, itemCount - 1)),
28
+ canGoNext: itemCount > 0 && offset < maxOffset - 1,
29
+ canGoPrevious: itemCount > 0 && offset > 1,
30
+ firstVisibleIndex: itemCount === 0 ? -1 : firstVisibleIndex,
31
+ lastVisibleIndex: itemCount === 0 ? -1 : lastVisibleIndex,
32
+ maxOffset,
33
+ offset,
34
+ stride,
35
+ };
36
+ }
@@ -0,0 +1,35 @@
1
+ import type { ReactNode } from 'react';
2
+
3
+ import type { ZoraBaseProps } from '../../theme/ZoraBaseProps';
4
+
5
+ export type ContentRailDirection = 'auto' | 'ltr' | 'rtl';
6
+ export type ContentRailItemSize = 'compact' | 'regular' | 'wide' | 'responsive';
7
+ export type ContentRailMotion = 'system' | 'animated' | 'reduced';
8
+ export type ContentRailSpacing = 'none' | 'xs' | 's' | 'm' | 'l' | 'xl';
9
+
10
+ export interface ContentRailControlPressEvent {
11
+ direction: 'previous' | 'next';
12
+ targetIndex: number;
13
+ }
14
+
15
+ export interface ContentRailVisibleRangeChangeEvent {
16
+ firstVisibleIndex: number;
17
+ lastVisibleIndex: number;
18
+ itemCount: number;
19
+ }
20
+
21
+ export interface ContentRailProps extends ZoraBaseProps {
22
+ children?: ReactNode;
23
+ itemSize?: ContentRailItemSize;
24
+ gap?: ContentRailSpacing;
25
+ padding?: ContentRailSpacing;
26
+ peek?: number;
27
+ showControls?: boolean;
28
+ direction?: ContentRailDirection;
29
+ motion?: ContentRailMotion;
30
+ accessibilityLabel?: string;
31
+ previousLabel?: string;
32
+ nextLabel?: string;
33
+ onControlPress?: (event: ContentRailControlPressEvent) => void | Promise<void>;
34
+ onVisibleRangeChange?: (event: ContentRailVisibleRangeChangeEvent) => void | Promise<void>;
35
+ }
@@ -0,0 +1,28 @@
1
+ import React from 'react';
2
+ import { AccessibilityInfo } from 'react-native';
3
+
4
+ import type { ContentRailMotion } from './types';
5
+
6
+ export function useContentRailReducedMotion(motion: ContentRailMotion): boolean {
7
+ const [systemReducedMotion, setSystemReducedMotion] = React.useState(false);
8
+
9
+ React.useEffect(() => {
10
+ if (motion !== 'system') return;
11
+
12
+ let active = true;
13
+ void AccessibilityInfo.isReduceMotionEnabled().then((enabled) => {
14
+ if (active) setSystemReducedMotion(enabled);
15
+ });
16
+ const subscription = AccessibilityInfo.addEventListener(
17
+ 'reduceMotionChanged',
18
+ setSystemReducedMotion,
19
+ );
20
+
21
+ return () => {
22
+ active = false;
23
+ subscription.remove();
24
+ };
25
+ }, [motion]);
26
+
27
+ return motion === 'reduced' || (motion === 'system' && systemReducedMotion);
28
+ }
@@ -0,0 +1,66 @@
1
+ import { describe, expect, test } from 'bun:test';
2
+
3
+ import { ZORA_BINDABLE_COMPONENT_META, ZORA_COMPONENT_META } from '../../metadata';
4
+ import { missingElementMeta } from './meta';
5
+
6
+ describe('MissingElement draft contract', () => {
7
+ test('is a release-blocking manifest leaf distinct from completed states', () => {
8
+ expect(ZORA_COMPONENT_META.MissingElement).toBe(missingElementMeta);
9
+ expect(missingElementMeta.directManifestNode).toBe(true);
10
+ expect(missingElementMeta.allowedChildren).toEqual([]);
11
+ expect(missingElementMeta.manifestPolicy).toEqual({
12
+ kind: 'unresolved-element',
13
+ availability: 'draft-only',
14
+ releaseGate: 'blocked',
15
+ });
16
+ expect(ZORA_COMPONENT_META.Notice.manifestPolicy).toBeUndefined();
17
+ expect(ZORA_COMPONENT_META.EmptyState.manifestPolicy).toBeUndefined();
18
+ });
19
+
20
+ test('captures only stable serializable gap and layout data', () => {
21
+ expect(Object.keys(missingElementMeta.props)).toEqual([
22
+ 'requestedCapability',
23
+ 'reason',
24
+ 'evidenceId',
25
+ 'minimumWidth',
26
+ 'minimumHeight',
27
+ ]);
28
+ expect(missingElementMeta.blueprint.defaultProps).toEqual({
29
+ requestedCapability: 'Unresolved interface capability',
30
+ reason: 'No matching ZORA element is available.',
31
+ minimumWidth: 240,
32
+ minimumHeight: 160,
33
+ });
34
+ expect(() => JSON.stringify(missingElementMeta)).not.toThrow();
35
+ });
36
+
37
+ test('has no event, data-binding, runtime-requirement, or fallback contract', () => {
38
+ expect(missingElementMeta.events).toBeUndefined();
39
+ expect(missingElementMeta.requirements).toBeUndefined();
40
+ expect(ZORA_BINDABLE_COMPONENT_META).not.toHaveProperty('MissingElement');
41
+ });
42
+
43
+ test('visibly and accessibly identifies the gap while preserving its minimum dimensions', async () => {
44
+ const source = await Bun.file('src/patterns/missing-element/MissingElement.tsx').text();
45
+
46
+ expect(source).toContain('accessibilityLabel={accessibilityLabel}');
47
+ expect(source).toContain('accessibilityRole="text"');
48
+ expect(source).toContain('accessible');
49
+ expect(source).toContain('minHeight={minimumHeight}');
50
+ expect(source).toContain('minWidth={minimumWidth}');
51
+ expect(source).toContain('{requestedCapability}');
52
+ expect(source).toContain('{reason}');
53
+ });
54
+
55
+ test('accepts the canonical interaction policy without owning interactive behavior', async () => {
56
+ const [source, types] = await Promise.all([
57
+ Bun.file('src/patterns/missing-element/MissingElement.tsx').text(),
58
+ Bun.file('src/patterns/missing-element/types.ts').text(),
59
+ ]);
60
+
61
+ expect(types).toContain('MissingElementProps extends ZoraBaseProps');
62
+ expect(source).toContain('interactionPolicy: _interactionPolicy');
63
+ expect(source).not.toMatch(/onPress|onLongPress|onChange|onSubmit/);
64
+ expect(source).not.toMatch(/<Button|<IconButton|<Notice|<EmptyState/);
65
+ });
66
+ });
@@ -0,0 +1,73 @@
1
+ import React from 'react';
2
+ import { StyleSheet } from 'react-native';
3
+
4
+ import { Badge } from '../../components/badge';
5
+ import { Text } from '../../components/text';
6
+ import { Box, Stack } from '../../foundation';
7
+ import { useZoraTheme } from '../../theme/useZoraTheme';
8
+ import { withZoraThemeScope } from '../../theme/withZoraThemeScope';
9
+ import type { MissingElementProps } from './types';
10
+
11
+ /***
12
+ * Renders the visible, non-interactive body of a draft missing-element marker.
13
+ */
14
+ function MissingElementInner({
15
+ themeId: _themeId,
16
+ mode: _mode,
17
+ interactionPolicy: _interactionPolicy,
18
+ requestedCapability,
19
+ reason,
20
+ evidenceId,
21
+ minimumWidth,
22
+ minimumHeight,
23
+ testID,
24
+ }: MissingElementProps) {
25
+ const { theme } = useZoraTheme();
26
+ const accessibilityLabel = `Missing element: ${requestedCapability}. ${reason}`;
27
+
28
+ return (
29
+ <Box
30
+ accessibilityLabel={accessibilityLabel}
31
+ accessibilityRole="text"
32
+ accessible
33
+ bg={theme.semantics.warning.softBg}
34
+ borderColor={theme.semantics.warning.base}
35
+ borderWidth={1}
36
+ minHeight={minimumHeight}
37
+ minWidth={minimumWidth}
38
+ p="m"
39
+ radius="m"
40
+ style={styles.root}
41
+ testID={testID}
42
+ >
43
+ <Stack gap="xs">
44
+ <Badge color="warning">Missing element</Badge>
45
+ <Text variant="label" weight="semiBold">
46
+ {requestedCapability}
47
+ </Text>
48
+ <Text emphasis="muted" variant="bodySmall">
49
+ {reason}
50
+ </Text>
51
+ {evidenceId ? (
52
+ <Text emphasis="subtle" variant="caption">
53
+ Evidence: {evidenceId}
54
+ </Text>
55
+ ) : null}
56
+ </Stack>
57
+ </Box>
58
+ );
59
+ }
60
+
61
+ const styles = StyleSheet.create({
62
+ root: {
63
+ borderStyle: 'dashed',
64
+ },
65
+ });
66
+
67
+ /***
68
+ * Draft-only marker that preserves an unsupported manifest region without faking its capability.
69
+ *
70
+ * `MissingElement` is intentionally non-interactive and release-blocking in component metadata.
71
+ * Replace it with a released semantic ZORA element before publishing a production template.
72
+ */
73
+ export const MissingElement = withZoraThemeScope(MissingElementInner);
@@ -0,0 +1,2 @@
1
+ export { MissingElement } from './MissingElement';
2
+ export type { MissingElementProps } from './types';
@@ -0,0 +1,62 @@
1
+ import type { ZoraComponentMeta } from '../../metadata';
2
+
3
+ const instanceAuthoring = { authority: 'instance' } as const;
4
+
5
+ export const missingElementMeta = {
6
+ name: 'MissingElement',
7
+ category: 'pattern',
8
+ description:
9
+ 'Draft-only marker for an unresolved semantic ZORA capability that must be replaced before release.',
10
+ directManifestNode: true,
11
+ allowedChildren: [],
12
+ manifestPolicy: {
13
+ kind: 'unresolved-element',
14
+ availability: 'draft-only',
15
+ releaseGate: 'blocked',
16
+ },
17
+ blueprint: {
18
+ label: 'Missing element',
19
+ defaultProps: {
20
+ requestedCapability: 'Unresolved interface capability',
21
+ reason: 'No matching ZORA element is available.',
22
+ minimumWidth: 240,
23
+ minimumHeight: 160,
24
+ },
25
+ },
26
+ props: {
27
+ requestedCapability: {
28
+ type: 'string',
29
+ category: 'Gap',
30
+ label: 'Requested capability',
31
+ default: 'Unresolved interface capability',
32
+ authoring: instanceAuthoring,
33
+ },
34
+ reason: {
35
+ type: 'string',
36
+ category: 'Gap',
37
+ label: 'Reason',
38
+ default: 'No matching ZORA element is available.',
39
+ authoring: instanceAuthoring,
40
+ },
41
+ evidenceId: {
42
+ type: 'string',
43
+ category: 'Evidence',
44
+ label: 'Evidence ID',
45
+ authoring: instanceAuthoring,
46
+ },
47
+ minimumWidth: {
48
+ type: 'number',
49
+ category: 'Layout',
50
+ label: 'Minimum width',
51
+ default: 240,
52
+ authoring: instanceAuthoring,
53
+ },
54
+ minimumHeight: {
55
+ type: 'number',
56
+ category: 'Layout',
57
+ label: 'Minimum height',
58
+ default: 160,
59
+ authoring: instanceAuthoring,
60
+ },
61
+ },
62
+ } as const satisfies ZoraComponentMeta;
@@ -0,0 +1,9 @@
1
+ import type { ZoraBaseProps } from '../../theme/ZoraBaseProps';
2
+
3
+ export interface MissingElementProps extends ZoraBaseProps {
4
+ requestedCapability: string;
5
+ reason: string;
6
+ evidenceId?: string;
7
+ minimumWidth?: number;
8
+ minimumHeight?: number;
9
+ }
package/src/registry.ts CHANGED
@@ -72,6 +72,7 @@ import {
72
72
  import { ChatListItem } from './patterns/chat-list-item';
73
73
  import { CollectionEditor } from './patterns/collection-editor';
74
74
  import { ConfirmDialog } from './patterns/confirm-dialog';
75
+ import { ContentRail } from './patterns/content-rail';
75
76
  import { DisclosureSection } from './patterns/disclosure-section';
76
77
  import { EmptyState } from './patterns/empty-state';
77
78
  import { FilterBar } from './patterns/filter-bar';
@@ -81,6 +82,7 @@ import { ImageUploadField } from './patterns/image-upload-field';
81
82
  import { InspectorField } from './patterns/inspector-field';
82
83
  import { List, ListRow, ListSection } from './patterns/list';
83
84
  import { MessageBubble } from './patterns/message-bubble';
85
+ import { MissingElement } from './patterns/missing-element';
84
86
  import { Notice } from './patterns/notice';
85
87
  import { Panel } from './patterns/panel';
86
88
  import { PostCard } from './patterns/post-card';
@@ -206,6 +208,7 @@ const _ZORA_COMPONENT_REGISTRY = {
206
208
  ChatListItem,
207
209
  CollectionEditor,
208
210
  ConfirmDialog,
211
+ ContentRail,
209
212
  DisclosureSection,
210
213
  EmptyState,
211
214
  FilterBar,
@@ -217,6 +220,7 @@ const _ZORA_COMPONENT_REGISTRY = {
217
220
  ListRow,
218
221
  ListSection,
219
222
  MessageBubble,
223
+ MissingElement,
220
224
  Notice,
221
225
  Panel,
222
226
  PostCard,