@swan-io/lake 2.5.0 → 2.6.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@swan-io/lake",
3
- "version": "2.5.0",
3
+ "version": "2.6.1",
4
4
  "engines": {
5
5
  "node": ">=18.0.0",
6
6
  "yarn": "^1.22.0"
@@ -27,29 +27,29 @@
27
27
  "license": "MIT",
28
28
  "dependencies": {
29
29
  "@popperjs/core": "^2.11.8",
30
- "@react-three/drei": "^9.77.0",
31
- "@react-three/fiber": "^8.13.3",
30
+ "@react-three/drei": "^9.77.10",
31
+ "@react-three/fiber": "^8.13.4",
32
32
  "@swan-io/boxed": "^1.0.2",
33
- "@swan-io/chicane": "^1.4.0",
34
- "dayjs": "^1.11.8",
33
+ "@swan-io/chicane": "^1.4.1",
34
+ "dayjs": "^1.11.9",
35
35
  "polished": "^4.2.2",
36
- "prism-react-renderer": "^2.0.5",
36
+ "prism-react-renderer": "^2.0.6",
37
37
  "react": "^18.2.0",
38
38
  "react-atomic-state": "^1.2.7",
39
39
  "react-dom": "^18.2.0",
40
- "react-native-web": "^0.19.5",
40
+ "react-native-web": "^0.19.6",
41
41
  "react-popper": "^2.3.0",
42
42
  "react-ux-form": "^1.3.0",
43
43
  "rifm": "^0.12.1",
44
- "three": "^0.153.0",
44
+ "three": "^0.154.0",
45
45
  "ts-dedent": "^2.2.0",
46
46
  "ts-pattern": "^5.0.1",
47
47
  "urql": "^4.0.4",
48
48
  "uuid": "^9.0.0"
49
49
  },
50
50
  "devDependencies": {
51
- "@types/react": "^18.2.12",
52
- "@types/react-dom": "^18.2.5",
51
+ "@types/react": "^18.2.14",
52
+ "@types/react-dom": "^18.2.6",
53
53
  "@types/react-native": "^0.72.2",
54
54
  "@types/three": "^0.152.1",
55
55
  "@types/uuid": "^9.0.2",
@@ -50,5 +50,5 @@ export const LakeCheckbox = ({ value, color = "current", disabled = false, isErr
50
50
  ], children: [value === true && (_jsx(Svg, { viewBox: "0 0 16 16", children: _jsx(Path, { d: "m3.5 7.5 2.8 3.4 5.6-6.7", stroke: colors[color].contrast, strokeWidth: 1.5, fill: "none", strokeLinecap: "round", strokeLinejoin: "round", strokeDasharray: "20", strokeDashoffset: shouldAnimate ? "20" : "0", children: shouldAnimate && (_jsx(Animate, { attributeName: "stroke-dashoffset", values: "20;0", dur: "150ms", begin: "150ms", fill: "freeze" })) }) })), value === "mixed" && (_jsx(View, { style: [styles.mixed, { backgroundColor: colors[color].contrast }] }))] }));
51
51
  };
52
52
  export const LakeLabelledCheckbox = ({ value, color, label, onValueChange, disabled = false, isError = false, }) => {
53
- return (_jsxs(Pressable, { "aria-checked": value, style: styles.labelled, onPress: () => onValueChange(value === true ? false : true), disabled: disabled, children: [_jsx(LakeCheckbox, { value: value, color: color, disabled: disabled, isError: isError }), _jsx(Space, { width: 8 }), _jsx(LakeText, { color: colors.gray[900], userSelect: "none", children: label })] }));
53
+ return (_jsxs(Pressable, { role: "checkbox", "aria-checked": value, style: styles.labelled, onPress: () => onValueChange(value === true ? false : true), disabled: disabled, children: [_jsx(LakeCheckbox, { value: value, color: color, disabled: disabled, isError: isError }), _jsx(Space, { width: 8 }), _jsx(LakeText, { color: colors.gray[900], userSelect: "none", children: label })] }));
54
54
  };
@@ -82,5 +82,5 @@ export const Switch = memo(forwardRef(({ value, disabled = false, onValueChange
82
82
  useNativeDriver: false,
83
83
  }).start();
84
84
  }, [animation, animatedValue]);
85
- return (_jsx(Pressable, { ref: ref, role: "switch", disabled: disabled, onPress: () => onValueChange?.(!value), children: ({ hovered }) => (_jsxs(_Fragment, { children: [_jsx(View, { style: [styles.shadow, hovered && styles.opaque] }), _jsx(View, { style: [styles.base, value && styles.active, disabled && styles.disabled], children: _jsx(View, { ref: buttonRef, style: styles.button, children: _jsx(Icon, { color: colors.positive[400], name: "checkmark-filled", size: 10, style: [styles.icon, value && styles.opaque] }) }) })] })) }));
85
+ return (_jsx(Pressable, { ref: ref, role: "switch", "aria-checked": value, disabled: disabled, onPress: () => onValueChange?.(!value), children: ({ hovered }) => (_jsxs(_Fragment, { children: [_jsx(View, { style: [styles.shadow, hovered && styles.opaque] }), _jsx(View, { style: [styles.base, value && styles.active, disabled && styles.disabled], children: _jsx(View, { ref: buttonRef, style: styles.button, children: _jsx(Icon, { color: colors.positive[400], name: "checkmark-filled", size: 10, style: [styles.icon, value && styles.opaque] }) }) })] })) }));
86
86
  }));
@@ -1,19 +1,24 @@
1
1
  import { IconName } from "./Icon";
2
2
  import { SpacingValue } from "./Space";
3
3
  export declare const tabsViewHeight: number;
4
- type Tab = {
5
- label: string;
4
+ type Tab = ({
5
+ id: string;
6
+ } | {
6
7
  url: string;
8
+ }) & {
9
+ count?: number;
7
10
  icon?: IconName;
11
+ label: string;
8
12
  withSeparator?: boolean;
9
- count?: number;
10
13
  };
11
14
  type Props = {
15
+ activeTabId?: string;
16
+ onChange?: (id: string) => void;
12
17
  tabs: Tab[];
13
18
  otherLabel: string;
14
19
  hideIfSingleItem?: boolean;
15
20
  padding?: SpacingValue;
16
21
  sticky?: boolean;
17
22
  };
18
- export declare const TabView: ({ tabs, otherLabel, hideIfSingleItem, sticky, padding, }: Props) => import("react/jsx-runtime").JSX.Element | null;
23
+ export declare const TabView: ({ tabs, otherLabel, hideIfSingleItem, sticky, padding, activeTabId, onChange, }: Props) => import("react/jsx-runtime").JSX.Element | null;
19
24
  export {};
@@ -1,7 +1,7 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import { useLocation } from "@swan-io/chicane";
3
3
  import { Fragment, forwardRef, useCallback, useEffect, useLayoutEffect, useMemo, useReducer, useRef, useState, } from "react";
4
- import { StyleSheet, Text, View } from "react-native";
4
+ import { StyleSheet, Text, View, } from "react-native";
5
5
  import { P, match } from "ts-pattern";
6
6
  import { animations, backgroundColor, colors, negativeSpacings, radii, shadows, spacings, texts, } from "../constants/design";
7
7
  import { useHover } from "../hooks/useHover";
@@ -121,24 +121,37 @@ const styles = StyleSheet.create({
121
121
  top: -1,
122
122
  },
123
123
  });
124
- const Dropdown = ({ tabs, onHoverStart, onHoverEnd, onLinkFocus, onLinkBlur, onLinkPress, }) => {
124
+ const TabViewLink = forwardRef(({ children, style, tab, onChange, activeTabId, onBlur, onFocus, onPress }, ref) => {
125
+ return match(tab)
126
+ .with({ url: P.string }, ({ url }) => (_jsx(Link, { ref: ref, to: url, style: style, onFocus: onFocus, onBlur: onBlur, onPress: onPress, children: children })))
127
+ .with({ id: P.string }, ({ id }) => {
128
+ const isActive = id === activeTabId;
129
+ return (_jsx(PressableText, { ref: ref, style: state => style({ ...state, active: isActive }), onPress: () => {
130
+ onChange?.(id);
131
+ onPress?.();
132
+ }, onFocus: onFocus, onBlur: onBlur, children: children }));
133
+ })
134
+ .exhaustive();
135
+ });
136
+ const Dropdown = ({ tabs, onHoverStart, onHoverEnd, onLinkFocus, onLinkBlur, onLinkPress, activeTabId, onChange, }) => {
125
137
  const containerRef = useRef(null);
126
138
  useHover(containerRef, {
127
139
  onHoverStart,
128
140
  onHoverEnd,
129
141
  });
130
- return (_jsx(View, { role: "menu", style: styles.dropdown, ref: containerRef, children: tabs.map(({ url, label }) => {
131
- return (_jsx(Link, { to: url, onFocus: onLinkFocus, onBlur: onLinkBlur, onPress: onLinkPress, role: "menuitem", ariaCurrentValue: "location", style: ({ active, hovered }) => [
142
+ return (_jsx(View, { role: "menu", style: styles.dropdown, ref: containerRef, children: tabs.map(tab => {
143
+ const tabId = getTabId(tab);
144
+ return (_jsx(TabViewLink, { onChange: onChange, activeTabId: activeTabId, tab: tab, onFocus: onLinkFocus, onBlur: onLinkBlur, onPress: onLinkPress, role: "menuitem", ariaCurrentValue: "location", style: ({ active, hovered }) => [
132
145
  styles.dropdownLink,
133
146
  active && styles.dropdownLinkTextActive,
134
147
  hovered && styles.dropdownLinkTextHovered,
135
- ], children: label }, url));
148
+ ], children: tab.label }, tabId));
136
149
  }) }));
137
150
  };
138
151
  const SHOULD_AUTOFOCUS = new Set(["ForcedOpen", "OpenFromFocus"]);
139
152
  const SHOULD_OPEN = new Set(["Open", "ForcedOpen", "OpenFromFocus"]);
140
153
  const SHOULD_LOCK_FOCUS = new Set(["ForcedOpen"]);
141
- const DropdownItems = forwardRef(({ tabs, otherLabel, currentUrl }, ref) => {
154
+ const DropdownItems = forwardRef(({ tabs, otherLabel, currentUrl, activeTabId, onChange }, ref) => {
142
155
  const [openingStatus, dispatch] = useReducer((state, action) => {
143
156
  return match([action, state])
144
157
  .returnType()
@@ -251,13 +264,21 @@ const DropdownItems = forwardRef(({ tabs, otherLabel, currentUrl }, ref) => {
251
264
  onHoverEnd,
252
265
  });
253
266
  const mergedRef = useMergeRefs(containerRef, ref);
254
- const activeTab = tabs.find(({ url }) => url === currentUrl);
267
+ const activeTab = useMemo(() => tabs.find(tab => isTabActive({ activeTabId, currentLocationURL: currentUrl, tab })), [activeTabId, currentUrl, tabs]);
255
268
  return (_jsxs(View, { style: styles.dropdownHandleContainer, ref: mergedRef, children: [_jsxs(PressableText, { ref: handleRef, role: "button", "aria-expanded": shouldOpen, "aria-haspopup": "true", onFocus: onHandleFocus, onBlur: onAnyBlur, onPress: onPress, style: ({ hovered }) => [
256
269
  styles.link,
257
270
  isNotNullish(activeTab) ? styles.activeLink : hovered ? styles.hoveredLink : null,
258
- ], children: [_jsx(Text, { children: otherLabel }), _jsx(Space, { width: 8 }), _jsx(Text, { style: styles.count, children: tabs.length }), _jsx(Space, { width: 4 }), _jsx(Icon, { name: "chevron-down-filled", size: 12 })] }), _jsx(TransitionView, { ...animations.fadeAndSlideInFromBottom, style: styles.dropdownPlacement, children: shouldOpen ? (_jsx(FocusTrap, { autoFocus: shouldAutoFocus, focusLock: shouldLockFocus, returnFocus: shouldLockFocus, onClickOutside: onPressOutside, onEscapeKey: shouldLockFocus ? onEscapeKey : undefined, children: _jsx(Dropdown, { tabs: tabs, onHoverStart: onHoverStart, onHoverEnd: onHoverEnd, onLinkFocus: onLinkFocus, onLinkBlur: onAnyBlur, onLinkPress: onEscapeKey }) })) : null })] }));
271
+ ], children: [_jsx(Text, { children: otherLabel }), _jsx(Space, { width: 8 }), _jsx(Text, { style: styles.count, children: tabs.length }), _jsx(Space, { width: 4 }), _jsx(Icon, { name: "chevron-down-filled", size: 12 })] }), _jsx(TransitionView, { ...animations.fadeAndSlideInFromBottom, style: styles.dropdownPlacement, children: shouldOpen ? (_jsx(FocusTrap, { autoFocus: shouldAutoFocus, focusLock: shouldLockFocus, returnFocus: shouldLockFocus, onClickOutside: onPressOutside, onEscapeKey: shouldLockFocus ? onEscapeKey : undefined, children: _jsx(Dropdown, { onChange: onChange, activeTabId: activeTabId, tabs: tabs, onHoverStart: onHoverStart, onHoverEnd: onHoverEnd, onLinkFocus: onLinkFocus, onLinkBlur: onAnyBlur, onLinkPress: onEscapeKey }) })) : null })] }));
259
272
  });
260
- export const TabView = ({ tabs, otherLabel, hideIfSingleItem = true, sticky = false, padding, }) => {
273
+ const isTabActive = ({ tab, activeTabId, currentLocationURL }) => match(tab)
274
+ .with({ url: P.string }, ({ url }) => currentLocationURL.startsWith(url))
275
+ .with({ id: P.string }, ({ id }) => isNotNullish(activeTabId) && id === activeTabId)
276
+ .exhaustive();
277
+ const getTabId = (tab) => match(tab)
278
+ .with({ url: P.string }, ({ url }) => url)
279
+ .with({ id: P.string }, ({ id }) => id)
280
+ .exhaustive();
281
+ export const TabView = ({ tabs, otherLabel, hideIfSingleItem = true, sticky = false, padding, activeTabId, onChange, }) => {
261
282
  const containerRef = useRef(null);
262
283
  const placeholderRef = useRef(null);
263
284
  const otherPlaceholderRef = useRef(null);
@@ -273,8 +294,10 @@ export const TabView = ({ tabs, otherLabel, hideIfSingleItem = true, sticky = fa
273
294
  if (isNotNullish(linksRefs.current)) {
274
295
  const values = Object.entries(linksRefs.current);
275
296
  const container = containerRef.current;
276
- for (const [link, node] of values) {
277
- if ("/" + path.join("/") === link && isNotNullish(node) && isNotNullish(container)) {
297
+ for (const [tabId, node] of values) {
298
+ if ((tabId === activeTabId || "/" + path.join("/") === tabId) &&
299
+ isNotNullish(node) &&
300
+ isNotNullish(container)) {
278
301
  node.measureLayout(container, (left, _, width) => {
279
302
  const leftOffset = padding ?? 0;
280
303
  setUnderlinePosition({ left: left - leftOffset, width });
@@ -284,14 +307,15 @@ export const TabView = ({ tabs, otherLabel, hideIfSingleItem = true, sticky = fa
284
307
  }
285
308
  }
286
309
  setUnderlinePosition({ left: 0, width: 0 });
287
- }, [path, kept, collapsed, padding]);
310
+ }, [path, kept, collapsed, padding, activeTabId]);
288
311
  useEffect(() => {
289
312
  setHasRendered(width > 0);
290
313
  }, [width]);
291
314
  const reajustLayout = useCallback(({ width }) => {
292
315
  const items = tabs.map(tab => {
293
316
  if (placeholderLinkRef.current) {
294
- const ref = placeholderLinkRef.current[tab.url];
317
+ const tabId = getTabId(tab);
318
+ const ref = placeholderLinkRef.current[tabId];
295
319
  if (isNotNullish(ref)) {
296
320
  const element = ref;
297
321
  const width = element.getBoundingClientRect().width;
@@ -314,7 +338,7 @@ export const TabView = ({ tabs, otherLabel, hideIfSingleItem = true, sticky = fa
314
338
  kept.push({ ...item.tab, width: item.width });
315
339
  }
316
340
  else {
317
- if (currentLocationURL.startsWith(item.tab.url)) {
341
+ if (isTabActive({ activeTabId, currentLocationURL, tab: item.tab })) {
318
342
  while (kept.length !== 0 &&
319
343
  kept.reduce((acc, item) => acc + item.width, 0) + (item.width + 16) >= width) {
320
344
  const last = kept.pop();
@@ -336,7 +360,7 @@ export const TabView = ({ tabs, otherLabel, hideIfSingleItem = true, sticky = fa
336
360
  const otherLabel = otherLabelRef;
337
361
  otherLabelWidth = otherLabel.getBoundingClientRect().width;
338
362
  }
339
- const activeInKeptIndex = kept.findIndex(item => currentLocationURL.startsWith(item.url));
363
+ const activeInKeptIndex = kept.findIndex(item => isTabActive({ activeTabId, currentLocationURL, tab: item }));
340
364
  if (activeInKeptIndex !== -1) {
341
365
  const activeInKept = kept[activeInKeptIndex];
342
366
  const activeInKeptWidth = activeInKept?.width ?? 0;
@@ -364,7 +388,7 @@ export const TabView = ({ tabs, otherLabel, hideIfSingleItem = true, sticky = fa
364
388
  else {
365
389
  setKeptCollapsed([kept, collapsed]);
366
390
  }
367
- }, [tabs, currentLocationURL]);
391
+ }, [tabs, activeTabId, currentLocationURL]);
368
392
  const onLayout = useCallback(({ target, nativeEvent: { layout: { width }, }, }) => {
369
393
  reajustLayout({ container: target, width });
370
394
  }, [reajustLayout]);
@@ -379,27 +403,36 @@ export const TabView = ({ tabs, otherLabel, hideIfSingleItem = true, sticky = fa
379
403
  if (tabs.length <= 1 && hideIfSingleItem) {
380
404
  return null;
381
405
  }
382
- return (_jsxs(Box, { alignItems: "center", direction: "row", role: "tablist", ref: containerRef, style: [styles.container, sticky && styles.sticky, { paddingHorizontal: padding }], children: [_jsxs(View, { style: styles.placeholder, "aria-hidden": true, ref: placeholderRef, onLayout: onLayout, children: [tabs.map(({ label, url, icon, count }) => (_jsxs(Fragment, { children: [_jsxs(Link, { ref: ref => {
383
- if (placeholderLinkRef.current) {
384
- placeholderLinkRef.current[url] = ref;
385
- }
386
- }, to: url, style: ({ active, hovered }) => [
387
- styles.link,
388
- active ? styles.activeLink : hovered ? styles.hoveredLink : null,
389
- ], children: [isNotNullish(icon) && (_jsxs(_Fragment, { children: [_jsx(Icon, { name: icon, size: 16, color: "currentColor" }), _jsx(Space, { width: 8 })] })), _jsx(Text, { children: label }), count != null ? (_jsxs(_Fragment, { children: [_jsx(Space, { width: 8 }), _jsx(Text, { style: styles.count, children: count })] })) : null] }), _jsx(Space, { width: 32 })] }, url))), _jsxs(LakeText, { ref: otherPlaceholderRef, style: styles.link, children: [_jsx(Text, { children: otherLabel }), _jsx(Space, { width: 8 }), _jsx(Text, { style: styles.count, children: tabs.length }), _jsx(Space, { width: 4 }), _jsx(Icon, { name: "chevron-down-filled", size: 12 })] })] }), kept.map(({ label, url, icon, withSeparator, count }) => (_jsxs(Fragment, { children: [_jsxs(Link, { role: "tab", ref: ref => {
390
- if (linksRefs.current) {
391
- linksRefs.current[url] = ref;
392
- }
393
- }, to: url, style: ({ active, hovered }) => [
394
- styles.link,
395
- active ? styles.activeLink : hovered ? styles.hoveredLink : null,
396
- ], children: [withSeparator === true && _jsx(View, { style: styles.separator, role: "none" }), isNotNullish(icon) && (_jsxs(_Fragment, { children: [_jsx(Icon, { name: icon, size: 16, color: "currentColor" }), _jsx(Space, { width: 8 })] })), _jsx(Text, { children: label }), count != null ? (_jsxs(_Fragment, { children: [_jsx(Space, { width: 8 }), _jsx(Text, { style: styles.count, children: count })] })) : null] }), _jsx(Space, { width: 32 })] }, url))), collapsed.length > 0 ? (_jsx(DropdownItems, { ref: ref => {
406
+ return (_jsxs(Box, { alignItems: "center", direction: "row", role: "tablist", ref: containerRef, style: [styles.container, sticky && styles.sticky, { paddingHorizontal: padding }], children: [_jsxs(View, { style: styles.placeholder, "aria-hidden": true, ref: placeholderRef, onLayout: onLayout, children: [tabs.map(tab => {
407
+ const { label, icon, count } = tab;
408
+ const tabId = getTabId(tab);
409
+ return (_jsxs(Fragment, { children: [_jsxs(TabViewLink, { ref: ref => {
410
+ if (placeholderLinkRef.current) {
411
+ placeholderLinkRef.current[tabId] = ref;
412
+ }
413
+ }, activeTabId: activeTabId, tab: tab, onChange: onChange, style: ({ active, hovered }) => [
414
+ styles.link,
415
+ active ? styles.activeLink : hovered ? styles.hoveredLink : null,
416
+ ], children: [isNotNullish(icon) && (_jsxs(_Fragment, { children: [_jsx(Icon, { name: icon, size: 16, color: "currentColor" }), _jsx(Space, { width: 8 })] })), _jsx(Text, { children: label }), count != null ? (_jsxs(_Fragment, { children: [_jsx(Space, { width: 8 }), _jsx(Text, { style: styles.count, children: count })] })) : null] }), _jsx(Space, { width: 32 })] }, tabId));
417
+ }), _jsxs(LakeText, { ref: otherPlaceholderRef, style: styles.link, children: [_jsx(Text, { children: otherLabel }), _jsx(Space, { width: 8 }), _jsx(Text, { style: styles.count, children: tabs.length }), _jsx(Space, { width: 4 }), _jsx(Icon, { name: "chevron-down-filled", size: 12 })] })] }), kept.map(tab => {
418
+ const { label, icon, withSeparator, count } = tab;
419
+ const tabId = getTabId(tab);
420
+ return (_jsxs(Fragment, { children: [_jsxs(TabViewLink, { ref: ref => {
421
+ if (linksRefs.current) {
422
+ linksRefs.current[tabId] = ref;
423
+ }
424
+ }, onChange: onChange, activeTabId: activeTabId, tab: tab, role: "tab", style: ({ active, hovered }) => [
425
+ styles.link,
426
+ active ? styles.activeLink : hovered ? styles.hoveredLink : null,
427
+ ], children: [withSeparator === true && _jsx(View, { style: styles.separator, role: "none" }), isNotNullish(icon) && (_jsxs(_Fragment, { children: [_jsx(Icon, { name: icon, size: 16, color: "currentColor" }), _jsx(Space, { width: 8 })] })), _jsx(Text, { children: label }), count != null ? (_jsxs(_Fragment, { children: [_jsx(Space, { width: 8 }), _jsx(Text, { style: styles.count, children: count })] })) : null] }), _jsx(Space, { width: 32 })] }, tabId));
428
+ }), collapsed.length > 0 ? (_jsx(DropdownItems, { ref: ref => {
397
429
  collapsed.forEach(item => {
398
430
  if (linksRefs.current) {
399
- linksRefs.current[item.url] = ref;
431
+ const tabId = getTabId(item);
432
+ linksRefs.current[tabId] = ref;
400
433
  }
401
434
  });
402
- }, tabs: collapsed, currentUrl: currentLocationURL, otherLabel: otherLabel })) : null, hasRendered && (_jsx(View, { style: [
435
+ }, onChange: onChange, tabs: collapsed, currentUrl: currentLocationURL, otherLabel: otherLabel, activeTabId: activeTabId })) : null, hasRendered && (_jsx(View, { style: [
403
436
  styles.underline,
404
437
  styles.animatedUnderline,
405
438
  { transform: `translateX(${left}px) scaleX(${width})` },