@symbiote-native/slider 5.1.0 → 5.2.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 (61) hide show
  1. package/README.md +24 -7
  2. package/build/angular/slider/index.android.js +7 -1
  3. package/build/angular/slider/index.ios.js +8 -2
  4. package/build/angular/slider/shared.js +4 -1
  5. package/build/core/index.d.ts +2 -2
  6. package/build/core/render-slider.js +5 -1
  7. package/build/core/render-steps-indicator.js +13 -4
  8. package/build/react/slider/index.ios.js +1 -1
  9. package/build/react/slider/shared.js +11 -4
  10. package/build/register/index.js +5 -2
  11. package/build/solid/index.d.ts +4 -0
  12. package/build/solid/index.js +8 -0
  13. package/build/solid/slider/index.android.d.ts +2 -0
  14. package/build/solid/slider/index.android.js +10 -0
  15. package/build/solid/slider/index.d.ts +1 -0
  16. package/build/solid/slider/index.ios.d.ts +2 -0
  17. package/build/solid/slider/index.ios.js +11 -0
  18. package/build/solid/slider/index.js +4 -0
  19. package/build/solid/slider/shared.d.ts +12 -0
  20. package/build/solid/slider/shared.js +296 -0
  21. package/build/svelte/slider/index.svelte +62 -10
  22. package/build/svelte/slider/slider-platform.ios.js +1 -1
  23. package/build/vue/slider/index.ios.js +1 -1
  24. package/build/vue/slider/shared.js +11 -5
  25. package/build-ngc/angular/slider/index.android.js +14 -2
  26. package/build-ngc/angular/slider/index.android.js.map +1 -1
  27. package/build-ngc/angular/slider/index.ios.js +15 -3
  28. package/build-ngc/angular/slider/index.ios.js.map +1 -1
  29. package/build-ngc/angular/slider/shared.js +4 -1
  30. package/build-ngc/angular/slider/shared.js.map +1 -1
  31. package/build-ngc/core/index.d.ts +2 -2
  32. package/build-ngc/core/index.js.map +1 -1
  33. package/build-ngc/core/render-slider.js +5 -1
  34. package/build-ngc/core/render-slider.js.map +1 -1
  35. package/build-ngc/core/render-steps-indicator.js +13 -4
  36. package/build-ngc/core/render-steps-indicator.js.map +1 -1
  37. package/build-ngc/core/slider-state.js.map +1 -1
  38. package/build-ngc/register/index.js +5 -2
  39. package/build-ngc/register/index.js.map +1 -1
  40. package/package.json +23 -9
  41. package/src/angular/slider/index.android.ts +7 -1
  42. package/src/angular/slider/index.ios.ts +11 -2
  43. package/src/angular/slider/shared.ts +31 -10
  44. package/src/core/index.ts +9 -2
  45. package/src/core/render-slider.ts +24 -6
  46. package/src/core/render-steps-indicator.ts +38 -10
  47. package/src/core/slider-props.ts +5 -1
  48. package/src/core/slider-state.ts +16 -5
  49. package/src/react/slider/index.ios.ts +4 -1
  50. package/src/react/slider/shared.ts +66 -15
  51. package/src/register/index.ts +9 -2
  52. package/src/solid/index.ts +12 -0
  53. package/src/solid/slider/index.android.ts +15 -0
  54. package/src/solid/slider/index.ios.ts +19 -0
  55. package/src/solid/slider/index.ts +5 -0
  56. package/src/solid/slider/shared.ts +468 -0
  57. package/src/svelte/slider/index.svelte +62 -10
  58. package/src/svelte/slider/slider-platform.ios.ts +4 -1
  59. package/src/svelte/slider/slider-props.ts +4 -1
  60. package/src/vue/slider/index.ios.ts +4 -1
  61. package/src/vue/slider/shared.ts +72 -19
package/README.md CHANGED
@@ -1,9 +1,10 @@
1
1
  # @symbiote-native/slider
2
2
 
3
3
  A wrapper package for [SymbioteNative](../../README.md) that makes the third-party native
4
- `@react-native-community/slider` view usable from **every** adapter — React, Vue, and Angular —
5
- not just React. The library's default export is a React component (it calls hooks off the React
6
- dispatcher), so a non-React adapter renders it with a null dispatcher and it throws. This package
4
+ `@react-native-community/slider` view usable from **every** adapter — React, Vue, Svelte, Solid,
5
+ and Angular — not just React. The library's default export is a React component (it calls hooks
6
+ off the React dispatcher), so a non-React adapter renders it with a null dispatcher and it throws.
7
+ This package
7
8
  reaches the native `RNCSlider` view directly through the engine's `createNode`-by-ViewConfig path
8
9
  instead of importing the library's React component — the same mechanism SymbioteNative uses for its
9
10
  own primitives.
@@ -29,14 +30,16 @@ src/register.ts side-effect import of RNCSlider's codegen spec — registers its
29
30
  (never the library's React Slider.tsx)
30
31
  src/react/ @symbiote-native/slider/react — forwardRef FC + descriptorToReact
31
32
  src/vue/ @symbiote-native/slider/vue — defineComponent + descriptorToVue
33
+ src/svelte/ @symbiote-native/slider/svelte — .svelte component + the DOM-shim renderer
34
+ src/solid/ @symbiote-native/slider/solid — signals + descriptorToSolid
32
35
  src/angular/ @symbiote-native/slider/angular — Angular component + descriptorToAngular
33
36
  ```
34
37
 
35
38
  Each adapter entry imports `../register` first (so the ViewConfig is registered before the
36
39
  component ever mounts), then exposes a platform-split `Slider`. The core is written once; every
37
40
  adapter inherits the same folding logic and the same `ISliderProps` surface (extended per-adapter
38
- only where a field carries a framework element — the custom `StepMarker` is a React render prop
39
- but a Vue scoped slot).
41
+ only where a field carries a framework element — the custom `StepMarker` is a React render prop, a
42
+ Vue scoped slot, a Svelte `Snippet`, and a Solid render prop taking an **accessor**).
40
43
 
41
44
  ## Packaging — one dependency, not two
42
45
 
@@ -64,7 +67,7 @@ import { Slider } from '@symbiote-native/slider/react';
64
67
  minimumTrackTintColor="#7fb5ff"
65
68
  maximumTrackTintColor="#334155"
66
69
  thumbTintColor="#ffffff"
67
- />
70
+ />;
68
71
  ```
69
72
 
70
73
  ```vue
@@ -81,6 +84,20 @@ import { Slider } from '@symbiote-native/slider/vue';
81
84
  </template>
82
85
  ```
83
86
 
87
+ ```tsx
88
+ // Solid — the marker props arrive as an ACCESSOR, so only the leaf reading them re-runs
89
+ import { Slider } from '@symbiote-native/slider/solid';
90
+
91
+ <Slider
92
+ value={volume()}
93
+ onValueChange={setVolume}
94
+ step={0.25}
95
+ StepMarker={marker => (
96
+ <symbiote-view style={{ opacity: marker().stepMarked ? 1 : 0.3 }} />
97
+ )}
98
+ />;
99
+ ```
100
+
84
101
  ```ts
85
102
  // Angular — examples/angular/App.ts
86
103
  import { Slider } from '@symbiote-native/slider/angular';
@@ -93,6 +110,6 @@ processors from its own ViewConfig at runtime, so there is nothing to pre-resolv
93
110
  ## Test it
94
111
 
95
112
  Headless component tests live next to each adapter entry
96
- (`src/{react,vue,angular}/slider/slider.test.{ts,tsx}`) and inject a fake ViewConfig via
113
+ (`src/{react,vue,svelte,solid,angular}/slider/slider*.test.{ts,tsx}`) and inject a fake ViewConfig via
97
114
  `setNativeViewConfigSource`, so they run without a real Fabric host. Native rendering is verified
98
115
  on-device (see the parent [README](../../README.md) for the project's testing model).
@@ -46,7 +46,13 @@ let Slider = (() => {
46
46
  standalone: true,
47
47
  imports: [DescriptorOutlet],
48
48
  changeDetection: ChangeDetectionStrategy.OnPush,
49
- providers: [{ provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => Slider), multi: true }],
49
+ providers: [
50
+ {
51
+ provide: NG_VALUE_ACCESSOR,
52
+ useExisting: forwardRef(() => Slider),
53
+ multi: true,
54
+ },
55
+ ],
50
56
  template: `<symbiote-descriptor-outlet [node]="descriptor" />`,
51
57
  })];
52
58
  let _classDescriptor;
@@ -39,7 +39,7 @@ import { ChangeDetectionStrategy, Component, forwardRef } from '@angular/core';
39
39
  import { NG_VALUE_ACCESSOR } from '@angular/forms';
40
40
  import { DescriptorOutlet } from '@symbiote-native/angular';
41
41
  import { SliderBase } from './shared.js';
42
- import { SLIDER_IOS_DEFAULT_HEIGHT, SLIDER_STEP_RESOLUTION_IOS } from '../../core/index.js';
42
+ import { SLIDER_IOS_DEFAULT_HEIGHT, SLIDER_STEP_RESOLUTION_IOS, } from '../../core/index.js';
43
43
  const IOS_STEPS_CONTAINER_TOP = 10;
44
44
  let Slider = (() => {
45
45
  let _classDecorators = [Component({
@@ -47,7 +47,13 @@ let Slider = (() => {
47
47
  standalone: true,
48
48
  imports: [DescriptorOutlet],
49
49
  changeDetection: ChangeDetectionStrategy.OnPush,
50
- providers: [{ provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => Slider), multi: true }],
50
+ providers: [
51
+ {
52
+ provide: NG_VALUE_ACCESSOR,
53
+ useExisting: forwardRef(() => Slider),
54
+ multi: true,
55
+ },
56
+ ],
51
57
  template: `<symbiote-descriptor-outlet [node]="descriptor" />`,
52
58
  })];
53
59
  let _classDescriptor;
@@ -660,7 +660,10 @@ let SliderBase = (() => {
660
660
  inverted,
661
661
  platform: this.platform,
662
662
  });
663
- return renderSlider(view, this.platform, { steps, onLayout: this.handleLayout });
663
+ return renderSlider(view, this.platform, {
664
+ steps,
665
+ onLayout: this.handleLayout,
666
+ });
664
667
  }
665
668
  };
666
669
  return SliderBase = _classThis;
@@ -1,7 +1,7 @@
1
1
  export * from './constants';
2
2
  export { sanitizeSliderValue, resolveSliderDisabled, resolveSliderAccessibilityState, resolveSliderLowerLimit, resolveSliderUpperLimit, valueFromSliderEvent, computeStepOptions, shouldRenderStepsIndicator, resolveThumbTintColor, shouldPassNativeThumbImage, isInvalidLimitConfig, } from './slider-state';
3
3
  export type { ISliderAccessibilityState } from './slider-state';
4
- export type { ISliderProps, ISliderPlatform, ISliderViewProps } from './slider-props';
4
+ export type { ISliderProps, ISliderPlatform, ISliderViewProps, } from './slider-props';
5
5
  export { renderSlider, renderSliderNative, resolveSliderWrapperStyle, resolveSliderNativeStyle, } from './render-slider';
6
6
  export { renderStepsIndicator, resolveStepsContainerStyle, stepNumberFontSize, orderStepOptions, STEP_INDICATOR_ELEMENT_STYLE, TRACK_MARK_CONTAINER_STYLE, THUMB_IMAGE_CONTAINER_STYLE, THUMB_IMAGE_STYLE, STEP_NUMBER_CONTAINER_STYLE, } from './render-steps-indicator';
7
- export type { IStepsIndicatorParams, IStepMarkerProps } from './render-steps-indicator';
7
+ export type { IStepsIndicatorParams, IStepMarkerProps, } from './render-steps-indicator';
@@ -11,7 +11,11 @@ import { RNC_SLIDER_VIEW_NAME } from './constants.js';
11
11
  // The native slider stretches to the measured wrapper width when one is given (the step indicator
12
12
  // and thumb then share one coordinate space); without it the slider sizes naturally.
13
13
  export function resolveSliderNativeStyle(width, platform) {
14
- const base = { zIndex: 1, alignContent: 'center', alignItems: 'center' };
14
+ const base = {
15
+ zIndex: 1,
16
+ alignContent: 'center',
17
+ alignItems: 'center',
18
+ };
15
19
  const sized = width === undefined ? base : [base, { width }];
16
20
  return [platform.defaultStyle, sized];
17
21
  }
@@ -64,16 +64,25 @@ export function orderStepOptions(options, inverted) {
64
64
  function renderStepCell(value, displayIndex, params, fontSize) {
65
65
  const trackMarkChildren = [];
66
66
  if (params.thumbImage !== undefined && value === params.currentValue) {
67
- trackMarkChildren.push(el('symbiote-view', { style: THUMB_IMAGE_CONTAINER_STYLE, testID: 'sliderTrackMark-thumbImage' }, [el('symbiote-image', { source: params.thumbImage, style: THUMB_IMAGE_STYLE })]));
67
+ trackMarkChildren.push(el('symbiote-view', {
68
+ style: THUMB_IMAGE_CONTAINER_STYLE,
69
+ testID: 'sliderTrackMark-thumbImage',
70
+ }, [
71
+ el('symbiote-image', {
72
+ source: params.thumbImage,
73
+ style: THUMB_IMAGE_STYLE,
74
+ }),
75
+ ]));
68
76
  }
69
77
  const cellChildren = [
70
78
  el('symbiote-view', { style: TRACK_MARK_CONTAINER_STYLE }, trackMarkChildren),
71
79
  ];
72
80
  if (params.renderStepNumber === true) {
73
81
  cellChildren.push(el('symbiote-view', { style: STEP_NUMBER_CONTAINER_STYLE }, [
74
- txt({ testID: `${displayIndex}th-step`, style: { fontSize } }, [
75
- String(value),
76
- ]),
82
+ txt({
83
+ testID: `${displayIndex}th-step`,
84
+ style: { fontSize },
85
+ }, [String(value)]),
77
86
  ]));
78
87
  }
79
88
  return el('symbiote-view', { style: STEP_INDICATOR_ELEMENT_STYLE }, cellChildren);
@@ -2,7 +2,7 @@
2
2
  // default), lays out implicit steps at 1000-point resolution, and nudges the step row down 10pt.
3
3
  // Also the base (./index re-exports it) for headless. Mirrors the library's iOS branches.
4
4
  import { createSlider } from './shared.js';
5
- import { SLIDER_IOS_DEFAULT_HEIGHT, SLIDER_STEP_RESOLUTION_IOS } from '../../core/index.js';
5
+ import { SLIDER_IOS_DEFAULT_HEIGHT, SLIDER_STEP_RESOLUTION_IOS, } from '../../core/index.js';
6
6
  const IOS_STEPS_CONTAINER_TOP = 10;
7
7
  export const Slider = createSlider({
8
8
  defaultStyle: { height: SLIDER_IOS_DEFAULT_HEIGHT },
@@ -8,10 +8,10 @@
8
8
  // it). This is the React twin of the Vue wrapper; both drive the engine, NEITHER imports the
9
9
  // library's React Slider component — that component internally calls React hooks off the React
10
10
  // dispatcher, so a non-React adapter rendering it directly would crash on a null dispatcher.
11
- import { createElement, forwardRef, useCallback, useEffect, useState } from 'react';
11
+ import { createElement, forwardRef, useCallback, useEffect, useState, } from 'react';
12
12
  import { descriptorToReact, Image } from '@symbiote-native/react';
13
13
  import { resolveAccessibilityProps } from '@symbiote-native/components';
14
- import { dlog } from '@symbiote-native/engine';
14
+ import { dlog, } from '@symbiote-native/engine';
15
15
  import { sanitizeSliderValue, resolveSliderDisabled, resolveSliderAccessibilityState, resolveSliderLowerLimit, resolveSliderUpperLimit, valueFromSliderEvent, shouldRenderStepsIndicator, resolveThumbTintColor, shouldPassNativeThumbImage, isInvalidLimitConfig, computeStepOptions, orderStepOptions, stepNumberFontSize, renderSlider, renderSliderNative, resolveStepsContainerStyle, renderStepsIndicator, STEP_INDICATOR_ELEMENT_STYLE, TRACK_MARK_CONTAINER_STYLE, THUMB_IMAGE_CONTAINER_STYLE, THUMB_IMAGE_STYLE, STEP_NUMBER_CONTAINER_STYLE, SLIDER_DEFAULT_MINIMUM_VALUE, SLIDER_DEFAULT_MAXIMUM_VALUE, SLIDER_DEFAULT_STEP, SLIDER_ON_CHANGE, SLIDER_ON_VALUE_CHANGE, SLIDER_ON_SLIDING_START, SLIDER_ON_SLIDING_COMPLETE, SLIDER_ON_ACCESSIBILITY_ACTION, } from '../../core/index.js';
16
16
  function isRecord(value) {
17
17
  return typeof value === 'object' && value !== null && !Array.isArray(value);
@@ -116,7 +116,11 @@ export function createSlider(platform) {
116
116
  platform,
117
117
  StepMarker,
118
118
  });
119
- return createElement('symbiote-view', { style: resolveStepsWrapperStyle(style, platform), onLayout: handleLayout, className }, overlay, descriptorToReact(renderSliderNative(view, platform)));
119
+ return createElement('symbiote-view', {
120
+ style: resolveStepsWrapperStyle(style, platform),
121
+ onLayout: handleLayout,
122
+ className,
123
+ }, overlay, descriptorToReact(renderSliderNative(view, platform)));
120
124
  }
121
125
  const steps = renderStepsIndicator({
122
126
  options,
@@ -158,7 +162,10 @@ function renderCustomStepsOverlay(params) {
158
162
  key: 'thumb',
159
163
  style: THUMB_IMAGE_CONTAINER_STYLE,
160
164
  testID: 'sliderTrackMark-thumbImage',
161
- }, createElement(Image, { source: params.thumbImage, style: THUMB_IMAGE_STYLE })));
165
+ }, createElement(Image, {
166
+ source: params.thumbImage,
167
+ style: THUMB_IMAGE_STYLE,
168
+ })));
162
169
  }
163
170
  const cellChildren = [
164
171
  createElement('symbiote-view', { key: 'track', style: TRACK_MARK_CONTAINER_STYLE }, ...trackChildren),
@@ -6,7 +6,7 @@
6
6
  // hooks, so it is framework-agnostic and shared by every adapter entry (vue / react). Each adapter
7
7
  // barrel imports it; the component modules and their headless tests stay free of the spec.
8
8
  import '@react-native-community/slider/dist/RNCSliderNativeComponent';
9
- import { processColor, registerComponent } from '@symbiote-native/engine';
9
+ import { processColor, registerComponent, } from '@symbiote-native/engine';
10
10
  import { RNC_SLIDER_VIEW_NAME, SLIDER_ON_CHANGE, SLIDER_ON_VALUE_CHANGE, SLIDER_ON_SLIDING_START, SLIDER_ON_SLIDING_COMPLETE, SLIDER_ON_ACCESSIBILITY_ACTION, } from '../core/index.js';
11
11
  function listenerName(prop) {
12
12
  return prop.charAt(2).toLowerCase() + prop.slice(3);
@@ -22,7 +22,10 @@ function processSliderColor(value) {
22
22
  registerComponent(RNC_SLIDER_VIEW_NAME, {
23
23
  events: [
24
24
  { raw: 'topChange', listener: listenerName(SLIDER_ON_CHANGE) },
25
- { raw: 'topRNCSliderValueChange', listener: listenerName(SLIDER_ON_VALUE_CHANGE) },
25
+ {
26
+ raw: 'topRNCSliderValueChange',
27
+ listener: listenerName(SLIDER_ON_VALUE_CHANGE),
28
+ },
26
29
  {
27
30
  raw: 'topRNCSliderSlidingStart',
28
31
  listener: listenerName(SLIDER_ON_SLIDING_START),
@@ -0,0 +1,4 @@
1
+ import '../register';
2
+ export { Slider } from './slider';
3
+ export type { ISliderProps, ISliderStepMarker } from './slider';
4
+ export type { IStepMarkerProps } from '../core';
@@ -0,0 +1,8 @@
1
+ // @symbiote-native/slider/solid: the Solid wrapper over the @react-native-community/slider native
2
+ // view. The bare import below is FIRST and is a side effect, not a re-export: it registers the
3
+ // native view's ViewConfig (the codegen spec, never the library's React component) before any
4
+ // Slider can mount. Metro's production-only `inlineRequires` moves a require down to the first
5
+ // place its binding is USED and compiles a barrel's re-export into a lazy getter, so a registration
6
+ // reached through one silently never runs in RELEASE builds only. Do not restructure it.
7
+ import '../register';
8
+ export { Slider } from './slider';
@@ -0,0 +1,2 @@
1
+ export type { ISliderProps, ISliderStepMarker } from './shared';
2
+ export declare const Slider: (props: import("./shared").ISliderProps) => import("@symbiote-native/solid/jsx-runtime").JSX.Element;
@@ -0,0 +1,10 @@
1
+ // Slider on Android: the native view sizes itself (no default height), lays out implicit steps at
2
+ // 128-point resolution, and keeps the step row at the top. Mirrors the library's Android branches.
3
+ import { createSlider } from './shared.js';
4
+ import { SLIDER_STEP_RESOLUTION_ANDROID } from '../../core/index.js';
5
+ const ANDROID_STEPS_CONTAINER_TOP = 0;
6
+ export const Slider = createSlider({
7
+ defaultStyle: {},
8
+ stepResolution: SLIDER_STEP_RESOLUTION_ANDROID,
9
+ stepsContainerTop: ANDROID_STEPS_CONTAINER_TOP,
10
+ });
@@ -0,0 +1 @@
1
+ export * from './index.ios';
@@ -0,0 +1,2 @@
1
+ export type { ISliderProps, ISliderStepMarker } from './shared';
2
+ export declare const Slider: (props: import("./shared").ISliderProps) => import("@symbiote-native/solid/jsx-runtime").JSX.Element;
@@ -0,0 +1,11 @@
1
+ // Slider on iOS: the native view has an intrinsic 40pt height (the wrapper applies it as the
2
+ // default), lays out implicit steps at 1000-point resolution, and nudges the step row down 10pt.
3
+ // Also the base (./index re-exports it) for headless. Mirrors the library's iOS branches.
4
+ import { createSlider } from './shared.js';
5
+ import { SLIDER_IOS_DEFAULT_HEIGHT, SLIDER_STEP_RESOLUTION_IOS, } from '../../core/index.js';
6
+ const IOS_STEPS_CONTAINER_TOP = 10;
7
+ export const Slider = createSlider({
8
+ defaultStyle: { height: SLIDER_IOS_DEFAULT_HEIGHT },
9
+ stepResolution: SLIDER_STEP_RESOLUTION_IOS,
10
+ stepsContainerTop: IOS_STEPS_CONTAINER_TOP,
11
+ });
@@ -0,0 +1,4 @@
1
+ // Base / default Slider: re-exports the iOS build. Metro overrides this with index.ios.ts /
2
+ // index.android.ts on a real host; under vitest / tsc the resolution lands here. Filename is the
3
+ // selector, no Platform.OS read — the same convention adapters/solid's own Switch follows.
4
+ export * from './index.ios.js';
@@ -0,0 +1,12 @@
1
+ import type { Accessor, Ref } from 'solid-js';
2
+ import { type IHostInstance } from '@symbiote-native/solid';
3
+ import type { JSX } from '@symbiote-native/solid/jsx-runtime';
4
+ import { type IClassNameValue } from '@symbiote-native/engine';
5
+ import { type ISliderPlatform, type ISliderProps as ISliderBaseProps, type IStepMarkerProps } from '../../core';
6
+ export type ISliderStepMarker = (marker: Accessor<IStepMarkerProps>) => JSX.Element;
7
+ export type ISliderProps = ISliderBaseProps & {
8
+ class?: IClassNameValue;
9
+ StepMarker?: ISliderStepMarker;
10
+ ref?: Ref<IHostInstance>;
11
+ };
12
+ export declare function createSlider(platform: ISliderPlatform): (props: ISliderProps) => JSX.Element;
@@ -0,0 +1,296 @@
1
+ // Slider, the Solid lifecycle half. The logic (value/limit/disabled folds, the step-option layout)
2
+ // and the native render live in @symbiote-native/slider core, shared verbatim with React, Vue,
3
+ // Svelte and Angular; Solid supplies only the reactivity — a signal for the value native last
4
+ // reported (to mark the active step), a signal for the measured width — plus the descriptor bridge.
5
+ // The native RNCSlider carries no symbiote metadata: the engine derives its events and color/image
6
+ // processors from the library's ViewConfig, registered by ../register (the package barrel pulls it,
7
+ // NOT this module, so the tests stay free of the third-party spec). Like every other adapter here,
8
+ // this NEVER imports the library's React Slider component — it calls hooks off the React dispatcher
9
+ // and would crash under a non-React adapter.
10
+ //
11
+ // NOTHING here destructures `props`. Solid props are getters and a component body runs ONCE, so a
12
+ // destructure would freeze the slider at its mount-time values; every read sits inside an accessor.
13
+ //
14
+ // WHY THE TREE IS ASSEMBLED BY HAND INSTEAD OF THROUGH renderSlider(). Solid has no reconciler
15
+ // between what this file returns and the host nodes, so a tree whose SHAPE changes has to be
16
+ // rebuilt, and rebuilding the native slider would throw away the node identity its own thumb grip
17
+ // and every imperative command key on. renderSlider's Descriptor carries the step overlay as a
18
+ // child, i.e. its child count flips with `renderStepNumber` — which descriptorToSolid refuses
19
+ // (.claude/rules/solid-descriptor-bridge.md §2). So the wrapper is built once here, the native leaf
20
+ // is bridged once via renderSliderNative (the split core exposes for exactly this: React takes it
21
+ // too on its custom-marker path), and only the OVERLAY sits behind a rebuild boundary. The result
22
+ // is a stable native node across every overlay change — where React remounts it, since the wrapper's
23
+ // unkeyed children shift by one.
24
+ //
25
+ // The hand-written createElement / spread / insert calls below are what compiled JSX emits; this
26
+ // package cannot use JSX at all, because its tsconfig is shared with the React/Vue/Angular entries
27
+ // and TypeScript has one `jsx` setting per program. descriptor-to-solid.ts drives the renderer the
28
+ // same way for the same reason.
29
+ // createComponent comes from solid-js, not the renderer's re-export: the renderer's is typed to
30
+ // return its own host-node union, which cannot hold what a component returns (an Image is
31
+ // JSX.Element). Same function either way.
32
+ import { createComponent, createEffect, createMemo, createSignal, untrack, } from 'solid-js';
33
+ import { descriptorToSolid, Image, } from '@symbiote-native/solid';
34
+ import { createElement, createTextNode, insert, insertNode, setProp, spread, } from '@symbiote-native/solid/renderer';
35
+ import { resolveAccessibilityProps } from '@symbiote-native/components';
36
+ import { dlog, isSymbioteNode, } from '@symbiote-native/engine';
37
+ import { sanitizeSliderValue, resolveSliderDisabled, resolveSliderAccessibilityState, resolveSliderLowerLimit, resolveSliderUpperLimit, valueFromSliderEvent, shouldRenderStepsIndicator, resolveThumbTintColor, shouldPassNativeThumbImage, isInvalidLimitConfig, computeStepOptions, orderStepOptions, stepNumberFontSize, renderSliderNative, resolveSliderWrapperStyle, resolveStepsContainerStyle, renderStepsIndicator, STEP_INDICATOR_ELEMENT_STYLE, TRACK_MARK_CONTAINER_STYLE, THUMB_IMAGE_CONTAINER_STYLE, THUMB_IMAGE_STYLE, STEP_NUMBER_CONTAINER_STYLE, SLIDER_DEFAULT_MINIMUM_VALUE, SLIDER_DEFAULT_MAXIMUM_VALUE, SLIDER_DEFAULT_STEP, SLIDER_ON_CHANGE, SLIDER_ON_VALUE_CHANGE, SLIDER_ON_SLIDING_START, SLIDER_ON_SLIDING_COMPLETE, SLIDER_ON_ACCESSIBILITY_ACTION, } from '../../core/index.js';
38
+ // Read by the component itself; everything else (the track tints and images, thumbSize, tapToSeek,
39
+ // vertical, testID, the accessibility surface, a caller's `ref`) forwards onto the native leaf. The
40
+ // four JS callbacks MUST be stripped — a function prop reaching Fabric crashes Android's
41
+ // folly::dynamic serializer — and are re-supplied as the native event handlers in `passthrough`.
42
+ const HANDLED_PROPS = [
43
+ 'value',
44
+ 'minimumValue',
45
+ 'maximumValue',
46
+ 'step',
47
+ 'lowerLimit',
48
+ 'upperLimit',
49
+ 'disabled',
50
+ 'inverted',
51
+ 'thumbTintColor',
52
+ 'thumbImage',
53
+ 'accessibilityState',
54
+ 'renderStepNumber',
55
+ 'style',
56
+ 'class',
57
+ 'StepMarker',
58
+ 'onValueChange',
59
+ 'onSlidingStart',
60
+ 'onSlidingComplete',
61
+ 'onAccessibilityAction',
62
+ ];
63
+ function forwardProps(resolved) {
64
+ const forwarded = { ...resolved };
65
+ for (const key of HANDLED_PROPS)
66
+ delete forwarded[key];
67
+ return forwarded;
68
+ }
69
+ function isRecord(value) {
70
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
71
+ }
72
+ // Narrowing, not defensive: the renderer's createElement is typed over its IHostNode union (which
73
+ // includes the surface), while everything below needs a real host node. Same guard descriptorToSolid
74
+ // opens with.
75
+ function hostElement(tag) {
76
+ const node = createElement(tag);
77
+ if (!isSymbioteNode(node)) {
78
+ throw new Error(`Slider: ${tag} did not create a host node`);
79
+ }
80
+ return node;
81
+ }
82
+ // Solid core's own render-prop shape, line for line with `<Show>`: untrack + accessor, with an arity
83
+ // guard because `typeof fn === 'function'` cannot separate a render prop from the bare zero-argument
84
+ // accessor JSX.Element also permits. A 0-arity marker is handed back unread so `insert` wraps it in
85
+ // its own render effect; calling it here would freeze it at its first value.
86
+ function callStepMarker(marker, props) {
87
+ if (marker.length === 0)
88
+ return () => marker(props);
89
+ return untrack(() => marker(props));
90
+ }
91
+ export function createSlider(platform) {
92
+ return function Slider(props) {
93
+ // What native last reported, kept only to mark the active step. NOT the controlled value — the
94
+ // slider is uncontrolled during a drag, native owns the thumb and reports up.
95
+ const [reportedValue, setReportedValue] = createSignal(undefined);
96
+ // The measured wrapper width the step indicator lays out against; 0 until the first layout.
97
+ const [width, setWidth] = createSignal(0);
98
+ const handleValueChange = (event) => {
99
+ const next = valueFromSliderEvent(event);
100
+ if (next === undefined)
101
+ return;
102
+ setReportedValue(next);
103
+ props.onValueChange?.(next);
104
+ };
105
+ const handleSlidingStart = (event) => {
106
+ const next = valueFromSliderEvent(event);
107
+ if (next !== undefined)
108
+ props.onSlidingStart?.(next);
109
+ };
110
+ const handleSlidingComplete = (event) => {
111
+ const next = valueFromSliderEvent(event);
112
+ if (next !== undefined)
113
+ props.onSlidingComplete?.(next);
114
+ };
115
+ const handleAccessibilityAction = (event) => {
116
+ props.onAccessibilityAction?.(event);
117
+ };
118
+ const handleLayout = (event) => {
119
+ const layout = event.nativeEvent.layout;
120
+ if (isRecord(layout) && typeof layout.width === 'number') {
121
+ setWidth(layout.width);
122
+ }
123
+ };
124
+ const minimumValue = () => props.minimumValue ?? SLIDER_DEFAULT_MINIMUM_VALUE;
125
+ const maximumValue = () => props.maximumValue ?? SLIDER_DEFAULT_MAXIMUM_VALUE;
126
+ const step = () => props.step ?? SLIDER_DEFAULT_STEP;
127
+ const lowerLimit = () => resolveSliderLowerLimit(props.lowerLimit);
128
+ const upperLimit = () => resolveSliderUpperLimit(props.upperLimit);
129
+ createEffect(() => {
130
+ if (isInvalidLimitConfig(lowerLimit(), upperLimit())) {
131
+ dlog('Slider: lowerLimit must be smaller than upperLimit');
132
+ }
133
+ });
134
+ const hasStepMarker = () => props.StepMarker !== undefined;
135
+ const hasThumbImage = () => props.thumbImage !== undefined;
136
+ const showSteps = () => shouldRenderStepsIndicator(hasStepMarker(), props.renderStepNumber);
137
+ const view = () => {
138
+ // Folded BEFORE the handled props are read, exactly as React folds rawProps before it
139
+ // destructures: `aria-disabled` lands in accessibilityState, and reading the raw prop instead
140
+ // would hand the fold below an undefined and blank the a11y state the alias just set.
141
+ const resolved = resolveAccessibilityProps(props);
142
+ return {
143
+ value: sanitizeSliderValue(props.value),
144
+ minimumValue: minimumValue(),
145
+ maximumValue: maximumValue(),
146
+ step: step(),
147
+ lowerLimit: lowerLimit(),
148
+ upperLimit: upperLimit(),
149
+ disabled: resolveSliderDisabled(props.disabled, resolved.accessibilityState),
150
+ inverted: props.inverted ?? false,
151
+ thumbTintColor: resolveThumbTintColor(props.thumbTintColor, hasStepMarker(), hasThumbImage()),
152
+ // Passed to native raw — the engine runs the image processor derived from RNCSlider's
153
+ // ViewConfig — and only when no custom marker draws its own thumb (matches the library).
154
+ thumbImage: shouldPassNativeThumbImage(hasStepMarker(), hasThumbImage())
155
+ ? props.thumbImage
156
+ : undefined,
157
+ accessibilityState: resolveSliderAccessibilityState(props.disabled, resolved.accessibilityState),
158
+ width: width(),
159
+ style: props.style,
160
+ passthrough: {
161
+ ...forwardProps(resolved),
162
+ [SLIDER_ON_CHANGE]: handleValueChange,
163
+ [SLIDER_ON_VALUE_CHANGE]: handleValueChange,
164
+ [SLIDER_ON_SLIDING_START]: handleSlidingStart,
165
+ [SLIDER_ON_SLIDING_COMPLETE]: handleSlidingComplete,
166
+ [SLIDER_ON_ACCESSIBILITY_ACTION]: handleAccessibilityAction,
167
+ },
168
+ };
169
+ };
170
+ const options = createMemo(() => computeStepOptions(minimumValue(), maximumValue(), step(), platform.stepResolution));
171
+ const currentValue = createMemo(() => reportedValue() ?? sanitizeSliderValue(props.value) ?? minimumValue());
172
+ // THE REBUILD BOUNDARY (.claude/rules/solid-descriptor-bridge.md §5). Everything read TRACKED
173
+ // here changes the overlay's SHAPE — which overlay kind, how many cells, whether each cell
174
+ // carries a number — and the build itself is untracked so it does not also subscribe to every
175
+ // value/width read inside it. Drop the untrack and one drag rebuilds the whole indicator.
176
+ const overlay = createMemo(() => {
177
+ if (!showSteps())
178
+ return null;
179
+ const marker = props.StepMarker;
180
+ const cells = options();
181
+ const withNumbers = props.renderStepNumber === true;
182
+ const inverted = props.inverted ?? false;
183
+ const thumbImage = props.thumbImage;
184
+ // Read for its dependency, not its value: the DEFAULT overlay hands the thumb image to
185
+ // whichever cell matches the current value, so the cell's child count moves with that value —
186
+ // a shape change descriptorToSolid refuses. With no thumb image the shape is fixed and the
187
+ // bridge just re-props the same nodes.
188
+ if (marker === undefined && thumbImage !== undefined)
189
+ currentValue();
190
+ return untrack(() => marker === undefined
191
+ ? descriptorToSolid(() => renderStepsIndicator({
192
+ options: cells,
193
+ currentValue: currentValue(),
194
+ width: width(),
195
+ renderStepNumber: withNumbers,
196
+ thumbImage,
197
+ inverted,
198
+ platform,
199
+ }))
200
+ : buildCustomOverlay({
201
+ cells,
202
+ marker,
203
+ withNumbers,
204
+ inverted,
205
+ currentValue,
206
+ width,
207
+ thumbImage: () => props.thumbImage,
208
+ platform,
209
+ }));
210
+ });
211
+ // Built ONCE and kept by identity: descriptorToSolid wires every prop through a render effect on
212
+ // this same node, so a value/tint/limit change re-commits the existing native slider instead of
213
+ // replacing it — which would drop the thumb the user is dragging.
214
+ const leaf = descriptorToSolid(() => renderSliderNative(view(), platform));
215
+ const wrapper = hostElement('symbiote-view');
216
+ // A statically-present key set (all three are always emitted), so this needs no withStableKeys
217
+ // widening — `spread` alone has no removal pass for a key that vanishes.
218
+ spread(wrapper, () => ({
219
+ style: resolveSliderWrapperStyle(props.style, platform),
220
+ class: props.class,
221
+ onLayout: handleLayout,
222
+ }), true);
223
+ insertNode(wrapper, leaf);
224
+ // The overlay goes BEFORE the leaf, matching the z-order renderSlider composes; `leaf` is the
225
+ // marker `insert` positions against, which is also what keeps it from clearing the wrapper.
226
+ insert(wrapper, overlay, leaf);
227
+ return wrapper;
228
+ };
229
+ }
230
+ // The custom-marker overlay: the same flex row the agnostic default paints, but each mark hosts the
231
+ // caller's own marker element, mirroring the library's SliderTrackMark composition. Built by hand
232
+ // rather than as a Descriptor because a Descriptor holds only VALUES, and a marker is a live Solid
233
+ // subtree — the same boundary View and Pressable sit on.
234
+ function buildCustomOverlay(params) {
235
+ const fontSize = stepNumberFontSize(params.cells.length);
236
+ const ordered = orderStepOptions(params.cells, params.inverted);
237
+ const min = params.cells[0];
238
+ const max = params.cells[params.cells.length - 1];
239
+ const container = hostElement('symbiote-view');
240
+ // pointerEvents none so the overlay never eats the drag. `style` tracks the measured width, hence
241
+ // an accessor rather than a one-shot setProp.
242
+ spread(container, () => ({
243
+ pointerEvents: 'none',
244
+ testID: 'StepsIndicator-Container',
245
+ style: resolveStepsContainerStyle(params.width(), params.platform),
246
+ }), true);
247
+ ordered.forEach((value, index) => {
248
+ insertNode(container, buildStepCell({ value, index, min, max, fontSize, params }));
249
+ });
250
+ return container;
251
+ }
252
+ function buildStepCell(cell) {
253
+ const { params } = cell;
254
+ const element = hostElement('symbiote-view');
255
+ setProp(element, 'style', STEP_INDICATOR_ELEMENT_STYLE);
256
+ const track = hostElement('symbiote-view');
257
+ setProp(track, 'style', TRACK_MARK_CONTAINER_STYLE);
258
+ insertNode(element, track);
259
+ // A memo, not a bare accessor: several leaves inside one marker must see the same object, and an
260
+ // unchanged value must not hand out a fresh one.
261
+ const markerProps = createMemo(() => ({
262
+ stepMarked: cell.value === params.currentValue(),
263
+ currentValue: params.currentValue(),
264
+ index: cell.index,
265
+ min: cell.min,
266
+ max: cell.max,
267
+ }));
268
+ const markerElement = callStepMarker(params.marker, markerProps);
269
+ // One `insert` owns the track's children: the marker (stable identity, reconciled in place) and
270
+ // the thumb image, which only the cell matching the current value carries.
271
+ insert(track, () => [markerElement, buildThumbImage(cell)]);
272
+ if (params.withNumbers) {
273
+ const numberBox = hostElement('symbiote-view');
274
+ setProp(numberBox, 'style', STEP_NUMBER_CONTAINER_STYLE);
275
+ const label = hostElement('symbiote-text');
276
+ setProp(label, 'testID', `${cell.index}th-step`);
277
+ setProp(label, 'style', { fontSize: cell.fontSize });
278
+ insertNode(label, createTextNode(String(cell.value)));
279
+ insertNode(numberBox, label);
280
+ insertNode(element, numberBox);
281
+ }
282
+ return element;
283
+ }
284
+ function buildThumbImage(cell) {
285
+ const source = cell.params.thumbImage();
286
+ if (source === undefined || cell.value !== cell.params.currentValue()) {
287
+ return null;
288
+ }
289
+ const container = hostElement('symbiote-view');
290
+ setProp(container, 'style', THUMB_IMAGE_CONTAINER_STYLE);
291
+ setProp(container, 'testID', 'sliderTrackMark-thumbImage');
292
+ // The Image COMPONENT, not a raw symbiote-image: it resolves an asset id through RN's own
293
+ // resolveAssetSource, exactly as React's and Vue's custom overlays do.
294
+ insert(container, createComponent(Image, { source, style: THUMB_IMAGE_STYLE }));
295
+ return container;
296
+ }