panelui-native 0.53.0 → 0.56.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 (40) hide show
  1. package/README.md +4 -1
  2. package/lib/module/components/combobox/index.js +61 -7
  3. package/lib/module/components/combobox/index.js.map +1 -1
  4. package/lib/module/components/hex-chart/index.js +765 -0
  5. package/lib/module/components/hex-chart/index.js.map +1 -0
  6. package/lib/module/components/sortable/index.js +74 -12
  7. package/lib/module/components/sortable/index.js.map +1 -1
  8. package/lib/module/components/steps/index.js +95 -12
  9. package/lib/module/components/steps/index.js.map +1 -1
  10. package/lib/module/components/tag-input/index.js +435 -0
  11. package/lib/module/components/tag-input/index.js.map +1 -0
  12. package/lib/module/components/tour/index.js +661 -0
  13. package/lib/module/components/tour/index.js.map +1 -0
  14. package/lib/module/index.js +3 -0
  15. package/lib/module/index.js.map +1 -1
  16. package/lib/module/utils/chart.js +209 -0
  17. package/lib/module/utils/chart.js.map +1 -1
  18. package/lib/typescript/src/components/combobox/index.d.ts.map +1 -1
  19. package/lib/typescript/src/components/hex-chart/index.d.ts +277 -0
  20. package/lib/typescript/src/components/hex-chart/index.d.ts.map +1 -0
  21. package/lib/typescript/src/components/sortable/index.d.ts.map +1 -1
  22. package/lib/typescript/src/components/steps/index.d.ts +19 -0
  23. package/lib/typescript/src/components/steps/index.d.ts.map +1 -1
  24. package/lib/typescript/src/components/tag-input/index.d.ts +240 -0
  25. package/lib/typescript/src/components/tag-input/index.d.ts.map +1 -0
  26. package/lib/typescript/src/components/tour/index.d.ts +168 -0
  27. package/lib/typescript/src/components/tour/index.d.ts.map +1 -0
  28. package/lib/typescript/src/index.d.ts +3 -0
  29. package/lib/typescript/src/index.d.ts.map +1 -1
  30. package/lib/typescript/src/utils/chart.d.ts +98 -0
  31. package/lib/typescript/src/utils/chart.d.ts.map +1 -1
  32. package/package.json +1 -1
  33. package/src/components/combobox/index.tsx +65 -12
  34. package/src/components/hex-chart/index.tsx +1009 -0
  35. package/src/components/sortable/index.tsx +72 -16
  36. package/src/components/steps/index.tsx +103 -8
  37. package/src/components/tag-input/index.tsx +619 -0
  38. package/src/components/tour/index.tsx +933 -0
  39. package/src/index.ts +30 -0
  40. package/src/utils/chart.ts +236 -0
@@ -583,18 +583,32 @@ function SortableRoot({
583
583
 
584
584
  const indexOf = useCallback((id: string) => indices.get(id) ?? -1, [indices]);
585
585
 
586
+ /*
587
+ * Both maps are accumulated in a ref and then published, rather than built by
588
+ * reading the shared value back and spreading it. Every row reports its
589
+ * layout in the same batch on mount, and a write to `.value` is not visible
590
+ * to the next read in that batch — so a read-modify-write there has all the
591
+ * rows spreading the same empty map and only the last one surviving. A list
592
+ * that knows one row's height puts every slot a gap apart, and the first
593
+ * drag drops the row at the end of the list.
594
+ */
595
+ const measuredHeights = useRef<Record<string, number>>({});
596
+ const pinnedFlags = useRef<Record<string, boolean>>({});
597
+
586
598
  const measured = useCallback(
587
599
  (id: string, height: number) => {
588
- if (heights.value[id] === height) return;
589
- heights.value = { ...heights.value, [id]: height };
600
+ if (measuredHeights.current[id] === height) return;
601
+ measuredHeights.current = { ...measuredHeights.current, [id]: height };
602
+ heights.value = measuredHeights.current;
590
603
  },
591
604
  [heights]
592
605
  );
593
606
 
594
607
  const setPinned = useCallback(
595
608
  (id: string, next: boolean) => {
596
- if (Boolean(pinned.value[id]) === next) return;
597
- pinned.value = { ...pinned.value, [id]: next };
609
+ if (Boolean(pinnedFlags.current[id]) === next) return;
610
+ pinnedFlags.current = { ...pinnedFlags.current, [id]: next };
611
+ pinned.value = pinnedFlags.current;
598
612
  },
599
613
  [pinned]
600
614
  );
@@ -1031,10 +1045,15 @@ function SortableItem({
1031
1045
  * drop unreported; `seq` is what tells the two cases apart, because
1032
1046
  * the only interruption that should be ignored is the row being picked
1033
1047
  * up again.
1048
+ *
1049
+ * `activeId` is checked as well as `seq` because reporting the drop is
1050
+ * itself what interrupts the spring: the caller applies the reorder,
1051
+ * and the reset that follows puts `translate` back to rest, which ends
1052
+ * the animation and calls this a second time under the same `seq`.
1034
1053
  */
1035
1054
  const land = () => {
1036
1055
  'worklet';
1037
- if (dragSeq.value !== seq) return;
1056
+ if (dragSeq.value !== seq || activeId.value === null) return;
1038
1057
  activeId.value = null;
1039
1058
  runOnJS(notifySettled)(id);
1040
1059
  };
@@ -1142,17 +1161,27 @@ function SortableItem({
1142
1161
  * no way to discover it from the row. Moving by whole slots is published as
1143
1162
  * an accessibility action instead, which is the only path to reordering for
1144
1163
  * someone who is not dragging anything.
1164
+ *
1165
+ * The actions sit wherever the drag does. A handle list keeps them on the
1166
+ * grip, which is an element in its own right; put here they would never be
1167
+ * offered, because the actions of a view that is not itself an accessibility
1168
+ * element are not reachable, and a row full of text is not one. A long-press
1169
+ * list has no grip and gives the whole row to the drag, so the row becomes
1170
+ * the element — which is what a screen reader wants from a row in any case.
1145
1171
  */
1146
- const a11y = locked
1147
- ? undefined
1148
- : [
1172
+ const carriesActions = activation === 'longPress' && !locked;
1173
+
1174
+ const a11y = carriesActions
1175
+ ? [
1149
1176
  { name: 'moveUp', label: 'Move up' },
1150
1177
  { name: 'moveDown', label: 'Move down' },
1151
- ];
1178
+ ]
1179
+ : undefined;
1152
1180
 
1153
1181
  const row = (
1154
1182
  <Animated.View
1155
1183
  onLayout={onLayout}
1184
+ accessible={carriesActions}
1156
1185
  accessibilityActions={a11y}
1157
1186
  onAccessibilityAction={(event) => {
1158
1187
  if (event.nativeEvent.actionName === 'moveUp') step(id, -1);
@@ -1224,7 +1253,7 @@ function SortableHandle({
1224
1253
  accessibilityLabel = 'Drag to reorder',
1225
1254
  ...props
1226
1255
  }: SortableHandleProps) {
1227
- const { activation, disabled: rootDisabled } = useSortableRoot('Sortable.Handle');
1256
+ const { activation, disabled: rootDisabled, step } = useSortableRoot('Sortable.Handle');
1228
1257
  const item = useContext(SortableItemContext);
1229
1258
 
1230
1259
  /*
@@ -1235,17 +1264,44 @@ function SortableHandle({
1235
1264
  const muted = useCSSVariable('--color-muted-foreground');
1236
1265
  const tint = typeof muted === 'string' ? muted : undefined;
1237
1266
 
1267
+ const locked = rootDisabled || item?.disabled;
1268
+
1269
+ /*
1270
+ * `adjustable` promises an element that answers a swipe up or down, and the
1271
+ * promise was never kept: the grip published the role and nothing else, so
1272
+ * the one part of a row a screen reader could reach did nothing at all.
1273
+ * Moving by whole slots is what it was always meant to do. The same move is
1274
+ * offered as a named action too, because a swipe says nothing about which
1275
+ * way the row is going to travel.
1276
+ */
1277
+ const move = (delta: number) => {
1278
+ if (locked || !item) return;
1279
+ step(item.id, delta);
1280
+ };
1281
+
1238
1282
  const glyph = (
1239
1283
  <View
1240
1284
  accessible
1241
1285
  accessibilityRole="adjustable"
1242
1286
  accessibilityLabel={accessibilityLabel}
1243
- accessibilityState={{ disabled: rootDisabled || item?.disabled }}
1244
- className={cn(
1245
- 'items-center justify-center px-2 py-1.5',
1246
- (rootDisabled || item?.disabled) && 'opacity-40',
1247
- className
1248
- )}
1287
+ accessibilityState={{ disabled: locked }}
1288
+ accessibilityValue={item ? { text: `Position ${item.index + 1}` } : undefined}
1289
+ accessibilityActions={
1290
+ locked
1291
+ ? undefined
1292
+ : [
1293
+ { name: 'increment' },
1294
+ { name: 'decrement' },
1295
+ { name: 'moveUp', label: 'Move up' },
1296
+ { name: 'moveDown', label: 'Move down' },
1297
+ ]
1298
+ }
1299
+ onAccessibilityAction={(event) => {
1300
+ const action = event.nativeEvent.actionName;
1301
+ if (action === 'increment' || action === 'moveUp') move(-1);
1302
+ if (action === 'decrement' || action === 'moveDown') move(1);
1303
+ }}
1304
+ className={cn('items-center justify-center px-2 py-1.5', locked && 'opacity-40', className)}
1249
1305
  {...props}
1250
1306
  >
1251
1307
  <IconColorProvider color={tint}>
@@ -14,10 +14,24 @@
14
14
  * Steps does not own your flow: it reflects whatever step your app says is
15
15
  * active. Pass `value` to control it, or `defaultValue` to let it manage
16
16
  * its own.
17
+ *
18
+ * The connectors are the component's job, not yours. The root counts the items
19
+ * it holds and each one draws the connector to the next, so a stepper is just
20
+ * its steps — there is no separator to forget, mis-order, or leave dangling
21
+ * past the last stop. An item that contains its own `Steps.Separator` keeps it
22
+ * and gets no second one, so hand-placed connectors still work; `separators`
23
+ * turns the automatic ones off wholesale.
24
+ *
25
+ * Knowing the count is also what lets a step say where it sits. A screen reader
26
+ * reaching the middle of a wizard hears "Payment, step 2 of 3, completed" —
27
+ * the position and the state, which are the two things the circle and its fill
28
+ * convey to everyone else.
17
29
  */
18
30
  import {
31
+ Children,
19
32
  createContext,
20
33
  forwardRef,
34
+ isValidElement,
21
35
  useCallback,
22
36
  useContext,
23
37
  useMemo,
@@ -91,6 +105,7 @@ interface StepsContextValue {
91
105
  activeStep: number;
92
106
  setActiveStep: (step: number) => void;
93
107
  orientation: StepsOrientation;
108
+ separators: boolean;
94
109
  }
95
110
 
96
111
  interface StepItemContextValue {
@@ -100,8 +115,23 @@ interface StepItemContextValue {
100
115
  isLoading: boolean;
101
116
  }
102
117
 
118
+ /**
119
+ * Where an item sits among its siblings, published by the root.
120
+ *
121
+ * Deliberately not the item's own `step` prop: that is the author's numbering
122
+ * of the flow and may skip, repeat or start anywhere, while the connector and
123
+ * the "2 of 3" announcement both need the position in the row as rendered.
124
+ * Absent when an item is used outside a root that maps its children, which is
125
+ * why every reader of it has a fallback.
126
+ */
127
+ interface StepPositionContextValue {
128
+ position: number;
129
+ total: number;
130
+ }
131
+
103
132
  const StepsContext = createContext<StepsContextValue | null>(null);
104
133
  const StepItemContext = createContext<StepItemContextValue | null>(null);
134
+ const StepPositionContext = createContext<StepPositionContextValue | null>(null);
105
135
 
106
136
  function useSteps(component: string): StepsContextValue {
107
137
  const context = useContext(StepsContext);
@@ -127,6 +157,13 @@ export interface StepsProps extends ViewProps {
127
157
  value?: number;
128
158
  onValueChange?: (value: number) => void;
129
159
  orientation?: StepsOrientation;
160
+ /**
161
+ * Draw the connector between one item and the next. On by default — an item
162
+ * that holds its own `Steps.Separator` is left alone either way, so this is
163
+ * for a stepper that wants no connectors at all rather than for one that
164
+ * places them by hand.
165
+ */
166
+ separators?: boolean;
130
167
  children?: ReactNode;
131
168
  }
132
169
 
@@ -138,6 +175,7 @@ const StepsRoot = forwardRef<View, StepsProps>(
138
175
  value,
139
176
  onValueChange,
140
177
  orientation = 'horizontal',
178
+ separators = true,
141
179
  children,
142
180
  ...props
143
181
  },
@@ -156,16 +194,37 @@ const StepsRoot = forwardRef<View, StepsProps>(
156
194
  );
157
195
 
158
196
  const context = useMemo(
159
- () => ({ activeStep, setActiveStep, orientation }),
160
- [activeStep, setActiveStep, orientation]
197
+ () => ({ activeStep, setActiveStep, orientation, separators }),
198
+ [activeStep, setActiveStep, orientation, separators]
161
199
  );
162
200
 
163
201
  const { root } = stepsVariants({ orientation });
164
202
 
203
+ /*
204
+ * Items are counted here rather than registered by each one on mount,
205
+ * because the count has to be right on the first frame: a connector that
206
+ * appears after the last item and disappears once the registrations land
207
+ * is a visible flicker on every mount. Reading the children gives the whole
208
+ * row at once, and a Provider adds no host view, so wrapping an item in one
209
+ * leaves the flex layout exactly as it was.
210
+ */
211
+ const nodes = Children.toArray(textChildren(children));
212
+ const total = nodes.filter((node) => isValidElement(node) && node.type === StepsItem).length;
213
+ let position = -1;
214
+
165
215
  return (
166
216
  <StepsContext.Provider value={context}>
167
217
  <View ref={ref} className={root({ className })} {...props}>
168
- {textChildren(children)}
218
+ {nodes.map((node) => {
219
+ if (!isValidElement(node) || node.type !== StepsItem) return node;
220
+ position += 1;
221
+ const placement = { position, total };
222
+ return (
223
+ <StepPositionContext.Provider key={node.key ?? position} value={placement}>
224
+ {node}
225
+ </StepPositionContext.Provider>
226
+ );
227
+ })}
169
228
  </View>
170
229
  </StepsContext.Provider>
171
230
  );
@@ -190,7 +249,8 @@ const StepsItem = forwardRef<View, StepsItemProps>(
190
249
  { className, step, completed = false, disabled = false, loading = false, children, ...props },
191
250
  ref
192
251
  ) => {
193
- const { activeStep, orientation } = useSteps('Steps.Item');
252
+ const { activeStep, orientation, separators } = useSteps('Steps.Item');
253
+ const placement = useContext(StepPositionContext);
194
254
 
195
255
  const isLoading = loading && step === activeStep;
196
256
  const state: StepState =
@@ -209,10 +269,23 @@ const StepsItem = forwardRef<View, StepsItemProps>(
209
269
 
210
270
  const { item } = stepsVariants({ orientation, state });
211
271
 
272
+ /*
273
+ * The last item has nothing to connect to, and one placed by hand is the
274
+ * author's — adding a second beside it would double the line rather than
275
+ * replace it. Only the top level is inspected, which is where a connector
276
+ * has to be anyway: it is a sibling of the trigger, not something buried
277
+ * inside it.
278
+ */
279
+ const isLast = placement ? placement.position === placement.total - 1 : true;
280
+ const hasOwnSeparator = Children.toArray(children).some(
281
+ (child) => isValidElement(child) && child.type === StepsSeparator
282
+ );
283
+
212
284
  return (
213
285
  <StepItemContext.Provider value={context}>
214
286
  <View ref={ref} className={item({ className })} {...props}>
215
287
  {textChildren(children)}
288
+ {separators && !isLast && !hasOwnSeparator ? <StepsSeparator /> : null}
216
289
  </View>
217
290
  </StepItemContext.Provider>
218
291
  );
@@ -225,18 +298,38 @@ export interface StepsTriggerProps extends ViewProps {
225
298
  children?: ReactNode;
226
299
  }
227
300
 
301
+ /** What each state is called when a screen reader reaches the step. */
302
+ const STATE_WORDS: Record<StepState, string> = {
303
+ completed: 'completed',
304
+ active: 'current step',
305
+ loading: 'in progress',
306
+ inactive: 'not started',
307
+ };
308
+
228
309
  /** Makes its item selectable. Omit it for a read-only stepper. */
229
310
  const StepsTrigger = forwardRef<View, StepsTriggerProps>(
230
311
  ({ className, children, ...props }, ref) => {
231
312
  const { setActiveStep, orientation } = useSteps('Steps.Trigger');
232
313
  const { step, state, isDisabled } = useStepItem('Steps.Trigger');
314
+ const placement = useContext(StepPositionContext);
233
315
  const { trigger } = stepsVariants({ orientation, state, isDisabled });
234
316
 
317
+ /*
318
+ * Said as a value rather than a label, because the label is the step's own
319
+ * title and the circle beside it — text the trigger already merges. An
320
+ * `accessibilityLabel` here would replace all of that with the position,
321
+ * trading the name of the step for its number; a value is read after it.
322
+ */
323
+ const position = placement
324
+ ? `step ${placement.position + 1} of ${placement.total}, ${STATE_WORDS[state]}`
325
+ : STATE_WORDS[state];
326
+
235
327
  return (
236
328
  <Pressable
237
329
  ref={ref}
238
330
  accessibilityRole="button"
239
331
  accessibilityState={{ disabled: isDisabled, selected: state === 'active' }}
332
+ accessibilityValue={{ text: position }}
240
333
  disabled={isDisabled}
241
334
  onPress={() => setActiveStep(step)}
242
335
  className={trigger({ className })}
@@ -310,11 +403,13 @@ export interface StepsSeparatorProps extends ViewProps {
310
403
  }
311
404
 
312
405
  /**
313
- * The connector between two steps. Fills with the primary colour once the
314
- * step before it is complete.
406
+ * The connector between two steps. Fills with the primary colour once the step
407
+ * before it is complete.
315
408
  *
316
- * Place it inside a Steps.Item so it can read that item's state — the
317
- * separator after step 1 goes solid when step 1 is done.
409
+ * Every item draws one automatically, so this is only worth writing to dress a
410
+ * particular connector — an item that holds its own keeps it and gets no
411
+ * second. It belongs inside a `Steps.Item` either way, so it can read that
412
+ * item's state: the connector after step 1 goes solid when step 1 is done.
318
413
  */
319
414
  const StepsSeparator = forwardRef<View, StepsSeparatorProps>(
320
415
  ({ className, ...props }, ref) => {