@symbiote-native/slider 0.1.0

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 (109) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +90 -0
  3. package/build/angular/index.d.ts +3 -0
  4. package/build/angular/index.js +2 -0
  5. package/build/angular/slider/index.android.d.ts +9 -0
  6. package/build/angular/slider/index.android.js +71 -0
  7. package/build/angular/slider/index.d.ts +2 -0
  8. package/build/angular/slider/index.ios.d.ts +11 -0
  9. package/build/angular/slider/index.ios.js +72 -0
  10. package/build/angular/slider/index.js +3 -0
  11. package/build/angular/slider/shared.d.ts +84 -0
  12. package/build/angular/slider/shared.js +648 -0
  13. package/build/core/constants.d.ts +20 -0
  14. package/build/core/constants.js +40 -0
  15. package/build/core/index.d.ts +7 -0
  16. package/build/core/index.js +11 -0
  17. package/build/core/render-slider.d.ts +11 -0
  18. package/build/core/render-slider.js +66 -0
  19. package/build/core/render-steps-indicator.d.ts +28 -0
  20. package/build/core/render-steps-indicator.js +91 -0
  21. package/build/core/slider-props.d.ts +52 -0
  22. package/build/core/slider-props.js +6 -0
  23. package/build/core/slider-state.d.ts +15 -0
  24. package/build/core/slider-state.js +73 -0
  25. package/build/react/index.d.ts +4 -0
  26. package/build/react/index.js +7 -0
  27. package/build/react/slider/index.android.d.ts +1 -0
  28. package/build/react/slider/index.android.js +10 -0
  29. package/build/react/slider/index.d.ts +1 -0
  30. package/build/react/slider/index.ios.d.ts +1 -0
  31. package/build/react/slider/index.ios.js +11 -0
  32. package/build/react/slider/index.js +3 -0
  33. package/build/react/slider/shared.d.ts +10 -0
  34. package/build/react/slider/shared.js +175 -0
  35. package/build/register.d.ts +1 -0
  36. package/build/register.js +48 -0
  37. package/build/vue/index.d.ts +3 -0
  38. package/build/vue/index.js +8 -0
  39. package/build/vue/slider/index.android.d.ts +18 -0
  40. package/build/vue/slider/index.android.js +10 -0
  41. package/build/vue/slider/index.d.ts +2 -0
  42. package/build/vue/slider/index.ios.d.ts +18 -0
  43. package/build/vue/slider/index.ios.js +11 -0
  44. package/build/vue/slider/index.js +3 -0
  45. package/build/vue/slider/shared.d.ts +26 -0
  46. package/build/vue/slider/shared.js +254 -0
  47. package/build-ngc/angular/index.d.ts +3 -0
  48. package/build-ngc/angular/index.js +3 -0
  49. package/build-ngc/angular/index.js.map +1 -0
  50. package/build-ngc/angular/slider/index.android.d.ts +12 -0
  51. package/build-ngc/angular/slider/index.android.js +28 -0
  52. package/build-ngc/angular/slider/index.android.js.map +1 -0
  53. package/build-ngc/angular/slider/index.d.ts +2 -0
  54. package/build-ngc/angular/slider/index.ios.d.ts +14 -0
  55. package/build-ngc/angular/slider/index.ios.js +29 -0
  56. package/build-ngc/angular/slider/index.ios.js.map +1 -0
  57. package/build-ngc/angular/slider/index.js +4 -0
  58. package/build-ngc/angular/slider/index.js.map +1 -0
  59. package/build-ngc/angular/slider/shared.d.ts +87 -0
  60. package/build-ngc/angular/slider/shared.js +427 -0
  61. package/build-ngc/angular/slider/shared.js.map +1 -0
  62. package/build-ngc/core/constants.d.ts +20 -0
  63. package/build-ngc/core/constants.js +41 -0
  64. package/build-ngc/core/constants.js.map +1 -0
  65. package/build-ngc/core/index.d.ts +7 -0
  66. package/build-ngc/core/index.js +12 -0
  67. package/build-ngc/core/index.js.map +1 -0
  68. package/build-ngc/core/render-slider.d.ts +11 -0
  69. package/build-ngc/core/render-slider.js +67 -0
  70. package/build-ngc/core/render-slider.js.map +1 -0
  71. package/build-ngc/core/render-steps-indicator.d.ts +28 -0
  72. package/build-ngc/core/render-steps-indicator.js +92 -0
  73. package/build-ngc/core/render-steps-indicator.js.map +1 -0
  74. package/build-ngc/core/slider-props.d.ts +52 -0
  75. package/build-ngc/core/slider-props.js +7 -0
  76. package/build-ngc/core/slider-props.js.map +1 -0
  77. package/build-ngc/core/slider-state.d.ts +15 -0
  78. package/build-ngc/core/slider-state.js +74 -0
  79. package/build-ngc/core/slider-state.js.map +1 -0
  80. package/build-ngc/register.d.ts +1 -0
  81. package/build-ngc/register.js +49 -0
  82. package/build-ngc/register.js.map +1 -0
  83. package/package.json +116 -0
  84. package/src/angular/index.ts +4 -0
  85. package/src/angular/slider/index.android.ts +25 -0
  86. package/src/angular/slider/index.ios.ts +26 -0
  87. package/src/angular/slider/index.ts +5 -0
  88. package/src/angular/slider/shared.ts +364 -0
  89. package/src/angular/slider/slider.test.ts +260 -0
  90. package/src/core/constants.ts +47 -0
  91. package/src/core/index.ts +46 -0
  92. package/src/core/render-slider.ts +89 -0
  93. package/src/core/render-steps-indicator.ts +154 -0
  94. package/src/core/slider-props.ts +83 -0
  95. package/src/core/slider-state.ts +112 -0
  96. package/src/react/index.ts +11 -0
  97. package/src/react/slider/index.android.ts +13 -0
  98. package/src/react/slider/index.ios.ts +14 -0
  99. package/src/react/slider/index.ts +4 -0
  100. package/src/react/slider/shared.ts +332 -0
  101. package/src/react/slider/slider.test.tsx +203 -0
  102. package/src/register.test.tsx +89 -0
  103. package/src/register.ts +59 -0
  104. package/src/vue/index.ts +11 -0
  105. package/src/vue/slider/index.android.ts +13 -0
  106. package/src/vue/slider/index.ios.ts +14 -0
  107. package/src/vue/slider/index.ts +5 -0
  108. package/src/vue/slider/shared.ts +383 -0
  109. package/src/vue/slider/slider.test.ts +184 -0
@@ -0,0 +1,260 @@
1
+ // Co-located Angular-driven test (ADR 0025) for the @symbiote-native/slider Angular wrapper. The native
2
+ // RNCSlider carries no symbiote metadata — the engine DERIVES its events + tint processors from an
3
+ // injected codegen-shaped ViewConfig (the same shape the React/Vue adapters' slider.test inject,
4
+ // the same shape RN's ReactNativeViewConfigRegistry holds on a real host). We import the Slider
5
+ // component from '.' (NOT the package barrel) so the third-party native-spec side-effect
6
+ // (../register) never loads headless. Proves: the wrapper paints the raw RNCSlider leaf inside
7
+ // its centering View, folds value/disabled/limits faithfully to the library, forwards + processes
8
+ // the tints, and maps the native value/sliding/accessibility-action events onto the
9
+ // valueChange/slidingStart/slidingComplete/accessibilityAction outputs.
10
+
11
+ import '@angular/compiler';
12
+ import { Component, Input } from '@angular/core';
13
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
14
+ import { mount, unmount, setNativeViewConfigSource } from '@symbiote-native/angular';
15
+ import { clearGlobalStyles, registerStyles } from '@symbiote-native/engine';
16
+ import { installFabric, type IFakeNode } from '@symbiote-native/test-utils';
17
+ import { Slider } from '.';
18
+
19
+ const ROOT_TAG = 312;
20
+ const SLIDER_VIEW = 'RNCSlider';
21
+
22
+ const fakeColor = (value: unknown): string => `processed(${value})`;
23
+
24
+ // The codegen-shaped config the engine derives from: both value rails (bubbling), the two sliding
25
+ // rails + accessibility (direct), plain pass-through attributes, and the tint processors.
26
+ const RNC_SLIDER_VIEW_CONFIG = {
27
+ bubblingEventTypes: {
28
+ topChange: { phasedRegistrationNames: { bubbled: 'onChange', captured: 'onChangeCapture' } },
29
+ topRNCSliderValueChange: {
30
+ phasedRegistrationNames: {
31
+ bubbled: 'onRNCSliderValueChange',
32
+ captured: 'onRNCSliderValueChangeCapture',
33
+ },
34
+ },
35
+ },
36
+ directEventTypes: {
37
+ topRNCSliderSlidingStart: { registrationName: 'onRNCSliderSlidingStart' },
38
+ topRNCSliderSlidingComplete: { registrationName: 'onRNCSliderSlidingComplete' },
39
+ topRNCSliderAccessibilityAction: { registrationName: 'onRNCSliderAccessibilityAction' },
40
+ },
41
+ validAttributes: {
42
+ value: true,
43
+ minimumValue: true,
44
+ maximumValue: true,
45
+ step: true,
46
+ lowerLimit: true,
47
+ upperLimit: true,
48
+ inverted: true,
49
+ disabled: true,
50
+ minimumTrackTintColor: { process: fakeColor },
51
+ maximumTrackTintColor: { process: fakeColor },
52
+ thumbTintColor: { process: fakeColor },
53
+ },
54
+ };
55
+
56
+ const fabric = installFabric();
57
+ setNativeViewConfigSource(name => (name === SLIDER_VIEW ? RNC_SLIDER_VIEW_CONFIG : undefined));
58
+
59
+ const tick = (): Promise<void> => new Promise(resolve => setTimeout(resolve, 0));
60
+
61
+ let capturedHost: SliderHost | undefined;
62
+
63
+ @Component({
64
+ selector: 'symbiote-slider-host',
65
+ standalone: true,
66
+ imports: [Slider],
67
+ template: `
68
+ <Slider
69
+ [value]="value"
70
+ [minimumValue]="minimumValue"
71
+ [maximumValue]="maximumValue"
72
+ [step]="step"
73
+ [lowerLimit]="lowerLimit"
74
+ [upperLimit]="upperLimit"
75
+ [disabled]="disabled"
76
+ [minimumTrackTintColor]="minimumTrackTintColor"
77
+ [maximumTrackTintColor]="maximumTrackTintColor"
78
+ [thumbTintColor]="thumbTintColor"
79
+ [accessibilityState]="accessibilityState"
80
+ [renderStepNumber]="renderStepNumber"
81
+ (valueChange)="onValueChange($event)"
82
+ (slidingStart)="onSlidingStart($event)"
83
+ (slidingComplete)="onSlidingComplete($event)"
84
+ (accessibilityAction)="onAccessibilityAction($event)"
85
+ />
86
+ `,
87
+ })
88
+ class SliderHost {
89
+ @Input() value?: number;
90
+ @Input() minimumValue?: number;
91
+ @Input() maximumValue?: number;
92
+ @Input() step?: number;
93
+ @Input() lowerLimit?: number;
94
+ @Input() upperLimit?: number;
95
+ @Input() disabled?: boolean;
96
+ @Input() minimumTrackTintColor?: string;
97
+ @Input() maximumTrackTintColor?: string;
98
+ @Input() thumbTintColor?: string;
99
+ @Input() accessibilityState?: { disabled?: boolean };
100
+ @Input() renderStepNumber?: boolean;
101
+
102
+ onValueChange = vi.fn();
103
+ onSlidingStart = vi.fn();
104
+ onSlidingComplete = vi.fn();
105
+ onAccessibilityAction = vi.fn();
106
+
107
+ constructor() {
108
+ // Captures the live component instance so the test can drive its signals after mount.
109
+ // eslint-disable-next-line @typescript-eslint/no-this-alias
110
+ capturedHost = this;
111
+ }
112
+ }
113
+
114
+ beforeEach(() => {
115
+ capturedHost = undefined;
116
+ fabric.reset();
117
+ });
118
+ afterEach(() => unmount(ROOT_TAG));
119
+
120
+ function sliderNode(): IFakeNode {
121
+ const node = fabric.find(n => n.viewName === SLIDER_VIEW);
122
+ if (!node) throw new Error(`no ${SLIDER_VIEW} was created`);
123
+ return node;
124
+ }
125
+
126
+ async function mountSlider(initialProps: Record<string, unknown>): Promise<void> {
127
+ mount(ROOT_TAG, SliderHost, { initialProps });
128
+ await tick();
129
+ }
130
+
131
+ describe('Angular Slider wrapper', () => {
132
+ it('paints the raw RNCSlider leaf inside a centering wrapper View', async () => {
133
+ await mountSlider({ value: 0.5, minimumValue: 0, maximumValue: 1, step: 0.1 });
134
+ const props = sliderNode().props;
135
+ expect(props.value).toBe(0.5);
136
+ expect(props.minimumValue).toBe(0);
137
+ expect(props.maximumValue).toBe(1);
138
+ expect(props.step).toBe(0.1);
139
+ // The native leaf lives under a symbiote-view wrapper (RCTView), not at the root.
140
+ expect(fabric.find(n => n.viewName === 'RCTView')).toBeDefined();
141
+ });
142
+
143
+ it('defaults range + limits the way the library wrapper does', async () => {
144
+ await mountSlider({ value: 0.3 });
145
+ const props = sliderNode().props;
146
+ expect(props.minimumValue).toBe(0);
147
+ expect(props.maximumValue).toBe(1);
148
+ expect(props.step).toBe(0);
149
+ expect(props.lowerLimit).toBe(Number.MIN_SAFE_INTEGER);
150
+ expect(props.upperLimit).toBe(Number.MAX_SAFE_INTEGER);
151
+ expect(props.inverted).toBe(false);
152
+ expect(props.disabled).toBe(false);
153
+ });
154
+
155
+ it('sanitizes a falsy/NaN value to undefined (library passedValue quirk)', async () => {
156
+ await mountSlider({ value: 0 });
157
+ expect(sliderNode().props.value).toBeUndefined();
158
+ unmount(ROOT_TAG);
159
+ fabric.reset();
160
+ await mountSlider({ value: Number.NaN });
161
+ expect(sliderNode().props.value).toBeUndefined();
162
+ });
163
+
164
+ it('forwards tint props and runs them through the derived processor', async () => {
165
+ await mountSlider({
166
+ value: 0.2,
167
+ minimumTrackTintColor: '#ff0000',
168
+ maximumTrackTintColor: '#00ff00',
169
+ thumbTintColor: '#0000ff',
170
+ });
171
+ const props = sliderNode().props;
172
+ expect(props.minimumTrackTintColor).toBe('processed(#ff0000)');
173
+ expect(props.maximumTrackTintColor).toBe('processed(#00ff00)');
174
+ expect(props.thumbTintColor).toBe('processed(#0000ff)');
175
+ });
176
+
177
+ it('maps both native value rails onto the valueChange output', async () => {
178
+ await mountSlider({ value: 0.2 });
179
+ const node = sliderNode();
180
+ fabric.fireEvent(node.instanceHandle, 'topChange', { value: 0.7 });
181
+ expect(capturedHost?.onValueChange).toHaveBeenCalledWith(0.7);
182
+ fabric.fireEvent(node.instanceHandle, 'topRNCSliderValueChange', { value: 0.42 });
183
+ expect(capturedHost?.onValueChange).toHaveBeenCalledWith(0.42);
184
+ });
185
+
186
+ it('maps the direct sliding events onto their outputs', async () => {
187
+ await mountSlider({ value: 0.2 });
188
+ const node = sliderNode();
189
+ fabric.fireEvent(node.instanceHandle, 'topRNCSliderSlidingStart', { value: 0.1 });
190
+ fabric.fireEvent(node.instanceHandle, 'topRNCSliderSlidingComplete', { value: 0.9 });
191
+ expect(capturedHost?.onSlidingStart).toHaveBeenCalledWith(0.1);
192
+ expect(capturedHost?.onSlidingComplete).toHaveBeenCalledWith(0.9);
193
+ });
194
+
195
+ it('maps the direct accessibility-action event onto the accessibilityAction output', async () => {
196
+ await mountSlider({ value: 0.2 });
197
+ const node = sliderNode();
198
+ fabric.fireEvent(node.instanceHandle, 'topRNCSliderAccessibilityAction', {
199
+ actionName: 'increment',
200
+ });
201
+ expect(capturedHost?.onAccessibilityAction).toHaveBeenCalledTimes(1);
202
+ });
203
+
204
+ it('resolves disabled from accessibilityState when no explicit boolean', async () => {
205
+ await mountSlider({ value: 0.2, accessibilityState: { disabled: true } });
206
+ expect(sliderNode().props.disabled).toBe(true);
207
+ });
208
+
209
+ it('renders the step indicator when renderStepNumber is set', async () => {
210
+ await mountSlider({
211
+ value: 0.5,
212
+ minimumValue: 0,
213
+ maximumValue: 1,
214
+ step: 0.5,
215
+ renderStepNumber: true,
216
+ });
217
+ const container = fabric.find(n => n.props.testID === 'StepsIndicator-Container');
218
+ expect(container, 'a StepsIndicator container is painted').toBeDefined();
219
+ });
220
+
221
+ it('does NOT leak a JS callback onto the native node as a prop', async () => {
222
+ await mountSlider({ value: 0.2 });
223
+ expect(typeof sliderNode().props.onValueChange).not.toBe('function');
224
+ expect('valueChange' in sliderNode().props).toBe(false);
225
+ });
226
+ });
227
+
228
+ // Regression test for the anchor/class bug (angular-adapter skill): Slider is its own
229
+ // ANCHOR_HOST_COMPONENTS entry (renderer.ts) AND renders through DescriptorOutlet (itself also an
230
+ // anchor entry) — a class= on <Slider> resolves onto Slider's OWN anchor, two levels up from the
231
+ // real committed centering wrapper View the descriptor builds (see the "paints the raw RNCSlider
232
+ // leaf inside a centering wrapper View" test above), and needs its OWN anchorHostStyle merge (see
233
+ // shared.ts's `inputProps`). Mirrors the Angular adapter's pressable.test.ts "resolves a class="
234
+ // case.
235
+ @Component({
236
+ selector: 'symbiote-slider-class-host',
237
+ standalone: true,
238
+ imports: [Slider],
239
+ template: `<Slider class="card" [value]="0.2" />`,
240
+ })
241
+ class SliderClassHost {}
242
+
243
+ describe('Angular Slider anchor class= resolution', () => {
244
+ beforeEach(() => {
245
+ fabric.reset();
246
+ registerStyles({ card: { backgroundColor: 'red' } });
247
+ });
248
+ afterEach(() => {
249
+ unmount(ROOT_TAG);
250
+ clearGlobalStyles();
251
+ });
252
+
253
+ it('resolves a class= on the Slider use site onto the real committed centering wrapper View', async () => {
254
+ mount(ROOT_TAG, SliderClassHost);
255
+ await tick();
256
+
257
+ const node = fabric.find(n => n.props.backgroundColor === 'red');
258
+ expect(node, 'a real Fabric node carries the class-derived style').toBeDefined();
259
+ });
260
+ });
@@ -0,0 +1,47 @@
1
+ // Framework-agnostic constants for the @react-native-community/slider native view, mirrored
2
+ // from the library's own utils/constants.ts so every adapter resolves identical defaults.
3
+ // symbiote ships zero metadata for RNCSlider at runtime — the engine derives its events and
4
+ // color processors from the library's ViewConfig — but the JS-side defaults the library's
5
+ // React wrapper applies (value/limit/step folding) are pure and shared here instead.
6
+
7
+ // The Fabric view name the library's codegen component registers (codegenNativeComponent<…>
8
+ // ('RNCSlider')). descriptorFor passes any non-`symbiote-` string through as a raw Fabric
9
+ // name, so the render fn emits this directly; the engine resolves its ViewConfig on first use.
10
+ export const RNC_SLIDER_VIEW_NAME = 'RNCSlider';
11
+
12
+ // Native event prop keys the engine routes to listeners. The library's ViewConfig declares the
13
+ // value change as BOTH a `topChange` (→ onChange) and `topRNCSliderValueChange`
14
+ // (→ onRNCSliderValueChange) bubbling event, plus the two direct sliding events; we bind both
15
+ // value-change keys exactly as the React wrapper does so neither host platform is missed.
16
+ export const SLIDER_ON_CHANGE = 'onChange';
17
+ export const SLIDER_ON_VALUE_CHANGE = 'onRNCSliderValueChange';
18
+ export const SLIDER_ON_SLIDING_START = 'onRNCSliderSlidingStart';
19
+ export const SLIDER_ON_SLIDING_COMPLETE = 'onRNCSliderSlidingComplete';
20
+ export const SLIDER_ON_ACCESSIBILITY_ACTION = 'onRNCSliderAccessibilityAction';
21
+
22
+ // Value/range defaults, matching the library wrapper's destructured prop defaults.
23
+ export const SLIDER_DEFAULT_INITIAL_VALUE = 0;
24
+ export const SLIDER_DEFAULT_MINIMUM_VALUE = 0;
25
+ export const SLIDER_DEFAULT_MAXIMUM_VALUE = 1;
26
+ export const SLIDER_DEFAULT_STEP = 0;
27
+
28
+ // lowerLimit/upperLimit default to the native sentinel (no limit) off-web; the library uses
29
+ // Number.MIN/MAX_SAFE_INTEGER for the unbounded case.
30
+ export const SLIDER_LIMIT_MIN_VALUE = Number.MIN_SAFE_INTEGER;
31
+ export const SLIDER_LIMIT_MAX_VALUE = Number.MAX_SAFE_INTEGER;
32
+
33
+ // Step-indicator resolution: how many implicit steps the library lays out when `step` is 0.
34
+ // Platform-specific in the library (android 128 / ios 1000); the adapter supplies the resolved
35
+ // number so the core render stays platform-invariant (no Platform.OS read here).
36
+ export const SLIDER_STEP_RESOLUTION_IOS = 1_000;
37
+ export const SLIDER_STEP_RESOLUTION_ANDROID = 128;
38
+
39
+ // iOS gives the native slider an intrinsic 40pt height; the wrapper applies it as the default.
40
+ export const SLIDER_IOS_DEFAULT_HEIGHT = 40;
41
+
42
+ // Step-indicator typography + layout, from the library's utils/styles + constants.
43
+ export const SLIDER_STEP_NUMBER_FONT_SMALL = 8;
44
+ export const SLIDER_STEP_NUMBER_FONT_BIG = 12;
45
+ export const SLIDER_STEP_NUMBER_FONT_THRESHOLD = 9;
46
+ export const SLIDER_THUMB_SIZE = 20;
47
+ export const SLIDER_MARGIN_HORIZONTAL_PADDING = 0.05;
@@ -0,0 +1,46 @@
1
+ // @symbiote-native/slider (core): the framework-agnostic half of the @react-native-community/slider
2
+ // wrapper. symbiote ships zero runtime metadata for RNCSlider — the engine derives its events
3
+ // and color processors from the library's own ViewConfig — so this layer is the pure JS folding
4
+ // the library's React wrapper does (value/limit/disabled resolution, the step-indicator layout)
5
+ // plus the Descriptor render of the native leaf. Every adapter (`@symbiote-native/slider/vue`, and
6
+ // `/angular`) reuses this verbatim and supplies only its lifecycle + descriptor bridge.
7
+ // This is the third-party-native-view track foreseen by ADR 0014; see ADR 0027.
8
+
9
+ export * from './constants';
10
+
11
+ export {
12
+ sanitizeSliderValue,
13
+ resolveSliderDisabled,
14
+ resolveSliderAccessibilityState,
15
+ resolveSliderLowerLimit,
16
+ resolveSliderUpperLimit,
17
+ valueFromSliderEvent,
18
+ computeStepOptions,
19
+ shouldRenderStepsIndicator,
20
+ resolveThumbTintColor,
21
+ shouldPassNativeThumbImage,
22
+ isInvalidLimitConfig,
23
+ } from './slider-state';
24
+ export type { ISliderAccessibilityState } from './slider-state';
25
+
26
+ export type { ISliderProps, ISliderPlatform, ISliderViewProps } from './slider-props';
27
+
28
+ export {
29
+ renderSlider,
30
+ renderSliderNative,
31
+ resolveSliderWrapperStyle,
32
+ resolveSliderNativeStyle,
33
+ } from './render-slider';
34
+
35
+ export {
36
+ renderStepsIndicator,
37
+ resolveStepsContainerStyle,
38
+ stepNumberFontSize,
39
+ orderStepOptions,
40
+ STEP_INDICATOR_ELEMENT_STYLE,
41
+ TRACK_MARK_CONTAINER_STYLE,
42
+ THUMB_IMAGE_CONTAINER_STYLE,
43
+ THUMB_IMAGE_STYLE,
44
+ STEP_NUMBER_CONTAINER_STYLE,
45
+ } from './render-steps-indicator';
46
+ export type { IStepsIndicatorParams, IStepMarkerProps } from './render-steps-indicator';
@@ -0,0 +1,89 @@
1
+ // Slider: the render half (framework-agnostic). Mirrors the library's React wrapper layout: an
2
+ // outer `symbiote-view` (default platform style + the caller's style, centered) hosting the
3
+ // native `RNCSlider` leaf, plus an optional step-indicator overlay sibling. The native leaf
4
+ // carries the folded value/limit/color/image props and the responder claims the wrapper does;
5
+ // every event handler + the ref ride in `passthrough` and the engine routes them against the
6
+ // view's derived ViewConfig. Pure and prop-driven — the adapter owns state, events, and the
7
+ // width measurement that feeds `view.width`.
8
+
9
+ import { dlog } from '@symbiote-native/engine';
10
+ import type { IStyleProp, IViewStyle, ISymbioteEvent } from '@symbiote-native/engine';
11
+ import { el } from '@symbiote-native/components';
12
+ import type { IDescriptor, IDescriptorChild } from '@symbiote-native/components';
13
+ import { RNC_SLIDER_VIEW_NAME } from './constants';
14
+ import type { ISliderPlatform, ISliderViewProps } from './slider-props';
15
+
16
+ // Extra wrapper props the adapter supplies: the pre-rendered default step overlay (placed before
17
+ // the native leaf, matching the library's z-order) and the onLayout handler that measures the
18
+ // width the step indicator needs. A custom StepMarker overlay is assembled per-adapter instead.
19
+ export type ISliderRenderOptions = {
20
+ steps?: IDescriptor | null;
21
+ onLayout?: (event: ISymbioteEvent) => void;
22
+ };
23
+
24
+ // The native slider stretches to the measured wrapper width when one is given (the step indicator
25
+ // and thumb then share one coordinate space); without it the slider sizes naturally.
26
+ export function resolveSliderNativeStyle(
27
+ width: number | undefined,
28
+ platform: ISliderPlatform,
29
+ ): IStyleProp<IViewStyle> {
30
+ const base: IViewStyle = { zIndex: 1, alignContent: 'center', alignItems: 'center' };
31
+ const sized: IStyleProp<IViewStyle> = width === undefined ? base : [base, { width }];
32
+ return [platform.defaultStyle, sized];
33
+ }
34
+
35
+ // The outer wrapper centers the native slider and gives it the platform default height; the
36
+ // caller's style layers on top, exactly as the library composes [defaultStyle, props.style]. The
37
+ // caller's style is opaque (`unknown`) — the engine flattens the array — so it is not narrowed.
38
+ export function resolveSliderWrapperStyle(style: unknown, platform: ISliderPlatform): unknown {
39
+ return [platform.defaultStyle, style, { justifyContent: 'center' }];
40
+ }
41
+
42
+ // The native `RNCSlider` leaf alone (no wrapper). Adapters that compose a custom step overlay
43
+ // (a per-adapter StepMarker element) assemble the wrapper themselves and drop this leaf in.
44
+ export function renderSliderNative(view: ISliderViewProps, platform: ISliderPlatform): IDescriptor {
45
+ dlog(
46
+ `Slider render value=${String(view.value)} disabled=${String(view.disabled)} width=${String(view.width)}`,
47
+ );
48
+ const props: Record<string, unknown> = {
49
+ // Forwarded verbatim: minimum/maximumTrackTintColor, track images, thumbSize, tapToSeek,
50
+ // vertical, accessibilityUnits/Increments, testID, accessibility*/aria-*, and the native
51
+ // event handlers the adapter wired in.
52
+ ...view.passthrough,
53
+ minimumValue: view.minimumValue,
54
+ maximumValue: view.maximumValue,
55
+ step: view.step,
56
+ lowerLimit: view.lowerLimit,
57
+ upperLimit: view.upperLimit,
58
+ value: view.value,
59
+ inverted: view.inverted,
60
+ disabled: view.disabled,
61
+ thumbTintColor: view.thumbTintColor,
62
+ thumbImage: view.thumbImage,
63
+ accessibilityState: view.accessibilityState,
64
+ style: resolveSliderNativeStyle(view.width, platform),
65
+ // The library claims the responder on the native slider so a parent ScrollView can't steal
66
+ // the drag, and refuses termination requests; mirrored for gesture parity.
67
+ onStartShouldSetResponder: () => true,
68
+ onResponderTerminationRequest: () => false,
69
+ };
70
+ return el(RNC_SLIDER_VIEW_NAME, props);
71
+ }
72
+
73
+ // The full slider: the centered wrapper hosting the native leaf, with an optional pre-rendered
74
+ // step-indicator overlay before it. The default overlay (renderStepsIndicator) is passed via
75
+ // `options.steps`; a custom StepMarker overlay is assembled per-adapter at the element level.
76
+ export function renderSlider(
77
+ view: ISliderViewProps,
78
+ platform: ISliderPlatform,
79
+ options: ISliderRenderOptions = {},
80
+ ): IDescriptor {
81
+ const children: IDescriptorChild[] = [];
82
+ if (options.steps) children.push(options.steps);
83
+ children.push(renderSliderNative(view, platform));
84
+ const wrapperProps: Record<string, unknown> = {
85
+ style: resolveSliderWrapperStyle(view.style, platform),
86
+ };
87
+ if (options.onLayout !== undefined) wrapperProps.onLayout = options.onLayout;
88
+ return el('symbiote-view', wrapperProps, children);
89
+ }
@@ -0,0 +1,154 @@
1
+ // Step indicator: the row of marks the library overlays on a stepped slider. Like a list's
2
+ // cells, each mark MAY hold the framework's own `StepMarker` element, so the overlay is shared
3
+ // as MATH + a DEFAULT render: the layout/style/font helpers below are reused by every adapter,
4
+ // and renderStepsIndicator paints the default overlay (empty mark containers + optional step
5
+ // numbers + the thumbImage on the current step). An adapter with a custom StepMarker slot
6
+ // assembles the overlay itself from these same helpers, dropping its element into each mark.
7
+
8
+ import { el, txt } from '@symbiote-native/components';
9
+ import type { IDescriptor, IDescriptorChild } from '@symbiote-native/components';
10
+ import type { IViewStyle, ITextStyle } from '@symbiote-native/engine';
11
+ import {
12
+ SLIDER_MARGIN_HORIZONTAL_PADDING,
13
+ SLIDER_STEP_NUMBER_FONT_BIG,
14
+ SLIDER_STEP_NUMBER_FONT_SMALL,
15
+ SLIDER_STEP_NUMBER_FONT_THRESHOLD,
16
+ } from './constants';
17
+ import type { ISliderPlatform } from './slider-props';
18
+
19
+ // Static overlay styles, ported from the library's utils/styles (native branch only — symbiote
20
+ // targets iOS/Android, so the web layout branches are dropped).
21
+ export const STEP_INDICATOR_ELEMENT_STYLE: IViewStyle = {
22
+ alignItems: 'center',
23
+ alignContent: 'center',
24
+ };
25
+
26
+ export const TRACK_MARK_CONTAINER_STYLE: IViewStyle = {
27
+ alignItems: 'center',
28
+ alignContent: 'center',
29
+ alignSelf: 'center',
30
+ justifyContent: 'center',
31
+ position: 'absolute',
32
+ zIndex: 3,
33
+ };
34
+
35
+ export const THUMB_IMAGE_CONTAINER_STYLE: IViewStyle = {
36
+ position: 'absolute',
37
+ zIndex: 3,
38
+ justifyContent: 'center',
39
+ alignItems: 'center',
40
+ alignContent: 'center',
41
+ };
42
+
43
+ export const THUMB_IMAGE_STYLE: IViewStyle = {
44
+ alignContent: 'center',
45
+ alignItems: 'center',
46
+ position: 'absolute',
47
+ };
48
+
49
+ export const STEP_NUMBER_CONTAINER_STYLE: IViewStyle = {
50
+ marginTop: 20,
51
+ alignItems: 'center',
52
+ position: 'absolute',
53
+ };
54
+
55
+ // The mark row: flex row, evenly spaced, inset by a fraction of the measured width. `top` is
56
+ // platform-specific (iOS nudges the row down 10pt), supplied via the platform piece.
57
+ export function resolveStepsContainerStyle(width: number, platform: ISliderPlatform): IViewStyle {
58
+ return {
59
+ flex: 1,
60
+ flexDirection: 'row',
61
+ justifyContent: 'space-between',
62
+ top: platform.stepsContainerTop,
63
+ zIndex: 2,
64
+ marginHorizontal: width * SLIDER_MARGIN_HORIZONTAL_PADDING,
65
+ };
66
+ }
67
+
68
+ // Smaller font once there are more than 9 steps, so the labels keep fitting.
69
+ export function stepNumberFontSize(optionsLength: number): number {
70
+ return optionsLength > SLIDER_STEP_NUMBER_FONT_THRESHOLD
71
+ ? SLIDER_STEP_NUMBER_FONT_SMALL
72
+ : SLIDER_STEP_NUMBER_FONT_BIG;
73
+ }
74
+
75
+ // Inverted sliders lay the marks right-to-left; copy before reversing (the helper is pure).
76
+ export function orderStepOptions(
77
+ options: readonly number[],
78
+ inverted: boolean | undefined,
79
+ ): number[] {
80
+ return inverted ? [...options].reverse() : [...options];
81
+ }
82
+
83
+ // The props a custom step marker receives, framework-agnostic scalars (the library's MarkerProps).
84
+ // Each adapter renders the marker its own way — React as an FC<IStepMarkerProps>, Vue as a scoped
85
+ // slot — but the props it gets are identical, so the shape lives here.
86
+ export type IStepMarkerProps = {
87
+ stepMarked: boolean;
88
+ currentValue: number;
89
+ index: number;
90
+ min: number;
91
+ max: number;
92
+ };
93
+
94
+ export type IStepsIndicatorParams = {
95
+ options: readonly number[];
96
+ currentValue: number;
97
+ width: number;
98
+ renderStepNumber?: boolean;
99
+ thumbImage?: unknown;
100
+ inverted?: boolean;
101
+ platform: ISliderPlatform;
102
+ };
103
+
104
+ // One mark cell: an absolutely-centered track-mark container (empty by default, or the
105
+ // thumbImage on the current step) plus the optional step number label.
106
+ function renderStepCell(
107
+ value: number,
108
+ displayIndex: number,
109
+ params: IStepsIndicatorParams,
110
+ fontSize: number,
111
+ ): IDescriptor {
112
+ const trackMarkChildren: IDescriptorChild[] = [];
113
+ if (params.thumbImage !== undefined && value === params.currentValue) {
114
+ trackMarkChildren.push(
115
+ el(
116
+ 'symbiote-view',
117
+ { style: THUMB_IMAGE_CONTAINER_STYLE, testID: 'sliderTrackMark-thumbImage' },
118
+ [el('symbiote-image', { source: params.thumbImage, style: THUMB_IMAGE_STYLE })],
119
+ ),
120
+ );
121
+ }
122
+
123
+ const cellChildren: IDescriptorChild[] = [
124
+ el('symbiote-view', { style: TRACK_MARK_CONTAINER_STYLE }, trackMarkChildren),
125
+ ];
126
+ if (params.renderStepNumber === true) {
127
+ cellChildren.push(
128
+ el('symbiote-view', { style: STEP_NUMBER_CONTAINER_STYLE }, [
129
+ txt({ testID: `${displayIndex}th-step`, style: { fontSize } satisfies ITextStyle }, [
130
+ String(value),
131
+ ]),
132
+ ]),
133
+ );
134
+ }
135
+ return el('symbiote-view', { style: STEP_INDICATOR_ELEMENT_STYLE }, cellChildren);
136
+ }
137
+
138
+ // The default overlay (no custom StepMarker). pointerEvents none so it never eats the drag.
139
+ export function renderStepsIndicator(params: IStepsIndicatorParams): IDescriptor {
140
+ const fontSize = stepNumberFontSize(params.options.length);
141
+ const ordered = orderStepOptions(params.options, params.inverted);
142
+ const cells = ordered.map((value, displayIndex) =>
143
+ renderStepCell(value, displayIndex, params, fontSize),
144
+ );
145
+ return el(
146
+ 'symbiote-view',
147
+ {
148
+ pointerEvents: 'none',
149
+ testID: 'StepsIndicator-Container',
150
+ style: resolveStepsContainerStyle(params.width, params.platform),
151
+ },
152
+ cells,
153
+ );
154
+ }
@@ -0,0 +1,83 @@
1
+ // Slider prop types. Per CLAUDE.md <prop_types_split_agnostic_vs_per_adapter>: every field of
2
+ // the slider's public surface is framework-agnostic EXCEPT the custom `StepMarker`, which is a
3
+ // render component returning a framework element (React FC vs Vue slot) — so that one stays in
4
+ // each adapter's own flavored prop type. ISliderProps here is the shared agnostic base every
5
+ // adapter re-exports and extends; ISliderViewProps is the pre-resolved input the render fn paints.
6
+
7
+ import type { IColorValue, IStyleProp, IViewStyle } from '@symbiote-native/engine';
8
+ import type {
9
+ IAccessibilityProps,
10
+ IAriaProps,
11
+ IImageSourceProp,
12
+ } from '@symbiote-native/components';
13
+ import type { ISliderAccessibilityState } from './slider-state';
14
+
15
+ // The agnostic public surface, shared by every adapter. Callbacks take a plain number (the
16
+ // adapter unwraps nativeEvent.value), colors/images/style are agnostic value types. The custom
17
+ // `StepMarker` component is deliberately absent — adapters add it to their own flavored type.
18
+ export interface ISliderProps extends IAccessibilityProps, IAriaProps {
19
+ value?: number;
20
+ minimumValue?: number;
21
+ maximumValue?: number;
22
+ step?: number;
23
+ lowerLimit?: number;
24
+ upperLimit?: number;
25
+ minimumTrackTintColor?: IColorValue;
26
+ maximumTrackTintColor?: IColorValue;
27
+ thumbTintColor?: IColorValue;
28
+ disabled?: boolean;
29
+ inverted?: boolean;
30
+ tapToSeek?: boolean;
31
+ vertical?: boolean;
32
+ thumbImage?: IImageSourceProp;
33
+ minimumTrackImage?: IImageSourceProp;
34
+ maximumTrackImage?: IImageSourceProp;
35
+ trackImage?: IImageSourceProp;
36
+ thumbSize?: number;
37
+ accessibilityUnits?: string;
38
+ accessibilityIncrements?: readonly string[];
39
+ renderStepNumber?: boolean;
40
+ onValueChange?: (value: number) => void;
41
+ onSlidingStart?: (value: number) => void;
42
+ onSlidingComplete?: (value: number) => void;
43
+ testID?: string;
44
+ style?: IStyleProp<IViewStyle>;
45
+ }
46
+
47
+ // The per-platform piece the render needs: the iOS default 40pt height (Android leaves it to
48
+ // the native view's intrinsic size), the step-indicator resolution fallback (ios 1000 /
49
+ // android 128), and the step container's platform `top` offset. The adapter's .ios/.android
50
+ // file supplies these (Metro filename-selected), keeping the render platform-invariant.
51
+ export type ISliderPlatform = {
52
+ defaultStyle: IViewStyle;
53
+ stepResolution: number;
54
+ stepsContainerTop: number;
55
+ };
56
+
57
+ // Pre-resolved inputs the native render paints from. Only the fields that need FOLDING are
58
+ // explicit here (value sanitized, disabled/accessibilityState resolved, limits defaulted, the
59
+ // thumb tint/image resolved against the StepMarker decision); everything that just passes through
60
+ // untouched — minimum/maximumTrackTintColor, the track images, thumbSize, tapToSeek, vertical,
61
+ // accessibility*/testID/aria-*, AND the native event handlers — rides in `passthrough` and lands
62
+ // on the host node verbatim (the engine routes the on*-keyed handlers and runs the ViewConfig
63
+ // color processors). `width` is set only when a step overlay needs it, so the common case has no
64
+ // width:0 first-paint flash.
65
+ export type ISliderViewProps = {
66
+ value?: number;
67
+ minimumValue: number;
68
+ maximumValue: number;
69
+ step: number;
70
+ lowerLimit: number;
71
+ upperLimit: number;
72
+ disabled: boolean;
73
+ inverted: boolean;
74
+ thumbTintColor?: unknown;
75
+ thumbImage?: unknown;
76
+ accessibilityState?: ISliderAccessibilityState;
77
+ width?: number;
78
+ // Opaque to the render — composed into a style array the engine flattens — so kept `unknown`
79
+ // (object/array/registered-id all valid) rather than narrowed off attrs. The PUBLIC ISliderProps
80
+ // keeps the precise IStyleProp typing.
81
+ style?: unknown;
82
+ passthrough: Record<string, unknown>;
83
+ };