@ecohouse/ui 0.1.32 → 0.1.33

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 (36) hide show
  1. package/dist/index.cjs +796 -509
  2. package/dist/index.cjs.map +1 -1
  3. package/dist/index.d.cts +74 -8
  4. package/dist/index.d.ts +74 -8
  5. package/dist/index.js +729 -448
  6. package/dist/index.js.map +1 -1
  7. package/package.json +1 -1
  8. package/src/components/DatePicker/DatePicker.tsx +115 -45
  9. package/src/components/Select/Select.tsx +5 -2
  10. package/src/components/Sidebar/Sidebar.tsx +25 -11
  11. package/src/components/StepPager/StepPager.tsx +150 -0
  12. package/src/components/StepPager/index.ts +2 -0
  13. package/src/components/index.ts +3 -0
  14. package/src/icons/Ban/BanIcon.tsx +15 -0
  15. package/src/icons/Ban/banPath.ts +2 -0
  16. package/src/icons/Ban/index.ts +1 -0
  17. package/src/icons/Fullscreen/FullscreenIcon.tsx +21 -0
  18. package/src/icons/Fullscreen/fullscreenPath.ts +2 -0
  19. package/src/icons/Fullscreen/index.ts +1 -0
  20. package/src/icons/Home/homePath.ts +1 -1
  21. package/src/icons/Sort/SortIcon.tsx +3 -3
  22. package/src/icons/Sort/SortIcon.web.tsx +2 -2
  23. package/src/icons/Sort/sortPath.ts +3 -2
  24. package/src/icons/VideoOff/VideoOffIcon.tsx +21 -0
  25. package/src/icons/VideoOff/VideoOffIcon.web.tsx +27 -0
  26. package/src/icons/VideoOff/index.ts +1 -0
  27. package/src/icons/VideoOff/videoOffPath.ts +2 -0
  28. package/src/icons/ZoomOut/ZoomOutIcon.tsx +21 -0
  29. package/src/icons/ZoomOut/index.ts +1 -0
  30. package/src/icons/ZoomOut/zoomOutPath.ts +2 -0
  31. package/src/icons/index.ts +4 -0
  32. package/src/icons/registry.ts +12 -0
  33. package/src/index.ts +8 -0
  34. package/src/theme/colors.test.ts +1 -1
  35. package/src/theme/typography.ts +4 -4
  36. package/src/utils/popoverAlign.ts +85 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ecohouse/ui",
3
- "version": "0.1.32",
3
+ "version": "0.1.33",
4
4
  "type": "module",
5
5
  "private": false,
6
6
  "description": "Cross-platform presentation-only UI components for EcoHouse (React Native + React Native Web)",
@@ -2,6 +2,7 @@ import {
2
2
  forwardRef,
3
3
  useCallback,
4
4
  useEffect,
5
+ useLayoutEffect,
5
6
  useMemo,
6
7
  useRef,
7
8
  useState,
@@ -93,6 +94,11 @@ type DatePickerBaseProps = Omit<ViewProps, "style" | "children"> & {
93
94
  weekStartsOn?: 0 | 1;
94
95
  /** Styles the calendar popup independently from the trigger width. */
95
96
  calendarStyle?: StyleProp<ViewStyle>;
97
+ /**
98
+ * When true, only the calendar panel is rendered (no field/trigger).
99
+ * Use with controlled `open` for icon-button / popover hosts.
100
+ */
101
+ hideTrigger?: boolean;
96
102
  style?: StyleProp<ViewStyle>;
97
103
  className?: string;
98
104
  };
@@ -174,6 +180,7 @@ export const DatePicker = forwardRef<ComponentRef<typeof View>, DatePickerProps>
174
180
  formatValue: formatValueProp,
175
181
  weekStartsOn = 1,
176
182
  calendarStyle,
183
+ hideTrigger = false,
177
184
  style,
178
185
  className,
179
186
  accessibilityLabel,
@@ -207,6 +214,8 @@ export const DatePicker = forwardRef<ComponentRef<typeof View>, DatePickerProps>
207
214
  );
208
215
  const [uncontrolledOpen, setUncontrolledOpen] = useState(defaultOpen);
209
216
  const [hoveredDayKey, setHoveredDayKey] = useState<number | null>(null);
217
+ /** Web: flip calendar to the trigger's right edge when it would overflow the viewport. */
218
+ const [menuAlignEnd, setMenuAlignEnd] = useState(false);
210
219
 
211
220
  const rangeValue: DateRange = isRange
212
221
  ? isValueControlled
@@ -221,6 +230,13 @@ export const DatePicker = forwardRef<ComponentRef<typeof View>, DatePickerProps>
221
230
 
222
231
  const isOpen = isOpenControlled ? openProp : uncontrolledOpen;
223
232
 
233
+ const defaultMenuWidth = usesRangeTrigger ? RANGE_TRIGGER_WIDTH : DEFAULT_WIDTH;
234
+ const resolvedMenuWidth = useMemo(() => {
235
+ const flat = StyleSheet.flatten(calendarStyle);
236
+ const width = flat?.width;
237
+ return typeof width === "number" ? width : defaultMenuWidth;
238
+ }, [calendarStyle, defaultMenuWidth]);
239
+
224
240
  const [visibleMonth, setVisibleMonth] = useState(() => {
225
241
  const anchor =
226
242
  defaultVisibleMonth ?? (isRange ? rangeValue.start : singleValue) ?? new Date();
@@ -238,11 +254,33 @@ export const DatePicker = forwardRef<ComponentRef<typeof View>, DatePickerProps>
238
254
  setVisibleMonth(new Date(anchor.getFullYear(), anchor.getMonth(), 1));
239
255
  } else {
240
256
  setHoveredDayKey(null);
257
+ setMenuAlignEnd(false);
241
258
  }
242
259
  },
243
260
  [isOpenControlled, isRange, onOpenChange, rangeValue.start, singleValue],
244
261
  );
245
262
 
263
+ // Prefer left alignment; flip to right when the menu would overflow the viewport (web).
264
+ useLayoutEffect(() => {
265
+ if (Platform.OS !== "web" || !isOpen || hideTrigger) return;
266
+
267
+ const updateMenuAlign = () => {
268
+ const trigger = triggerRef.current as unknown as {
269
+ getBoundingClientRect?: () => DOMRect;
270
+ } | null;
271
+ const rect = trigger?.getBoundingClientRect?.();
272
+ if (!rect) return;
273
+
274
+ const viewportPadding = 8;
275
+ const overflowsRight = rect.left + resolvedMenuWidth > window.innerWidth - viewportPadding;
276
+ setMenuAlignEnd(overflowsRight);
277
+ };
278
+
279
+ updateMenuAlign();
280
+ window.addEventListener("resize", updateMenuAlign);
281
+ return () => window.removeEventListener("resize", updateMenuAlign);
282
+ }, [hideTrigger, isOpen, resolvedMenuWidth]);
283
+
246
284
  // Close on outside click (web only; native has no document-level events).
247
285
  useEffect(() => {
248
286
  if (Platform.OS !== "web" || !isOpen) return;
@@ -371,6 +409,7 @@ export const DatePicker = forwardRef<ComponentRef<typeof View>, DatePickerProps>
371
409
  style={[
372
410
  styles.root,
373
411
  usesRangeTrigger && styles.rangeRoot,
412
+ hideTrigger && styles.rootHideTrigger,
374
413
  isOpen && styles.rootOpen,
375
414
  disabled && styles.disabled,
376
415
  style,
@@ -378,52 +417,65 @@ export const DatePicker = forwardRef<ComponentRef<typeof View>, DatePickerProps>
378
417
  accessibilityState={{ disabled, expanded: isOpen }}
379
418
  >
380
419
  <View style={[styles.triggerWrap, isOpen && styles.triggerWrapOpen]}>
381
- <Pressable
382
- ref={triggerRef}
383
- onPress={handleTriggerPress}
384
- disabled={disabled}
385
- accessibilityRole="button"
386
- accessibilityLabel={resolvedAccessibilityLabel}
387
- accessibilityState={{ disabled, expanded: isOpen }}
388
- style={[
389
- styles.trigger,
390
- usesRangeTrigger && styles.rangeTrigger,
391
- usesRangeTrigger &&
392
- (usesVerticalRangeTrigger
393
- ? styles.rangeTriggerVertical
394
- : styles.rangeTriggerHorizontal),
395
- {
396
- borderColor: triggerBorderColor,
397
- backgroundColor: usesRangeTrigger ? "transparent" : colors.grey700,
398
- },
399
- ]}
400
- >
401
- {usesRangeTrigger ? (
402
- <>
403
- <RangeTriggerField label={startLabel} time={startTime} value={startValue} />
404
- <View
405
- style={
406
- usesVerticalRangeTrigger
407
- ? styles.rangeDividerVertical
408
- : styles.rangeDividerHorizontal
409
- }
410
- />
411
- <RangeTriggerField label={endLabel} time={endTime} value={endValue} />
412
- </>
413
- ) : (
414
- <>
415
- <Text style={[styles.triggerText, { color: triggerTextColor }]} numberOfLines={1}>
416
- {triggerLabel}
417
- </Text>
418
- <View style={styles.iconSlot}>
419
- <CalendarOutlineIcon size={ICON_SIZE} color={colors.grey100} />
420
- </View>
421
- </>
422
- )}
423
- </Pressable>
420
+ {hideTrigger ? null : (
421
+ <Pressable
422
+ ref={triggerRef}
423
+ onPress={handleTriggerPress}
424
+ disabled={disabled}
425
+ accessibilityRole="button"
426
+ accessibilityLabel={resolvedAccessibilityLabel}
427
+ accessibilityState={{ disabled, expanded: isOpen }}
428
+ style={[
429
+ styles.trigger,
430
+ usesRangeTrigger && styles.rangeTrigger,
431
+ usesRangeTrigger &&
432
+ (usesVerticalRangeTrigger
433
+ ? styles.rangeTriggerVertical
434
+ : styles.rangeTriggerHorizontal),
435
+ {
436
+ borderColor: triggerBorderColor,
437
+ backgroundColor: usesRangeTrigger ? "transparent" : colors.grey700,
438
+ },
439
+ ]}
440
+ >
441
+ {usesRangeTrigger ? (
442
+ <>
443
+ <RangeTriggerField label={startLabel} time={startTime} value={startValue} />
444
+ <View
445
+ style={
446
+ usesVerticalRangeTrigger
447
+ ? styles.rangeDividerVertical
448
+ : styles.rangeDividerHorizontal
449
+ }
450
+ />
451
+ <RangeTriggerField label={endLabel} time={endTime} value={endValue} />
452
+ </>
453
+ ) : (
454
+ <>
455
+ <Text style={[styles.triggerText, { color: triggerTextColor }]} numberOfLines={1}>
456
+ {triggerLabel}
457
+ </Text>
458
+ <View style={styles.iconSlot}>
459
+ <CalendarOutlineIcon size={ICON_SIZE} color={colors.grey100} />
460
+ </View>
461
+ </>
462
+ )}
463
+ </Pressable>
464
+ )}
424
465
 
425
466
  {isOpen ? (
426
- <View style={[styles.menu, usesRangeTrigger && styles.rangeMenu, calendarStyle]}>
467
+ <View
468
+ style={[
469
+ styles.menu,
470
+ usesRangeTrigger && styles.rangeMenu,
471
+ hideTrigger
472
+ ? styles.menuInline
473
+ : menuAlignEnd
474
+ ? styles.menuAlignEnd
475
+ : styles.menuAlignStart,
476
+ calendarStyle,
477
+ ]}
478
+ >
427
479
  <View style={styles.calendarHeader}>
428
480
  <Pressable
429
481
  onPress={handlePrevMonth}
@@ -590,6 +642,10 @@ const styles = StyleSheet.create({
590
642
  gap: 8,
591
643
  alignSelf: "flex-start",
592
644
  },
645
+ rootHideTrigger: {
646
+ width: "auto",
647
+ gap: 0,
648
+ },
593
649
  rootOpen: {
594
650
  zIndex: 30,
595
651
  },
@@ -694,7 +750,6 @@ const styles = StyleSheet.create({
694
750
  menu: {
695
751
  position: "absolute",
696
752
  top: "100%",
697
- left: 0,
698
753
  width: DEFAULT_WIDTH,
699
754
  marginTop: 8,
700
755
  zIndex: 10,
@@ -711,6 +766,21 @@ const styles = StyleSheet.create({
711
766
  },
712
767
  }),
713
768
  },
769
+ menuAlignStart: {
770
+ left: 0,
771
+ right: "auto",
772
+ },
773
+ menuAlignEnd: {
774
+ left: "auto",
775
+ right: 0,
776
+ },
777
+ menuInline: {
778
+ position: "relative",
779
+ top: "auto",
780
+ left: "auto",
781
+ right: "auto",
782
+ marginTop: 0,
783
+ },
714
784
  rangeMenu: {
715
785
  width: RANGE_TRIGGER_WIDTH,
716
786
  },
@@ -61,6 +61,8 @@ type SelectBaseProps = Omit<ViewProps, "style" | "children"> & {
61
61
  loading?: boolean;
62
62
  /** Text shown when no options match. */
63
63
  emptyText?: string;
64
+ /** Override color for the selected value text in the trigger. */
65
+ valueColor?: string;
64
66
  style?: StyleProp<ViewStyle>;
65
67
  className?: string;
66
68
  };
@@ -264,6 +266,7 @@ export const Select = forwardRef<ComponentRef<typeof View>, SelectProps>(
264
266
  onSearchChange,
265
267
  loading = false,
266
268
  emptyText = "No results",
269
+ valueColor,
267
270
  style,
268
271
  className,
269
272
  accessibilityLabel,
@@ -508,7 +511,7 @@ export const Select = forwardRef<ComponentRef<typeof View>, SelectProps>(
508
511
 
509
512
  const leftIconNode = renderStatefulIcon(leftIcon, UserIcon, {
510
513
  size: ICON_SIZE,
511
- color: error ? colors.red : isOpen ? colors.primary : colors.grey100,
514
+ color: error ? colors.red : colors.primary,
512
515
  });
513
516
  const hasLeftIcon = showLeftIcon || leftIcon != null;
514
517
 
@@ -520,7 +523,7 @@ export const Select = forwardRef<ComponentRef<typeof View>, SelectProps>(
520
523
  const triggerTextColor = hasValue
521
524
  ? error
522
525
  ? colors.red
523
- : colors.white
526
+ : (valueColor ?? colors.white)
524
527
  : error
525
528
  ? colors.red
526
529
  : colors.grey100;
@@ -59,6 +59,8 @@ export interface SidebarProps extends Omit<ViewProps, "style" | "children"> {
59
59
  collapsed?: boolean;
60
60
  defaultCollapsed?: boolean;
61
61
  onCollapsedChange?: (collapsed: boolean) => void;
62
+ /** When false, hides the expand/collapse control. Defaults to `true`. */
63
+ showCollapseToggle?: boolean;
62
64
  style?: StyleProp<ViewStyle>;
63
65
  className?: string;
64
66
  }
@@ -86,6 +88,7 @@ export const Sidebar = forwardRef<ComponentRef<typeof View>, SidebarProps>(funct
86
88
  collapsed: collapsedProp,
87
89
  defaultCollapsed = false,
88
90
  onCollapsedChange,
91
+ showCollapseToggle = true,
89
92
  style,
90
93
  className,
91
94
  ...rest
@@ -136,7 +139,13 @@ export const Sidebar = forwardRef<ComponentRef<typeof View>, SidebarProps>(funct
136
139
  style={[styles.root, { width: widthAnim }, style]}
137
140
  accessibilityRole="menu"
138
141
  >
139
- <View style={[styles.header, collapsed ? styles.headerCollapsed : styles.headerExpanded]}>
142
+ <View
143
+ style={[
144
+ styles.header,
145
+ collapsed ? styles.headerCollapsed : styles.headerExpanded,
146
+ !showCollapseToggle ? styles.headerNoToggle : null,
147
+ ]}
148
+ >
140
149
  <Animated.View
141
150
  pointerEvents={collapsed ? "none" : "auto"}
142
151
  style={[
@@ -157,16 +166,18 @@ export const Sidebar = forwardRef<ComponentRef<typeof View>, SidebarProps>(funct
157
166
  />
158
167
  </Animated.View>
159
168
 
160
- <Pressable
161
- accessibilityRole="button"
162
- accessibilityLabel={collapsed ? expandAccessibilityLabel : collapseAccessibilityLabel}
163
- accessibilityState={{ expanded: !collapsed }}
164
- hitSlop={8}
165
- onPress={() => setCollapsed(!collapsed)}
166
- style={styles.toggleButton}
167
- >
168
- <SidebarIcon color={colors.grey100} size={TOGGLE_SIZE} strokeWidth={2} />
169
- </Pressable>
169
+ {showCollapseToggle ? (
170
+ <Pressable
171
+ accessibilityRole="button"
172
+ accessibilityLabel={collapsed ? expandAccessibilityLabel : collapseAccessibilityLabel}
173
+ accessibilityState={{ expanded: !collapsed }}
174
+ hitSlop={8}
175
+ onPress={() => setCollapsed(!collapsed)}
176
+ style={styles.toggleButton}
177
+ >
178
+ <SidebarIcon color={colors.grey100} size={TOGGLE_SIZE} strokeWidth={2} />
179
+ </Pressable>
180
+ ) : null}
170
181
  </View>
171
182
 
172
183
  <View
@@ -242,6 +253,9 @@ const styles = StyleSheet.create({
242
253
  justifyContent: "space-between",
243
254
  paddingHorizontal: 24,
244
255
  },
256
+ headerNoToggle: {
257
+ justifyContent: "flex-start",
258
+ },
245
259
  headerCollapsed: {
246
260
  justifyContent: "center",
247
261
  paddingHorizontal: 0,
@@ -0,0 +1,150 @@
1
+ import { forwardRef, useMemo, useRef, type ComponentRef } from "react";
2
+ import {
3
+ Platform,
4
+ Pressable,
5
+ StyleSheet,
6
+ Text,
7
+ View,
8
+ type StyleProp,
9
+ type ViewProps,
10
+ type ViewStyle,
11
+ } from "react-native";
12
+ import { ChevronLeftIcon } from "../../icons";
13
+ import { colors, fonts } from "../../theme";
14
+ import { mergeRefs } from "../../utils/mergeRefs";
15
+ import { useApplyWebClassName } from "../../utils/useApplyWebClassName";
16
+
17
+ export interface StepPagerProps extends Omit<ViewProps, "style" | "children"> {
18
+ /** Zero-based active step index. */
19
+ index: number;
20
+ /** Total number of steps. */
21
+ count: number;
22
+ onPrevious?: () => void;
23
+ onNext?: () => void;
24
+ previousAccessibilityLabel?: string;
25
+ nextAccessibilityLabel?: string;
26
+ style?: StyleProp<ViewStyle>;
27
+ className?: string;
28
+ }
29
+
30
+ const WIDTH = 121;
31
+ const HEIGHT = 48;
32
+ const ICON_SIZE = 24;
33
+
34
+ /**
35
+ * Compact prev / current-of-total / next control (e.g. camera fullscreen pager).
36
+ * Fixed 121×48 pill, 12 gap/padding, 77 radius.
37
+ */
38
+ export const StepPager = forwardRef<ComponentRef<typeof View>, StepPagerProps>(function StepPager(
39
+ {
40
+ index,
41
+ count,
42
+ onPrevious,
43
+ onNext,
44
+ previousAccessibilityLabel = "Previous",
45
+ nextAccessibilityLabel = "Next",
46
+ style,
47
+ className,
48
+ accessibilityLabel,
49
+ ...rest
50
+ },
51
+ forwardedRef,
52
+ ) {
53
+ const containerRef = useRef<ComponentRef<typeof View>>(null);
54
+ const setContainerRef = useMemo(() => mergeRefs(containerRef, forwardedRef), [forwardedRef]);
55
+ const safeCount = Math.max(0, count);
56
+ const safeIndex = safeCount === 0 ? 0 : Math.min(Math.max(index, 0), safeCount - 1);
57
+ const label = safeCount === 0 ? "0/0" : `${safeIndex + 1}/${safeCount}`;
58
+ const atStart = safeIndex <= 0;
59
+ const atEnd = safeIndex >= safeCount - 1;
60
+
61
+ useApplyWebClassName(containerRef, className);
62
+
63
+ return (
64
+ <View
65
+ {...rest}
66
+ ref={setContainerRef}
67
+ accessibilityRole="adjustable"
68
+ accessibilityLabel={accessibilityLabel ?? label}
69
+ accessibilityValue={{
70
+ min: safeCount === 0 ? 0 : 1,
71
+ max: safeCount,
72
+ now: safeCount === 0 ? 0 : safeIndex + 1,
73
+ text: label,
74
+ }}
75
+ style={[styles.root, webBlurStyle, style]}
76
+ >
77
+ <Pressable
78
+ onPress={onPrevious}
79
+ disabled={atStart || !onPrevious}
80
+ hitSlop={4}
81
+ accessibilityRole="button"
82
+ accessibilityLabel={previousAccessibilityLabel}
83
+ accessibilityState={{ disabled: atStart || !onPrevious }}
84
+ style={[styles.navButton, (atStart || !onPrevious) && styles.navDisabled]}
85
+ >
86
+ <ChevronLeftIcon size={ICON_SIZE} color={colors.white} strokeWidth={2} />
87
+ </Pressable>
88
+
89
+ <Text style={styles.label}>{label}</Text>
90
+
91
+ <Pressable
92
+ onPress={onNext}
93
+ disabled={atEnd || !onNext}
94
+ hitSlop={4}
95
+ accessibilityRole="button"
96
+ accessibilityLabel={nextAccessibilityLabel}
97
+ accessibilityState={{ disabled: atEnd || !onNext }}
98
+ style={[styles.navButton, (atEnd || !onNext) && styles.navDisabled]}
99
+ >
100
+ <View style={styles.chevronRight}>
101
+ <ChevronLeftIcon size={ICON_SIZE} color={colors.white} strokeWidth={2} />
102
+ </View>
103
+ </Pressable>
104
+ </View>
105
+ );
106
+ });
107
+
108
+ const webBlurStyle: ViewStyle =
109
+ Platform.OS === "web"
110
+ ? ({
111
+ backdropFilter: "blur(50px)",
112
+ WebkitBackdropFilter: "blur(50px)",
113
+ } as ViewStyle)
114
+ : {};
115
+
116
+ const styles = StyleSheet.create({
117
+ root: {
118
+ width: WIDTH,
119
+ height: HEIGHT,
120
+ flexDirection: "row",
121
+ alignItems: "center",
122
+ justifyContent: "center",
123
+ gap: 12,
124
+ padding: 12,
125
+ borderRadius: 77,
126
+ backgroundColor: colors.whiteAlpha6,
127
+ overflow: "hidden",
128
+ },
129
+ navButton: {
130
+ width: ICON_SIZE,
131
+ height: ICON_SIZE,
132
+ alignItems: "center",
133
+ justifyContent: "center",
134
+ },
135
+ navDisabled: {
136
+ opacity: 0.35,
137
+ },
138
+ chevronRight: {
139
+ transform: [{ scaleX: -1 }],
140
+ },
141
+ label: {
142
+ fontFamily: fonts.sans,
143
+ fontSize: 16,
144
+ fontWeight: "700",
145
+ lineHeight: 16,
146
+ letterSpacing: 0,
147
+ color: colors.white,
148
+ textAlign: "center",
149
+ },
150
+ });
@@ -0,0 +1,2 @@
1
+ export { StepPager } from "./StepPager";
2
+ export type { StepPagerProps } from "./StepPager";
@@ -16,6 +16,9 @@ export type { StatusBadgeProps, StatusBadgeTone } from "./StatusBadge";
16
16
  export { PaginationDots } from "./PaginationDots";
17
17
  export type { PaginationDotsProps } from "./PaginationDots";
18
18
 
19
+ export { StepPager } from "./StepPager";
20
+ export type { StepPagerProps } from "./StepPager";
21
+
19
22
  export { InfoTile } from "./InfoTile";
20
23
  export type { InfoTileProps } from "./InfoTile";
21
24
 
@@ -0,0 +1,15 @@
1
+ import Svg, { Path } from "react-native-svg";
2
+ import { colors } from "../../theme";
3
+ import type { IconProps } from "../types";
4
+ import { BAN_PATH } from "./banPath";
5
+
6
+ export { BAN_PATH } from "./banPath";
7
+
8
+ /** Filled prohibition / deactivated mark (28×28 viewBox). */
9
+ export function BanIcon({ size = 28, color = colors.primary }: IconProps) {
10
+ return (
11
+ <Svg width={size} height={size} viewBox="0 0 28 28" fill="none">
12
+ <Path d={BAN_PATH} fill={color} />
13
+ </Svg>
14
+ );
15
+ }
@@ -0,0 +1,2 @@
1
+ export const BAN_PATH =
2
+ "M14.0007 2.33301C11.6932 2.33301 9.43758 3.01725 7.519 4.2992C5.60043 5.58115 4.10508 7.40323 3.22206 9.53503C2.33904 11.6668 2.108 14.0126 2.55816 16.2757C3.00832 18.5388 4.11946 20.6176 5.75108 22.2493C7.38269 23.8809 9.46149 24.992 11.7246 25.4422C13.9877 25.8923 16.3335 25.6613 18.4653 24.7783C20.5971 23.8952 22.4192 22.3999 23.7011 20.4813C24.9831 18.5628 25.6673 16.3071 25.6673 13.9997C25.6673 12.4676 25.3656 10.9505 24.7793 9.53503C24.1929 8.11957 23.3336 6.83345 22.2502 5.7501C21.1669 4.66675 19.8808 3.80738 18.4653 3.22108C17.0498 2.63478 15.5327 2.33301 14.0007 2.33301ZM14.0007 23.333C11.5253 23.333 9.15133 22.3497 7.40099 20.5993C5.65065 18.849 4.66732 16.475 4.66732 13.9997C4.66475 11.9269 5.35917 9.91348 6.63899 8.28301L19.7173 21.3613C18.0869 22.6412 16.0734 23.3356 14.0007 23.333ZM21.3623 19.7163L8.28399 6.63801C9.91446 5.35819 11.9279 4.66376 14.0007 4.66634C16.476 4.66634 18.85 5.64967 20.6003 7.40001C22.3507 9.15035 23.334 11.5243 23.334 13.9997C23.3366 16.0724 22.6421 18.0859 21.3623 19.7163Z";
@@ -0,0 +1 @@
1
+ export { BanIcon, BAN_PATH } from "./BanIcon";
@@ -0,0 +1,21 @@
1
+ import Svg, { Path } from "react-native-svg";
2
+ import { colors } from "../../theme";
3
+ import type { IconProps } from "../types";
4
+ import { FULLSCREEN_PATH } from "./fullscreenPath";
5
+
6
+ export { FULLSCREEN_PATH } from "./fullscreenPath";
7
+
8
+ /** Four-corner fullscreen / maximize control (24×24 viewBox). */
9
+ export function FullscreenIcon({ size = 24, color = colors.white, strokeWidth = 2 }: IconProps) {
10
+ return (
11
+ <Svg width={size} height={size} viewBox="0 0 24 24" fill="none">
12
+ <Path
13
+ d={FULLSCREEN_PATH}
14
+ stroke={color}
15
+ strokeWidth={strokeWidth}
16
+ strokeLinecap="round"
17
+ strokeLinejoin="round"
18
+ />
19
+ </Svg>
20
+ );
21
+ }
@@ -0,0 +1,2 @@
1
+ export const FULLSCREEN_PATH =
2
+ "M16 8L21 3M21 8V3H16M8 8L3 3M8 3L3 3L3 8M8 16L3 21M3 16L3 21H8M16 16L21 21M16 21H21V16";
@@ -0,0 +1 @@
1
+ export { FullscreenIcon, FULLSCREEN_PATH } from "./FullscreenIcon";
@@ -1,2 +1,2 @@
1
1
  export const HOME_PATH =
2
- "M1.66675 16.6667H3.33341M3.33341 16.6667H8.33341M3.33341 16.6667V9.54352C3.33341 9.09823 3.33341 8.87548 3.38756 8.66828C3.43554 8.48466 3.51491 8.31089 3.6215 8.15387C3.74178 7.97668 3.90895 7.82974 4.24406 7.53652L8.24538 4.03537C8.86662 3.49178 9.17726 3.21997 9.5271 3.11652C9.83559 3.02529 10.1644 3.02529 10.4729 3.11652C10.823 3.22005 11.1341 3.49212 11.7563 4.03652L15.7563 7.53652C16.0914 7.82974 16.2586 7.97668 16.3789 8.15387C16.4855 8.31089 16.5642 8.48466 16.6122 8.66828C16.6664 8.87548 16.6667 9.09824 16.6667 9.54352V16.6667M8.33341 16.6667H11.6667M8.33341 16.6667V13.3334C8.33341 12.4129 9.07961 11.6667 10.0001 11.6667C10.9206 11.6667 11.6667 12.4129 11.6667 13.3334V16.6667M11.6667 16.6667H16.6667M16.6667 16.6667H18.3334";
2
+ "M6.77168 11.6663C7.14172 13.104 8.4468 14.1663 10 14.1663C11.5532 14.1663 12.8583 13.104 13.2283 11.6663M9.18141 2.30297L3.52949 6.6989C3.15168 6.99276 2.96278 7.13968 2.82669 7.32368C2.70614 7.48667 2.61633 7.67029 2.56169 7.86551C2.5 8.0859 2.5 8.32521 2.5 8.80384V14.833C2.5 15.7664 2.5 16.2331 2.68166 16.5896C2.84144 16.9032 3.09641 17.1582 3.41002 17.318C3.76654 17.4996 4.23325 17.4996 5.16667 17.4996H14.8333C15.7668 17.4996 16.2335 17.4996 16.59 17.318C16.9036 17.1582 17.1586 16.9032 17.3183 16.5896C17.5 16.2331 17.5 15.7664 17.5 14.833V8.80384C17.5 8.32521 17.5 8.0859 17.4383 7.86551C17.3837 7.67029 17.2939 7.48667 17.1733 7.32368C17.0372 7.13968 16.8483 6.99276 16.4705 6.69891L10.8186 2.30297C10.5258 2.07526 10.3794 1.9614 10.2178 1.91763C10.0752 1.87902 9.92484 1.87902 9.78221 1.91763C9.62057 1.9614 9.47418 2.07526 9.18141 2.30297Z";
@@ -5,10 +5,10 @@ import type { IconProps } from "../types";
5
5
 
6
6
  export { SORT_PATH } from "./sortPath";
7
7
 
8
- /** Native react-native-svg (peer dependency). */
9
- export function SortIcon({ size = 12, color = colors.grey150, strokeWidth = 1.5 }: IconProps) {
8
+ /** Bidirectional sort (20×20 viewBox). */
9
+ export function SortIcon({ size = 20, color = colors.grey300, strokeWidth = 2 }: IconProps) {
10
10
  return (
11
- <Svg width={size} height={size} viewBox="0 0 12 12" fill="none">
11
+ <Svg width={size} height={size} viewBox="0 0 20 20" fill="none">
12
12
  <Path
13
13
  d={SORT_PATH}
14
14
  stroke={color}
@@ -3,12 +3,12 @@ import { colors } from "../../theme";
3
3
  import type { IconProps } from "../types";
4
4
 
5
5
  /** Web / Storybook — plain SVG (no react-native-svg). */
6
- export function SortIcon({ size = 12, color = colors.grey150, strokeWidth = 1.5 }: IconProps) {
6
+ export function SortIcon({ size = 20, color = colors.grey300, strokeWidth = 2 }: IconProps) {
7
7
  return (
8
8
  <svg
9
9
  width={size}
10
10
  height={size}
11
- viewBox="0 0 12 12"
11
+ viewBox="0 0 20 20"
12
12
  fill="none"
13
13
  xmlns="http://www.w3.org/2000/svg"
14
14
  aria-hidden
@@ -1,2 +1,3 @@
1
- /** Bidirectional sort icon path — 12×12 viewBox. */
2
- export const SORT_PATH = "M3.5 2V10M5.5 8L3.5 10L1.5 8M8.5 10V2M10.5 4L8.5 2L6.5 4";
1
+ /** Bidirectional sort icon path — 20×20 viewBox. */
2
+ export const SORT_PATH =
3
+ "M5.83333 3.33301V16.6663M9.16667 13.333L5.83333 16.6663L2.5 13.333M14.1667 16.6663V3.33301M17.5 6.66634L14.1667 3.33301L10.8333 6.66634";
@@ -0,0 +1,21 @@
1
+ import Svg, { Path } from "react-native-svg";
2
+ import { colors } from "../../theme";
3
+ import type { IconProps } from "../types";
4
+ import { VIDEO_OFF_PATH } from "./videoOffPath";
5
+
6
+ export { VIDEO_OFF_PATH } from "./videoOffPath";
7
+
8
+ /** Video / camera off (24×24 viewBox). */
9
+ export function VideoOffIcon({ size = 24, color = colors.primary, strokeWidth = 2 }: IconProps) {
10
+ return (
11
+ <Svg width={size} height={size} viewBox="0 0 24 24" fill="none">
12
+ <Path
13
+ d={VIDEO_OFF_PATH}
14
+ stroke={color}
15
+ strokeWidth={strokeWidth}
16
+ strokeLinecap="round"
17
+ strokeLinejoin="round"
18
+ />
19
+ </Svg>
20
+ );
21
+ }