@ecohouse/ui 0.1.34 → 0.1.35
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/dist/index.cjs +440 -187
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +14 -4
- package/dist/index.d.ts +14 -4
- package/dist/index.js +441 -188
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/components/AccordionItem/AccordionItem.tsx +14 -11
- package/src/components/AccountHeader/AccountHeader.tsx +16 -0
- package/src/components/DatePicker/DatePicker.tsx +47 -14
- package/src/components/Input/Input.tsx +54 -21
- package/src/components/LanguageSwitcher/LanguageSwitcher.stories.tsx +11 -0
- package/src/components/LanguageSwitcher/LanguageSwitcher.tsx +103 -19
- package/src/components/LanguageSwitcher/index.ts +5 -1
- package/src/components/Select/Select.tsx +90 -5
- package/src/components/Textarea/Textarea.tsx +29 -4
- package/src/components/index.ts +5 -1
- package/src/icons/Globe/GlobeIcon.tsx +2 -2
- package/src/icons/Globe/GlobeIcon.web.tsx +2 -2
- package/src/icons/Globe/globePath.ts +2 -2
- package/src/icons/Key/keyPath.ts +1 -1
- package/src/index.ts +1 -0
- package/src/theme/typography.ts +3 -3
|
@@ -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,
|
|
@@ -64,6 +65,8 @@ type SelectBaseProps = Omit<ViewProps, "style" | "children"> & {
|
|
|
64
65
|
/** Override color for the selected value text in the trigger. */
|
|
65
66
|
valueColor?: string;
|
|
66
67
|
style?: StyleProp<ViewStyle>;
|
|
68
|
+
/** Styles applied to the dropdown menu only (not the trigger). */
|
|
69
|
+
menuStyle?: StyleProp<ViewStyle>;
|
|
67
70
|
className?: string;
|
|
68
71
|
};
|
|
69
72
|
|
|
@@ -268,6 +271,7 @@ export const Select = forwardRef<ComponentRef<typeof View>, SelectProps>(
|
|
|
268
271
|
emptyText = "No results",
|
|
269
272
|
valueColor,
|
|
270
273
|
style,
|
|
274
|
+
menuStyle,
|
|
271
275
|
className,
|
|
272
276
|
accessibilityLabel,
|
|
273
277
|
// Pulled out only to keep them off `rest` (which is spread onto the root View);
|
|
@@ -297,6 +301,10 @@ export const Select = forwardRef<ComponentRef<typeof View>, SelectProps>(
|
|
|
297
301
|
const [uncontrolledOpen, setUncontrolledOpen] = useState(defaultOpen);
|
|
298
302
|
const [searchQuery, setSearchQuery] = useState("");
|
|
299
303
|
const [hoveredValue, setHoveredValue] = useState<string | null>(null);
|
|
304
|
+
/** Web: flip menu to the trigger's right edge when it would overflow the viewport. */
|
|
305
|
+
const [menuAlignEnd, setMenuAlignEnd] = useState(false);
|
|
306
|
+
/** Web: open above the trigger when there isn't enough space below. */
|
|
307
|
+
const [menuPlaceAbove, setMenuPlaceAbove] = useState(false);
|
|
300
308
|
|
|
301
309
|
const selectedValues = useMemo(() => {
|
|
302
310
|
const current = isValueControlled
|
|
@@ -330,6 +338,25 @@ export const Select = forwardRef<ComponentRef<typeof View>, SelectProps>(
|
|
|
330
338
|
|
|
331
339
|
useApplyWebClassName(rootRef, className);
|
|
332
340
|
|
|
341
|
+
const resolvedMenuWidth = useMemo(() => {
|
|
342
|
+
const flat = StyleSheet.flatten(menuStyle);
|
|
343
|
+
const width = flat?.width;
|
|
344
|
+
if (typeof width === "number") return width;
|
|
345
|
+
const minWidth = flat?.minWidth;
|
|
346
|
+
if (typeof minWidth === "number") return minWidth;
|
|
347
|
+
return null;
|
|
348
|
+
}, [menuStyle]);
|
|
349
|
+
|
|
350
|
+
const estimatedMenuHeight = useMemo(() => {
|
|
351
|
+
const searchBlock = showSearch ? 40 + 8 : 0;
|
|
352
|
+
const optionCount = Math.max(filteredOptions.length, 1);
|
|
353
|
+
const listHeight = Math.min(
|
|
354
|
+
optionCount * 43 + Math.max(0, optionCount - 1) * 8,
|
|
355
|
+
MENU_MAX_LIST_HEIGHT,
|
|
356
|
+
);
|
|
357
|
+
return 24 + searchBlock + listHeight;
|
|
358
|
+
}, [filteredOptions.length, showSearch]);
|
|
359
|
+
|
|
333
360
|
const setOpen = useCallback(
|
|
334
361
|
(next: boolean) => {
|
|
335
362
|
if (!isOpenControlled) setUncontrolledOpen(next);
|
|
@@ -341,6 +368,8 @@ export const Select = forwardRef<ComponentRef<typeof View>, SelectProps>(
|
|
|
341
368
|
} else {
|
|
342
369
|
setSearchQuery("");
|
|
343
370
|
setHoveredValue(null);
|
|
371
|
+
setMenuAlignEnd(false);
|
|
372
|
+
setMenuPlaceAbove(false);
|
|
344
373
|
// Let the parent reset its externally fetched options.
|
|
345
374
|
onSearchChange?.("");
|
|
346
375
|
}
|
|
@@ -348,6 +377,38 @@ export const Select = forwardRef<ComponentRef<typeof View>, SelectProps>(
|
|
|
348
377
|
[isOpenControlled, onOpenChange, onSearchChange, selectedValues],
|
|
349
378
|
);
|
|
350
379
|
|
|
380
|
+
// Prefer start (left) / below; flip when the menu would overflow the viewport (web).
|
|
381
|
+
useLayoutEffect(() => {
|
|
382
|
+
if (Platform.OS !== "web" || !isOpen) return;
|
|
383
|
+
|
|
384
|
+
const updateMenuPlacement = () => {
|
|
385
|
+
const trigger = triggerRef.current as unknown as {
|
|
386
|
+
getBoundingClientRect?: () => DOMRect;
|
|
387
|
+
} | null;
|
|
388
|
+
const rect = trigger?.getBoundingClientRect?.();
|
|
389
|
+
if (!rect) return;
|
|
390
|
+
|
|
391
|
+
const viewportPadding = 8;
|
|
392
|
+
const gap = 8;
|
|
393
|
+
const menuWidth = Math.max(rect.width, resolvedMenuWidth ?? 0);
|
|
394
|
+
const overflowsRight = rect.left + menuWidth > window.innerWidth - viewportPadding;
|
|
395
|
+
setMenuAlignEnd(overflowsRight);
|
|
396
|
+
|
|
397
|
+
const spaceBelow = window.innerHeight - viewportPadding - rect.bottom;
|
|
398
|
+
const spaceAbove = rect.top - viewportPadding;
|
|
399
|
+
const needsAbove = estimatedMenuHeight + gap > spaceBelow && spaceAbove > spaceBelow;
|
|
400
|
+
setMenuPlaceAbove(needsAbove);
|
|
401
|
+
};
|
|
402
|
+
|
|
403
|
+
updateMenuPlacement();
|
|
404
|
+
window.addEventListener("resize", updateMenuPlacement);
|
|
405
|
+
window.addEventListener("scroll", updateMenuPlacement, true);
|
|
406
|
+
return () => {
|
|
407
|
+
window.removeEventListener("resize", updateMenuPlacement);
|
|
408
|
+
window.removeEventListener("scroll", updateMenuPlacement, true);
|
|
409
|
+
};
|
|
410
|
+
}, [estimatedMenuHeight, isOpen, resolvedMenuWidth]);
|
|
411
|
+
|
|
351
412
|
const handleSearchChange = (text: string) => {
|
|
352
413
|
setSearchQuery(text);
|
|
353
414
|
onSearchChange?.(text);
|
|
@@ -583,7 +644,14 @@ export const Select = forwardRef<ComponentRef<typeof View>, SelectProps>(
|
|
|
583
644
|
</Pressable>
|
|
584
645
|
|
|
585
646
|
{isOpen ? (
|
|
586
|
-
<View
|
|
647
|
+
<View
|
|
648
|
+
style={[
|
|
649
|
+
styles.menu,
|
|
650
|
+
menuAlignEnd ? styles.menuAlignEnd : styles.menuAlignStart,
|
|
651
|
+
menuPlaceAbove ? styles.menuAbove : styles.menuBelow,
|
|
652
|
+
menuStyle,
|
|
653
|
+
]}
|
|
654
|
+
>
|
|
587
655
|
{showSearch ? (
|
|
588
656
|
<View style={styles.searchField}>
|
|
589
657
|
<View style={styles.iconSlot}>
|
|
@@ -777,10 +845,7 @@ const styles = StyleSheet.create({
|
|
|
777
845
|
},
|
|
778
846
|
menu: {
|
|
779
847
|
position: "absolute",
|
|
780
|
-
|
|
781
|
-
left: 0,
|
|
782
|
-
right: 0,
|
|
783
|
-
marginTop: 8,
|
|
848
|
+
minWidth: "100%",
|
|
784
849
|
zIndex: 10,
|
|
785
850
|
borderRadius: MENU_RADIUS,
|
|
786
851
|
padding: 12,
|
|
@@ -795,6 +860,26 @@ const styles = StyleSheet.create({
|
|
|
795
860
|
},
|
|
796
861
|
}),
|
|
797
862
|
},
|
|
863
|
+
menuAlignStart: {
|
|
864
|
+
left: 0,
|
|
865
|
+
right: "auto",
|
|
866
|
+
},
|
|
867
|
+
menuAlignEnd: {
|
|
868
|
+
left: "auto",
|
|
869
|
+
right: 0,
|
|
870
|
+
},
|
|
871
|
+
menuBelow: {
|
|
872
|
+
top: "100%",
|
|
873
|
+
bottom: "auto",
|
|
874
|
+
marginTop: 8,
|
|
875
|
+
marginBottom: 0,
|
|
876
|
+
},
|
|
877
|
+
menuAbove: {
|
|
878
|
+
top: "auto",
|
|
879
|
+
bottom: "100%",
|
|
880
|
+
marginTop: 0,
|
|
881
|
+
marginBottom: 8,
|
|
882
|
+
},
|
|
798
883
|
optionList: {
|
|
799
884
|
maxHeight: MENU_MAX_LIST_HEIGHT,
|
|
800
885
|
},
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { forwardRef, useMemo, useRef, useState, type ComponentRef } from "react";
|
|
1
|
+
import { forwardRef, useId, useMemo, useRef, useState, type ComponentRef } from "react";
|
|
2
2
|
import {
|
|
3
3
|
Platform,
|
|
4
|
+
Pressable,
|
|
4
5
|
StyleSheet,
|
|
5
6
|
Text,
|
|
6
7
|
TextInput,
|
|
@@ -132,6 +133,8 @@ export const Textarea = forwardRef<ComponentRef<typeof TextInput>, TextareaProps
|
|
|
132
133
|
const setInputRef = useMemo(() => mergeRefs(inputRef, forwardedRef), [forwardedRef]);
|
|
133
134
|
const [focused, setFocused] = useState(false);
|
|
134
135
|
const [uncontrolledValue, setUncontrolledValue] = useState(defaultValue ?? "");
|
|
136
|
+
const reactId = useId();
|
|
137
|
+
const inputDomId = `eh-textarea-${reactId.replace(/:/g, "")}`;
|
|
135
138
|
|
|
136
139
|
const isControlled = typeof value === "string";
|
|
137
140
|
const currentValue = isControlled ? value : uncontrolledValue;
|
|
@@ -141,28 +144,40 @@ export const Textarea = forwardRef<ComponentRef<typeof TextInput>, TextareaProps
|
|
|
141
144
|
const chrome = chromeFor(visualState);
|
|
142
145
|
const resolvedAccessibilityLabel = accessibilityLabel ?? (showLabel ? undefined : label);
|
|
143
146
|
|
|
147
|
+
const focusInput = () => {
|
|
148
|
+
if (disabled) return;
|
|
149
|
+
inputRef.current?.focus();
|
|
150
|
+
};
|
|
151
|
+
|
|
144
152
|
useApplyWebClassName(rootRef, className);
|
|
145
153
|
useApplyWebClassName(inputRef, inputClassName);
|
|
146
154
|
|
|
147
155
|
return (
|
|
148
156
|
<View ref={rootRef} style={[styles.root, disabled && styles.disabled, style]}>
|
|
149
157
|
{showLabel && label ? (
|
|
150
|
-
<
|
|
158
|
+
<Pressable disabled={disabled} onPress={focusInput} style={styles.labelPressable}>
|
|
159
|
+
<Text style={[styles.label, { color: chrome.labelColor }]}>{label}</Text>
|
|
160
|
+
</Pressable>
|
|
151
161
|
) : null}
|
|
152
162
|
|
|
153
|
-
<
|
|
163
|
+
<Pressable
|
|
164
|
+
accessibilityRole="none"
|
|
165
|
+
disabled={disabled}
|
|
166
|
+
onPress={focusInput}
|
|
154
167
|
style={[
|
|
155
168
|
styles.field,
|
|
156
169
|
{
|
|
157
170
|
borderColor: chrome.borderColor,
|
|
158
171
|
backgroundColor: chrome.backgroundColor,
|
|
159
172
|
},
|
|
173
|
+
Platform.OS === "web" ? styles.fieldWeb : null,
|
|
160
174
|
fieldStyle,
|
|
161
175
|
]}
|
|
162
176
|
>
|
|
163
177
|
<TextInput
|
|
164
178
|
ref={setInputRef}
|
|
165
179
|
{...textInputProps}
|
|
180
|
+
nativeID={inputDomId}
|
|
166
181
|
multiline
|
|
167
182
|
textAlignVertical="top"
|
|
168
183
|
value={isControlled ? value : undefined}
|
|
@@ -193,7 +208,7 @@ export const Textarea = forwardRef<ComponentRef<typeof TextInput>, TextareaProps
|
|
|
193
208
|
accessibilityLabel={resolvedAccessibilityLabel}
|
|
194
209
|
accessibilityState={{ disabled }}
|
|
195
210
|
/>
|
|
196
|
-
</
|
|
211
|
+
</Pressable>
|
|
197
212
|
|
|
198
213
|
{showHint && hint ? (
|
|
199
214
|
<Text style={[styles.hint, { color: chrome.hintColor }]}>{hint}</Text>
|
|
@@ -211,6 +226,9 @@ const styles = StyleSheet.create({
|
|
|
211
226
|
disabled: {
|
|
212
227
|
opacity: 0.6,
|
|
213
228
|
},
|
|
229
|
+
labelPressable: {
|
|
230
|
+
alignSelf: "flex-start",
|
|
231
|
+
},
|
|
214
232
|
label: {
|
|
215
233
|
...textareaTypography.label,
|
|
216
234
|
},
|
|
@@ -223,14 +241,21 @@ const styles = StyleSheet.create({
|
|
|
223
241
|
paddingLeft: FIELD_PADDING_HORIZONTAL,
|
|
224
242
|
paddingRight: FIELD_PADDING_HORIZONTAL,
|
|
225
243
|
},
|
|
244
|
+
fieldWeb: {
|
|
245
|
+
cursor: "text" as unknown as ViewStyle["cursor"],
|
|
246
|
+
},
|
|
226
247
|
input: {
|
|
227
248
|
flex: 1,
|
|
249
|
+
alignSelf: "stretch",
|
|
228
250
|
paddingVertical: 0,
|
|
229
251
|
margin: 0,
|
|
230
252
|
},
|
|
231
253
|
inputWeb: {
|
|
232
254
|
outlineStyle: "none",
|
|
233
255
|
resize: "none",
|
|
256
|
+
height: "100%",
|
|
257
|
+
minWidth: 0,
|
|
258
|
+
cursor: "text" as unknown as TextStyle["cursor"],
|
|
234
259
|
} as TextStyle,
|
|
235
260
|
inputAndroid: {
|
|
236
261
|
includeFontPadding: false,
|
package/src/components/index.ts
CHANGED
|
@@ -41,7 +41,11 @@ export { Sidebar } from "./Sidebar";
|
|
|
41
41
|
export type { SidebarProps, SidebarItem, SidebarUser } from "./Sidebar";
|
|
42
42
|
|
|
43
43
|
export { LanguageSwitcher, defaultLanguageOptions } from "./LanguageSwitcher";
|
|
44
|
-
export type {
|
|
44
|
+
export type {
|
|
45
|
+
LanguageOption,
|
|
46
|
+
LanguageSwitcherProps,
|
|
47
|
+
LanguageSwitcherVariant,
|
|
48
|
+
} from "./LanguageSwitcher";
|
|
45
49
|
|
|
46
50
|
export { StatCard } from "./StatCard";
|
|
47
51
|
export type { StatCardProps } from "./StatCard";
|
|
@@ -3,9 +3,9 @@ import { colors } from "../../theme";
|
|
|
3
3
|
import type { IconProps } from "../types";
|
|
4
4
|
import { GLOBE_PATH } from "./globePath";
|
|
5
5
|
|
|
6
|
-
export function GlobeIcon({ size =
|
|
6
|
+
export function GlobeIcon({ size = 16, color = colors.primary, strokeWidth = 1.5 }: IconProps) {
|
|
7
7
|
return (
|
|
8
|
-
<Svg width={size} height={size} viewBox="0 0
|
|
8
|
+
<Svg width={size} height={size} viewBox="0 0 16 16" fill="none">
|
|
9
9
|
<Path
|
|
10
10
|
d={GLOBE_PATH}
|
|
11
11
|
stroke={color}
|
|
@@ -2,12 +2,12 @@ import { colors } from "../../theme";
|
|
|
2
2
|
import type { IconProps } from "../types";
|
|
3
3
|
import { GLOBE_PATH } from "./globePath";
|
|
4
4
|
|
|
5
|
-
export function GlobeIcon({ size =
|
|
5
|
+
export function GlobeIcon({ size = 16, color = colors.primary, strokeWidth = 1.5 }: IconProps) {
|
|
6
6
|
return (
|
|
7
7
|
<svg
|
|
8
8
|
width={size}
|
|
9
9
|
height={size}
|
|
10
|
-
viewBox="0 0
|
|
10
|
+
viewBox="0 0 16 16"
|
|
11
11
|
fill="none"
|
|
12
12
|
xmlns="http://www.w3.org/2000/svg"
|
|
13
13
|
aria-hidden
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
/** Globe paths —
|
|
1
|
+
/** Globe paths — 16×16 viewBox. */
|
|
2
2
|
export const GLOBE_PATH =
|
|
3
|
-
"
|
|
3
|
+
"M1.3335 8.00004H14.6668M1.3335 8.00004C1.3335 11.6819 4.31826 14.6667 8.00016 14.6667M1.3335 8.00004C1.3335 4.31814 4.31826 1.33337 8.00016 1.33337M14.6668 8.00004C14.6668 11.6819 11.6821 14.6667 8.00016 14.6667M14.6668 8.00004C14.6668 4.31814 11.6821 1.33337 8.00016 1.33337M8.00016 1.33337C9.66768 3.15894 10.6153 5.52806 10.6668 8.00004C10.6153 10.472 9.66768 12.8411 8.00016 14.6667M8.00016 1.33337C6.33264 3.15894 5.38499 5.52806 5.3335 8.00004C5.38499 10.472 6.33264 12.8411 8.00016 14.6667";
|
package/src/icons/Key/keyPath.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export const KEY_PATH =
|
|
2
|
-
"
|
|
2
|
+
"M14.1668 8.33333V6.66667C14.1668 4.36548 12.3013 2.5 10.0002 2.5C7.69898 2.5 5.8335 4.36548 5.8335 6.66667V8.33333M10.0002 12.0833V13.75M7.3335 17.5H12.6668C14.067 17.5 14.767 17.5 15.3018 17.2275C15.7722 16.9878 16.1547 16.6054 16.3943 16.135C16.6668 15.6002 16.6668 14.9001 16.6668 13.5V12.3333C16.6668 10.9332 16.6668 10.2331 16.3943 9.69836C16.1547 9.22795 15.7722 8.8455 15.3018 8.60582C14.767 8.33333 14.067 8.33333 12.6668 8.33333H7.3335C5.93336 8.33333 5.2333 8.33333 4.69852 8.60582C4.22811 8.8455 3.84566 9.22795 3.60598 9.69836C3.3335 10.2331 3.3335 10.9332 3.3335 12.3333V13.5C3.3335 14.9001 3.3335 15.6002 3.60598 16.135C3.84566 16.6054 4.22811 16.9878 4.69852 17.2275C5.2333 17.5 5.93336 17.5 7.3335 17.5Z";
|
package/src/index.ts
CHANGED
package/src/theme/typography.ts
CHANGED
|
@@ -47,13 +47,13 @@ export const segmentedToggleTypography = {
|
|
|
47
47
|
letterSpacing: 0,
|
|
48
48
|
} as const;
|
|
49
49
|
|
|
50
|
-
/** Language switcher trigger and option labels — SemiBold 14
|
|
50
|
+
/** Language switcher trigger and option labels — SemiBold 14, tracking 3%. */
|
|
51
51
|
export const languageSwitcherTypography = {
|
|
52
52
|
fontFamily: fonts.sans,
|
|
53
53
|
fontSize: 14,
|
|
54
54
|
fontWeight: "600" as const,
|
|
55
|
-
lineHeight:
|
|
56
|
-
letterSpacing: 0,
|
|
55
|
+
lineHeight: 14,
|
|
56
|
+
letterSpacing: 0.42,
|
|
57
57
|
} as const;
|
|
58
58
|
|
|
59
59
|
/** Counter value — Bold 14, line-height 100%, letter-spacing 0. */
|