jfs-components 0.0.63 → 0.0.64
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.
- package/CHANGELOG.md +23 -0
- package/lib/commonjs/components/Drawer/Drawer.js +107 -47
- package/lib/commonjs/components/Section/Section.js +280 -58
- package/lib/commonjs/icons/registry.js +1 -1
- package/lib/module/components/Drawer/Drawer.js +107 -47
- package/lib/module/components/Section/Section.js +280 -58
- package/lib/module/icons/registry.js +1 -1
- package/lib/typescript/src/components/Section/Section.d.ts +42 -1
- package/lib/typescript/src/icons/registry.d.ts +1 -1
- package/package.json +1 -1
- package/src/components/Drawer/Drawer.tsx +122 -57
- package/src/components/Section/Section.tsx +411 -71
- package/src/icons/registry.ts +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,29 @@ All notable changes to this project are documented in this file.
|
|
|
4
4
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
6
6
|
|
|
7
|
+
## [0.0.64] - 2026-04-20
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- **`Section.Bento` — expandable grid:** New built-in expand/collapse UX. Pass `collapsedCount` (default `4`) plus the full set of cells in `navSlot`, and `Section.Bento` auto-injects a toggle cell, owns the `expanded` boolean (uncontrolled), and animates extra cells in/out with a staggered fade cascade. Supports controlled mode via `expanded` + `onExpandedChange`, customizable labels (`toggleMoreLabel`, `toggleLessLabel`) and icons (`toggleMoreIcon`, `toggleLessIcon`), and a `renderToggle` escape hatch for non-`ListItem` toggles. The toggle is only injected when `navSlot.length > collapsedCount`, so existing usage at or below the threshold renders unchanged.
|
|
12
|
+
- **Drawer drop-shadow tokens:** New design tokens `drawer/shadow/primary/{offsetX,offsetY,blur,color}` and `drawer/shadow/secondary/{offsetX,color}` resolve a layered Figma drop shadow. Web stacks both shadows via `boxShadow`; iOS applies the primary shadow natively; Android uses elevation.
|
|
13
|
+
|
|
14
|
+
### Changed
|
|
15
|
+
|
|
16
|
+
- **`SlotGrid` sizing — first-row-anchored:** `SlotGrid` (used by `Section.Bento`) now measures only the first row's cells once on mount and applies that max width to every cell in every row. Previously every cell was measured every render, which caused width jumps when the cell count changed (expand/collapse) and could cancel `Animated.View` `entering` cascades by re-rendering them in the same React batch as the mount. Result: stable cell width across toggles, no layout shift, animations play uninterrupted. Edge-flush behavior (first cell flush-left, last cell flush-right) is preserved via `justify-content: space-between`.
|
|
17
|
+
- **`Section.Bento` height transition:** The section grows and shrinks via an explicit measured-height spring (`overflow: 'hidden'` clip + `withSpring`) instead of `LinearTransition`. Cells inside are never resized during the animation — only the container's clip rectangle interpolates.
|
|
18
|
+
- **Drawer gesture policy:** Pan now requires a 10px vertical drag to activate (`activeOffsetY([-10, 10])`) and surrenders the gesture entirely after ~16px of horizontal movement (`failOffsetX([-16, 16])`). A defense-in-depth check in `onUpdate` skips Y translation on frames where horizontal motion dominates. This lets horizontal children (carousels, sliders, horizontal `FlatList`s) inside the drawer scroll cleanly without the sheet also translating.
|
|
19
|
+
|
|
20
|
+
### Fixed
|
|
21
|
+
|
|
22
|
+
- **Drawer iOS shadow clipping:** The sheet now renders content inside an inner clip layer so `overflow: 'hidden'` no longer trims the outer view's drop shadow on iOS.
|
|
23
|
+
|
|
24
|
+
### Accessibility
|
|
25
|
+
|
|
26
|
+
- All new `Section.Bento` animations honor the OS reduce-motion setting via Reanimated's `ReduceMotion.System`.
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
7
30
|
## [0.0.63] - 2026-04-20
|
|
8
31
|
|
|
9
32
|
### Performance
|
|
@@ -131,7 +131,17 @@ function Drawer({
|
|
|
131
131
|
const updateMode = (0, _react.useCallback)(newMode => {
|
|
132
132
|
setMode(newMode);
|
|
133
133
|
}, []);
|
|
134
|
-
|
|
134
|
+
|
|
135
|
+
// Gesture policy:
|
|
136
|
+
// • activeOffsetY: require a clear *vertical* drag (10px) before this
|
|
137
|
+
// pan claims the gesture. Matches typical iOS scroll activation feel.
|
|
138
|
+
// • failOffsetX: if the finger crosses ~16px horizontally *before* we
|
|
139
|
+
// activate, surrender the gesture entirely so any horizontal child
|
|
140
|
+
// (FlatList horizontal, swiper, slider, etc.) can scroll cleanly
|
|
141
|
+
// without the drawer also translating on Y.
|
|
142
|
+
// • simultaneousWithExternalGesture(scrollRef): cooperate with the
|
|
143
|
+
// drawer's own internal vertical ScrollView for nested scrolling.
|
|
144
|
+
const gesture = _reactNativeGestureHandler.Gesture.Pan().simultaneousWithExternalGesture(scrollRef).activeOffsetY([-10, 10]).failOffsetX([-16, 16]).onStart(() => {
|
|
135
145
|
context.value = {
|
|
136
146
|
y: translateY.value
|
|
137
147
|
};
|
|
@@ -140,6 +150,16 @@ function Drawer({
|
|
|
140
150
|
prevAtTop.value = scrollY.value <= 1;
|
|
141
151
|
scrollTopTranslationOffset.value = 0;
|
|
142
152
|
}).onUpdate(event => {
|
|
153
|
+
// Defense-in-depth: even after vertical activation, if the *current*
|
|
154
|
+
// motion is dominantly horizontal (e.g., the user activated with a
|
|
155
|
+
// small Y nudge and then curved into a horizontal swipe on a child
|
|
156
|
+
// carousel), don't translate the drawer this frame. failOffsetX
|
|
157
|
+
// already prevents activation in pure-horizontal swipes; this guards
|
|
158
|
+
// the diagonal-then-horizontal case.
|
|
159
|
+
if (Math.abs(event.translationX) > Math.abs(event.translationY) * 1.5) {
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
|
|
143
163
|
// Logic for nested scrolling:
|
|
144
164
|
// If we are at the expanded position (minTranslateY) AND content is
|
|
145
165
|
// scrolled down (scrollY > 0), let the ScrollView handle the gesture.
|
|
@@ -251,71 +271,108 @@ function Drawer({
|
|
|
251
271
|
const titleWeight = (0, _figmaVariablesResolver.getVariableByName)('drawer/title/fontWeight', modes) || '700';
|
|
252
272
|
const titleLineHeight = (0, _figmaVariablesResolver.getVariableByName)('drawer/title/lineHeight', modes) || 17;
|
|
253
273
|
const titlePaddingBottom = (0, _figmaVariablesResolver.getVariableByName)('drawer/titleWrap/padding/bottom', modes) || 8;
|
|
274
|
+
|
|
275
|
+
// Drop shadow — Figma layers two shadows (primary + secondary) sharing
|
|
276
|
+
// the same offsetY/blur but with their own offsetX and color.
|
|
277
|
+
const shadowPrimaryOffsetX = (0, _figmaVariablesResolver.getVariableByName)('drawer/shadow/primary/offsetX', modes) ?? 0;
|
|
278
|
+
const shadowPrimaryOffsetY = (0, _figmaVariablesResolver.getVariableByName)('drawer/shadow/primary/offsetY', modes) ?? 16;
|
|
279
|
+
const shadowPrimaryBlur = (0, _figmaVariablesResolver.getVariableByName)('drawer/shadow/primary/blur', modes) ?? 48;
|
|
280
|
+
const shadowPrimaryColor = (0, _figmaVariablesResolver.getVariableByName)('drawer/shadow/primary/color', modes) ?? 'rgba(12, 13, 16, 0.16)';
|
|
281
|
+
const shadowSecondaryOffsetX = (0, _figmaVariablesResolver.getVariableByName)('drawer/shadow/secondary/offsetX', modes) ?? 0;
|
|
282
|
+
const shadowSecondaryColor = (0, _figmaVariablesResolver.getVariableByName)('drawer/shadow/secondary/color', modes) ?? 'rgba(12, 13, 16, 0.12)';
|
|
283
|
+
|
|
284
|
+
// Cross-platform shadow style. Web supports stacking two shadows via
|
|
285
|
+
// boxShadow. iOS only supports a single native shadow per view, so we
|
|
286
|
+
// apply the more prominent (primary) one. Android uses elevation.
|
|
287
|
+
const shadowStyle = _reactNative.Platform.select({
|
|
288
|
+
web: {
|
|
289
|
+
boxShadow: `${shadowSecondaryOffsetX}px ${shadowPrimaryOffsetY}px ${shadowPrimaryBlur}px 0px ${shadowSecondaryColor}, ` + `${shadowPrimaryOffsetX}px ${shadowPrimaryOffsetY}px ${shadowPrimaryBlur}px 0px ${shadowPrimaryColor}`
|
|
290
|
+
},
|
|
291
|
+
ios: {
|
|
292
|
+
shadowColor: shadowPrimaryColor,
|
|
293
|
+
shadowOffset: {
|
|
294
|
+
width: shadowPrimaryOffsetX,
|
|
295
|
+
height: shadowPrimaryOffsetY
|
|
296
|
+
},
|
|
297
|
+
shadowOpacity: 1,
|
|
298
|
+
shadowRadius: shadowPrimaryBlur / 2
|
|
299
|
+
},
|
|
300
|
+
android: {
|
|
301
|
+
elevation: 16
|
|
302
|
+
},
|
|
303
|
+
default: {}
|
|
304
|
+
});
|
|
254
305
|
const defaultAccessibilityLabel = accessibilityLabel || title || 'Drawer';
|
|
255
306
|
return /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNativeGestureHandler.GestureHandlerRootView, {
|
|
256
307
|
style: [styles.host, style],
|
|
257
308
|
pointerEvents: "box-none",
|
|
258
309
|
children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNativeGestureHandler.GestureDetector, {
|
|
259
310
|
gesture: gesture,
|
|
260
|
-
children: /*#__PURE__*/(0, _jsxRuntime.
|
|
311
|
+
children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNativeReanimated.default.View, {
|
|
261
312
|
style: [styles.sheet, {
|
|
262
313
|
// Constraint the height strictly to the expanded height
|
|
263
314
|
// This ensures the ScrollView has a finite frame to scroll within
|
|
264
315
|
height: expandedHeight,
|
|
265
316
|
backgroundColor,
|
|
266
317
|
borderTopLeftRadius: radius,
|
|
267
|
-
borderTopRightRadius: radius
|
|
268
|
-
|
|
269
|
-
paddingRight,
|
|
270
|
-
paddingBottom,
|
|
271
|
-
rowGap: drawerGap
|
|
272
|
-
}, sheetStyle, animatedStyle],
|
|
318
|
+
borderTopRightRadius: radius
|
|
319
|
+
}, shadowStyle, sheetStyle, animatedStyle],
|
|
273
320
|
accessible: true,
|
|
274
321
|
...(_reactNative.Platform.OS === 'web' ? {
|
|
275
322
|
accessibilityRole: 'dialog'
|
|
276
323
|
} : undefined),
|
|
277
324
|
accessibilityLabel: undefined,
|
|
278
325
|
accessibilityHint: accessibilityHint || 'Swipe up to expand, swipe down to collapse',
|
|
279
|
-
children:
|
|
280
|
-
style: [styles.
|
|
281
|
-
|
|
326
|
+
children: /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
|
|
327
|
+
style: [styles.sheetInner, {
|
|
328
|
+
borderTopLeftRadius: radius,
|
|
329
|
+
borderTopRightRadius: radius,
|
|
330
|
+
paddingLeft,
|
|
331
|
+
paddingRight,
|
|
332
|
+
paddingBottom,
|
|
333
|
+
rowGap: drawerGap
|
|
282
334
|
}],
|
|
283
|
-
children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
|
|
335
|
+
children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
|
|
336
|
+
style: [styles.handleArea, !title && !header && {
|
|
337
|
+
paddingBottom: 0
|
|
338
|
+
}],
|
|
339
|
+
children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
|
|
340
|
+
style: [{
|
|
341
|
+
backgroundColor: handleColor,
|
|
342
|
+
width: handleWidth,
|
|
343
|
+
height: handleHeight,
|
|
344
|
+
borderRadius: handleRadius
|
|
345
|
+
}]
|
|
346
|
+
})
|
|
347
|
+
}), header, title && /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
|
|
284
348
|
style: [{
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
overScrollMode: "always",
|
|
313
|
-
onScroll: (0, _reactNativeReanimated.useAnimatedScrollHandler)(event => {
|
|
314
|
-
scrollY.value = event.contentOffset.y;
|
|
315
|
-
}),
|
|
316
|
-
scrollEventThrottle: 16,
|
|
317
|
-
children: children
|
|
318
|
-
})]
|
|
349
|
+
color: titleColor,
|
|
350
|
+
fontSize: titleSize,
|
|
351
|
+
fontWeight: titleWeight,
|
|
352
|
+
lineHeight: titleLineHeight,
|
|
353
|
+
marginBottom: titlePaddingBottom
|
|
354
|
+
}],
|
|
355
|
+
children: title
|
|
356
|
+
}), /*#__PURE__*/(0, _jsxRuntime.jsx)(AnimatedScrollView, {
|
|
357
|
+
ref: scrollRef,
|
|
358
|
+
style: [styles.content, contentStyle],
|
|
359
|
+
contentContainerStyle: [{
|
|
360
|
+
paddingBottom: paddingBottom + bottomInset,
|
|
361
|
+
gap: drawerGap,
|
|
362
|
+
flexDirection: 'column',
|
|
363
|
+
alignItems: 'stretch'
|
|
364
|
+
}, contentContainerStyle],
|
|
365
|
+
showsVerticalScrollIndicator: showsVerticalScrollIndicator,
|
|
366
|
+
animatedProps: animatedScrollProps,
|
|
367
|
+
alwaysBounceVertical: false,
|
|
368
|
+
overScrollMode: "always",
|
|
369
|
+
onScroll: (0, _reactNativeReanimated.useAnimatedScrollHandler)(event => {
|
|
370
|
+
scrollY.value = event.contentOffset.y;
|
|
371
|
+
}),
|
|
372
|
+
scrollEventThrottle: 16,
|
|
373
|
+
children: children
|
|
374
|
+
})]
|
|
375
|
+
})
|
|
319
376
|
})
|
|
320
377
|
})
|
|
321
378
|
});
|
|
@@ -333,7 +390,10 @@ const styles = _reactNative.StyleSheet.create({
|
|
|
333
390
|
sheet: {
|
|
334
391
|
width: '100%',
|
|
335
392
|
position: 'absolute',
|
|
336
|
-
top: 0
|
|
393
|
+
top: 0
|
|
394
|
+
},
|
|
395
|
+
sheetInner: {
|
|
396
|
+
flex: 1,
|
|
337
397
|
overflow: 'hidden'
|
|
338
398
|
},
|
|
339
399
|
handleArea: {
|