@ecohouse/ui 0.1.7 → 0.1.9
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/README.md +17 -15
- package/dist/index.cjs +436 -120
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +66 -2
- package/dist/index.d.ts +66 -2
- package/dist/index.js +429 -121
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/components/LanguageSwitcher/LanguageSwitcher.stories.tsx +52 -0
- package/src/components/LanguageSwitcher/LanguageSwitcher.tsx +287 -0
- package/src/components/LanguageSwitcher/index.ts +2 -0
- package/src/components/index.ts +3 -0
- package/src/icons/Globe/GlobeIcon.tsx +20 -0
- package/src/icons/Globe/GlobeIcon.web.tsx +26 -0
- package/src/icons/Globe/globePath.ts +3 -0
- package/src/icons/Globe/index.ts +2 -0
- package/src/icons/index.ts +1 -0
- package/src/icons/registry.ts +3 -1
- package/src/index.ts +14 -2
- package/src/primitives/BrandLogo/BrandLogo.stories.tsx +65 -0
- package/src/primitives/BrandLogo/BrandLogo.test.ts +29 -0
- package/src/primitives/BrandLogo/BrandLogo.tsx +31 -0
- package/src/primitives/BrandLogo/BrandLogo.types.ts +45 -0
- package/src/primitives/BrandLogo/BrandLogo.web.tsx +30 -0
- package/src/primitives/BrandLogo/brandLogoPaths.ts +20 -0
- package/src/primitives/BrandLogo/index.ts +7 -0
- package/src/primitives/index.ts +8 -0
- package/src/theme/index.ts +1 -0
- package/src/theme/typography.ts +9 -0
package/package.json
CHANGED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
3
|
+
import { View } from "react-native";
|
|
4
|
+
import { LanguageSwitcher } from "./LanguageSwitcher";
|
|
5
|
+
|
|
6
|
+
const meta = {
|
|
7
|
+
title: "Components/LanguageSwitcher",
|
|
8
|
+
component: LanguageSwitcher,
|
|
9
|
+
args: {
|
|
10
|
+
defaultValue: "hy",
|
|
11
|
+
},
|
|
12
|
+
parameters: {
|
|
13
|
+
backgrounds: { default: "black" },
|
|
14
|
+
},
|
|
15
|
+
} satisfies Meta<typeof LanguageSwitcher>;
|
|
16
|
+
|
|
17
|
+
export default meta;
|
|
18
|
+
type Story = StoryObj<typeof meta>;
|
|
19
|
+
|
|
20
|
+
export const Default: Story = {};
|
|
21
|
+
|
|
22
|
+
export const Open: Story = {
|
|
23
|
+
args: { defaultOpen: true },
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export const Disabled: Story = {
|
|
27
|
+
args: { disabled: true },
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
function ControlledExample() {
|
|
31
|
+
const [value, setValue] = useState("hy");
|
|
32
|
+
return <LanguageSwitcher value={value} onValueChange={setValue} />;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export const Controlled: Story = {
|
|
36
|
+
render: () => <ControlledExample />,
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export const CustomLanguages: Story = {
|
|
40
|
+
render: () => (
|
|
41
|
+
<View style={{ alignItems: "flex-start" }}>
|
|
42
|
+
<LanguageSwitcher
|
|
43
|
+
defaultValue="fr"
|
|
44
|
+
options={[
|
|
45
|
+
{ value: "fr", label: "Fra" },
|
|
46
|
+
{ value: "de", label: "Deu" },
|
|
47
|
+
{ value: "it", label: "Ita" },
|
|
48
|
+
]}
|
|
49
|
+
/>
|
|
50
|
+
</View>
|
|
51
|
+
),
|
|
52
|
+
};
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
import {
|
|
2
|
+
forwardRef,
|
|
3
|
+
useCallback,
|
|
4
|
+
useEffect,
|
|
5
|
+
useMemo,
|
|
6
|
+
useRef,
|
|
7
|
+
useState,
|
|
8
|
+
type ComponentRef,
|
|
9
|
+
} from "react";
|
|
10
|
+
import {
|
|
11
|
+
Platform,
|
|
12
|
+
Pressable,
|
|
13
|
+
StyleSheet,
|
|
14
|
+
Text,
|
|
15
|
+
View,
|
|
16
|
+
type PressableProps,
|
|
17
|
+
type StyleProp,
|
|
18
|
+
type ViewProps,
|
|
19
|
+
type ViewStyle,
|
|
20
|
+
} from "react-native";
|
|
21
|
+
import { ChevronDownIcon, GlobeIcon } from "../../icons";
|
|
22
|
+
import { colors, fonts, languageSwitcherTypography } from "../../theme";
|
|
23
|
+
import { mergeRefs } from "../../utils/mergeRefs";
|
|
24
|
+
import { useApplyWebClassName } from "../../utils/useApplyWebClassName";
|
|
25
|
+
|
|
26
|
+
export interface LanguageOption {
|
|
27
|
+
/** Stable locale identifier, such as `hy`, `en`, or `ru`. */
|
|
28
|
+
value: string;
|
|
29
|
+
/** Short label shown in the trigger and menu. */
|
|
30
|
+
label: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface LanguageSwitcherProps extends Omit<ViewProps, "style" | "children"> {
|
|
34
|
+
options?: readonly LanguageOption[];
|
|
35
|
+
value?: string;
|
|
36
|
+
defaultValue?: string;
|
|
37
|
+
onValueChange?: (value: string) => void;
|
|
38
|
+
open?: boolean;
|
|
39
|
+
defaultOpen?: boolean;
|
|
40
|
+
onOpenChange?: (open: boolean) => void;
|
|
41
|
+
disabled?: boolean;
|
|
42
|
+
accessibilityLabel?: string;
|
|
43
|
+
style?: StyleProp<ViewStyle>;
|
|
44
|
+
className?: string;
|
|
45
|
+
hitSlop?: PressableProps["hitSlop"];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export const defaultLanguageOptions = [
|
|
49
|
+
{ value: "hy", label: "Հայ" },
|
|
50
|
+
{ value: "en", label: "Eng" },
|
|
51
|
+
{ value: "ru", label: "Рус" },
|
|
52
|
+
] as const satisfies readonly LanguageOption[];
|
|
53
|
+
|
|
54
|
+
const CONTROL_WIDTH = 90;
|
|
55
|
+
const CONTROL_HEIGHT = 35;
|
|
56
|
+
const ICON_SIZE = 16;
|
|
57
|
+
|
|
58
|
+
export const LanguageSwitcher = forwardRef<ComponentRef<typeof View>, LanguageSwitcherProps>(
|
|
59
|
+
function LanguageSwitcher(
|
|
60
|
+
{
|
|
61
|
+
options = defaultLanguageOptions,
|
|
62
|
+
value,
|
|
63
|
+
defaultValue,
|
|
64
|
+
onValueChange,
|
|
65
|
+
open: openProp,
|
|
66
|
+
defaultOpen = false,
|
|
67
|
+
onOpenChange,
|
|
68
|
+
disabled = false,
|
|
69
|
+
accessibilityLabel = "Language",
|
|
70
|
+
style,
|
|
71
|
+
className,
|
|
72
|
+
hitSlop = 8,
|
|
73
|
+
...rest
|
|
74
|
+
},
|
|
75
|
+
forwardedRef,
|
|
76
|
+
) {
|
|
77
|
+
const wrapperRef = useRef<ComponentRef<typeof View>>(null);
|
|
78
|
+
const triggerRef = useRef<ComponentRef<typeof Pressable>>(null);
|
|
79
|
+
const setWrapperRef = useMemo(() => mergeRefs(wrapperRef, forwardedRef), [forwardedRef]);
|
|
80
|
+
const firstValue = options[0]?.value ?? "";
|
|
81
|
+
const isValueControlled = value !== undefined;
|
|
82
|
+
const isOpenControlled = openProp !== undefined;
|
|
83
|
+
const [uncontrolledValue, setUncontrolledValue] = useState(defaultValue ?? firstValue);
|
|
84
|
+
const [uncontrolledOpen, setUncontrolledOpen] = useState(defaultOpen);
|
|
85
|
+
const [triggerHovered, setTriggerHovered] = useState(false);
|
|
86
|
+
const [hoveredValue, setHoveredValue] = useState<string | null>(null);
|
|
87
|
+
const selectedValue = isValueControlled ? value : uncontrolledValue;
|
|
88
|
+
const isOpen = isOpenControlled ? openProp : uncontrolledOpen;
|
|
89
|
+
const selectedOption =
|
|
90
|
+
options.find((option) => option.value === selectedValue) ?? options[0] ?? null;
|
|
91
|
+
|
|
92
|
+
useApplyWebClassName(wrapperRef, className?.trim() || undefined);
|
|
93
|
+
|
|
94
|
+
const setOpen = useCallback(
|
|
95
|
+
(next: boolean) => {
|
|
96
|
+
if (disabled || next === isOpen) return;
|
|
97
|
+
if (!isOpenControlled) setUncontrolledOpen(next);
|
|
98
|
+
onOpenChange?.(next);
|
|
99
|
+
},
|
|
100
|
+
[disabled, isOpen, isOpenControlled, onOpenChange],
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
const closeAndFocusTrigger = useCallback(() => {
|
|
104
|
+
setOpen(false);
|
|
105
|
+
const node = triggerRef.current as unknown as { focus?: () => void } | null;
|
|
106
|
+
node?.focus?.();
|
|
107
|
+
}, [setOpen]);
|
|
108
|
+
|
|
109
|
+
useEffect(() => {
|
|
110
|
+
if (Platform.OS !== "web" || !isOpen) return;
|
|
111
|
+
|
|
112
|
+
const handlePointerDown = (event: MouseEvent | TouchEvent) => {
|
|
113
|
+
const wrapper = wrapperRef.current as unknown as {
|
|
114
|
+
contains?: (node: Node) => boolean;
|
|
115
|
+
} | null;
|
|
116
|
+
const target = event.target;
|
|
117
|
+
if (wrapper?.contains && target instanceof Node && !wrapper.contains(target)) {
|
|
118
|
+
setOpen(false);
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
const handleKeyDown = (event: KeyboardEvent) => {
|
|
122
|
+
if (event.key === "Escape") {
|
|
123
|
+
event.preventDefault();
|
|
124
|
+
closeAndFocusTrigger();
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
document.addEventListener("mousedown", handlePointerDown);
|
|
129
|
+
document.addEventListener("touchstart", handlePointerDown);
|
|
130
|
+
document.addEventListener("keydown", handleKeyDown);
|
|
131
|
+
return () => {
|
|
132
|
+
document.removeEventListener("mousedown", handlePointerDown);
|
|
133
|
+
document.removeEventListener("touchstart", handlePointerDown);
|
|
134
|
+
document.removeEventListener("keydown", handleKeyDown);
|
|
135
|
+
};
|
|
136
|
+
}, [closeAndFocusTrigger, isOpen, setOpen]);
|
|
137
|
+
|
|
138
|
+
const selectLanguage = (nextValue: string) => {
|
|
139
|
+
if (nextValue !== selectedValue) {
|
|
140
|
+
if (!isValueControlled) setUncontrolledValue(nextValue);
|
|
141
|
+
onValueChange?.(nextValue);
|
|
142
|
+
}
|
|
143
|
+
closeAndFocusTrigger();
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
return (
|
|
147
|
+
<View
|
|
148
|
+
{...rest}
|
|
149
|
+
ref={setWrapperRef}
|
|
150
|
+
style={[styles.wrapper, isOpen && styles.wrapperOpen, disabled && styles.disabled, style]}
|
|
151
|
+
>
|
|
152
|
+
<Pressable
|
|
153
|
+
ref={triggerRef}
|
|
154
|
+
onPress={() => setOpen(!isOpen)}
|
|
155
|
+
disabled={disabled || options.length === 0}
|
|
156
|
+
hitSlop={hitSlop}
|
|
157
|
+
accessibilityRole="button"
|
|
158
|
+
accessibilityLabel={accessibilityLabel}
|
|
159
|
+
accessibilityState={{ disabled, expanded: isOpen }}
|
|
160
|
+
onHoverIn={() => setTriggerHovered(true)}
|
|
161
|
+
onHoverOut={() => setTriggerHovered(false)}
|
|
162
|
+
style={({ pressed }) => [
|
|
163
|
+
styles.trigger,
|
|
164
|
+
(triggerHovered || pressed || isOpen) && styles.triggerActive,
|
|
165
|
+
]}
|
|
166
|
+
>
|
|
167
|
+
<GlobeIcon size={ICON_SIZE} color={colors.white} strokeWidth={2} />
|
|
168
|
+
<Text style={styles.triggerLabel} numberOfLines={1}>
|
|
169
|
+
{selectedOption?.label ?? ""}
|
|
170
|
+
</Text>
|
|
171
|
+
<View style={[styles.chevron, isOpen && styles.chevronOpen]}>
|
|
172
|
+
<ChevronDownIcon size={ICON_SIZE} color={colors.white} strokeWidth={2} />
|
|
173
|
+
</View>
|
|
174
|
+
</Pressable>
|
|
175
|
+
|
|
176
|
+
{isOpen && options.length > 0 ? (
|
|
177
|
+
<View style={styles.menu} accessibilityRole="menu">
|
|
178
|
+
{options.map((option) => {
|
|
179
|
+
const selected = option.value === selectedValue;
|
|
180
|
+
return (
|
|
181
|
+
<Pressable
|
|
182
|
+
key={option.value}
|
|
183
|
+
onPress={() => selectLanguage(option.value)}
|
|
184
|
+
accessibilityRole="menuitem"
|
|
185
|
+
accessibilityLabel={option.label}
|
|
186
|
+
accessibilityState={{ selected }}
|
|
187
|
+
onHoverIn={() => setHoveredValue(option.value)}
|
|
188
|
+
onHoverOut={() => setHoveredValue(null)}
|
|
189
|
+
style={({ pressed }) => [
|
|
190
|
+
styles.item,
|
|
191
|
+
selected
|
|
192
|
+
? styles.itemSelected
|
|
193
|
+
: (hoveredValue === option.value || pressed) && styles.itemHovered,
|
|
194
|
+
]}
|
|
195
|
+
>
|
|
196
|
+
<Text style={[styles.itemLabel, selected && styles.itemLabelSelected]}>
|
|
197
|
+
{option.label}
|
|
198
|
+
</Text>
|
|
199
|
+
</Pressable>
|
|
200
|
+
);
|
|
201
|
+
})}
|
|
202
|
+
</View>
|
|
203
|
+
) : null}
|
|
204
|
+
</View>
|
|
205
|
+
);
|
|
206
|
+
},
|
|
207
|
+
);
|
|
208
|
+
|
|
209
|
+
const styles = StyleSheet.create({
|
|
210
|
+
wrapper: {
|
|
211
|
+
position: "relative",
|
|
212
|
+
width: CONTROL_WIDTH,
|
|
213
|
+
alignSelf: "flex-start",
|
|
214
|
+
},
|
|
215
|
+
wrapperOpen: {
|
|
216
|
+
zIndex: 20,
|
|
217
|
+
},
|
|
218
|
+
disabled: {
|
|
219
|
+
opacity: 0.6,
|
|
220
|
+
},
|
|
221
|
+
trigger: {
|
|
222
|
+
width: CONTROL_WIDTH,
|
|
223
|
+
height: CONTROL_HEIGHT,
|
|
224
|
+
flexDirection: "row",
|
|
225
|
+
alignItems: "center",
|
|
226
|
+
gap: 8,
|
|
227
|
+
padding: 8,
|
|
228
|
+
borderRadius: 12,
|
|
229
|
+
backgroundColor: "transparent",
|
|
230
|
+
},
|
|
231
|
+
triggerActive: {
|
|
232
|
+
backgroundColor: colors.grey700,
|
|
233
|
+
},
|
|
234
|
+
triggerLabel: {
|
|
235
|
+
...languageSwitcherTypography,
|
|
236
|
+
...Platform.select({ web: { fontFamily: `${fonts.sans}, sans-serif` } }),
|
|
237
|
+
minWidth: 0,
|
|
238
|
+
flex: 1,
|
|
239
|
+
color: colors.white,
|
|
240
|
+
},
|
|
241
|
+
chevron: {
|
|
242
|
+
width: ICON_SIZE,
|
|
243
|
+
height: ICON_SIZE,
|
|
244
|
+
flexShrink: 0,
|
|
245
|
+
alignItems: "center",
|
|
246
|
+
justifyContent: "center",
|
|
247
|
+
},
|
|
248
|
+
chevronOpen: {
|
|
249
|
+
transform: [{ rotate: "180deg" }],
|
|
250
|
+
},
|
|
251
|
+
menu: {
|
|
252
|
+
position: "absolute",
|
|
253
|
+
top: CONTROL_HEIGHT + 8,
|
|
254
|
+
left: 0,
|
|
255
|
+
width: CONTROL_WIDTH,
|
|
256
|
+
padding: 8,
|
|
257
|
+
gap: 8,
|
|
258
|
+
borderRadius: 12,
|
|
259
|
+
backgroundColor: colors.grey700,
|
|
260
|
+
...Platform.select({
|
|
261
|
+
web: { boxShadow: `0 8px 24px ${colors.black}73` },
|
|
262
|
+
default: { elevation: 8 },
|
|
263
|
+
}),
|
|
264
|
+
},
|
|
265
|
+
item: {
|
|
266
|
+
height: CONTROL_HEIGHT,
|
|
267
|
+
alignItems: "center",
|
|
268
|
+
justifyContent: "center",
|
|
269
|
+
paddingVertical: 8,
|
|
270
|
+
paddingHorizontal: 12,
|
|
271
|
+
borderRadius: 8,
|
|
272
|
+
},
|
|
273
|
+
itemHovered: {
|
|
274
|
+
backgroundColor: colors.whiteAlpha5,
|
|
275
|
+
},
|
|
276
|
+
itemSelected: {
|
|
277
|
+
backgroundColor: colors.primaryMuted,
|
|
278
|
+
},
|
|
279
|
+
itemLabel: {
|
|
280
|
+
...languageSwitcherTypography,
|
|
281
|
+
...Platform.select({ web: { fontFamily: `${fonts.sans}, sans-serif` } }),
|
|
282
|
+
color: colors.white,
|
|
283
|
+
},
|
|
284
|
+
itemLabelSelected: {
|
|
285
|
+
color: colors.primary,
|
|
286
|
+
},
|
|
287
|
+
});
|
package/src/components/index.ts
CHANGED
|
@@ -10,6 +10,9 @@ export type { CounterProps } from "./Counter";
|
|
|
10
10
|
export { SegmentedToggle } from "./SegmentedToggle";
|
|
11
11
|
export type { SegmentedToggleOption, SegmentedToggleProps } from "./SegmentedToggle";
|
|
12
12
|
|
|
13
|
+
export { LanguageSwitcher, defaultLanguageOptions } from "./LanguageSwitcher";
|
|
14
|
+
export type { LanguageOption, LanguageSwitcherProps } from "./LanguageSwitcher";
|
|
15
|
+
|
|
13
16
|
export { StatCard } from "./StatCard";
|
|
14
17
|
export type { StatCardProps } from "./StatCard";
|
|
15
18
|
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import Svg, { Path } from "react-native-svg";
|
|
2
|
+
import { colors } from "../../theme";
|
|
3
|
+
import type { IconProps } from "../types";
|
|
4
|
+
import { GLOBE_PATH } from "./globePath";
|
|
5
|
+
|
|
6
|
+
export function GlobeIcon({ size = 20, color = colors.primary, strokeWidth = 2 }: IconProps) {
|
|
7
|
+
return (
|
|
8
|
+
<Svg width={size} height={size} viewBox="0 0 20 20" fill="none">
|
|
9
|
+
<Path
|
|
10
|
+
d={GLOBE_PATH}
|
|
11
|
+
stroke={color}
|
|
12
|
+
strokeWidth={strokeWidth}
|
|
13
|
+
strokeLinecap="round"
|
|
14
|
+
strokeLinejoin="round"
|
|
15
|
+
/>
|
|
16
|
+
</Svg>
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export { GLOBE_PATH } from "./globePath";
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { colors } from "../../theme";
|
|
2
|
+
import type { IconProps } from "../types";
|
|
3
|
+
import { GLOBE_PATH } from "./globePath";
|
|
4
|
+
|
|
5
|
+
export function GlobeIcon({ size = 20, color = colors.primary, strokeWidth = 2 }: IconProps) {
|
|
6
|
+
return (
|
|
7
|
+
<svg
|
|
8
|
+
width={size}
|
|
9
|
+
height={size}
|
|
10
|
+
viewBox="0 0 20 20"
|
|
11
|
+
fill="none"
|
|
12
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
13
|
+
aria-hidden
|
|
14
|
+
>
|
|
15
|
+
<path
|
|
16
|
+
d={GLOBE_PATH}
|
|
17
|
+
stroke={color}
|
|
18
|
+
strokeWidth={strokeWidth}
|
|
19
|
+
strokeLinecap="round"
|
|
20
|
+
strokeLinejoin="round"
|
|
21
|
+
/>
|
|
22
|
+
</svg>
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export { GLOBE_PATH } from "./globePath";
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
/** Globe paths — 20×20 viewBox. */
|
|
2
|
+
export const GLOBE_PATH =
|
|
3
|
+
"M18.3333 10C18.3333 14.6024 14.6024 18.3333 10 18.3333M18.3333 10C18.3333 5.39763 14.6024 1.66667 10 1.66667M18.3333 10H1.66667M10 18.3333C5.39763 18.3333 1.66667 14.6024 1.66667 10M10 18.3333C12.0833 16.0511 13.2675 13.0836 13.3333 10C13.2675 6.91643 12.0833 3.94892 10 1.66667M10 18.3333C7.91667 16.0511 6.73248 13.0836 6.66667 10C6.73248 6.91643 7.91667 3.94892 10 1.66667M1.66667 10C1.66667 5.39763 5.39763 1.66667 10 1.66667";
|
package/src/icons/index.ts
CHANGED
|
@@ -19,6 +19,7 @@ export { ChevronLeftIcon } from "./ChevronLeft";
|
|
|
19
19
|
export { ClockIcon } from "./Clock";
|
|
20
20
|
export { FacebookIcon } from "./Facebook";
|
|
21
21
|
export { HeartIcon } from "./Heart";
|
|
22
|
+
export { GlobeIcon } from "./Globe";
|
|
22
23
|
export { HelpCircleIcon } from "./HelpCircle";
|
|
23
24
|
export { HomeIcon } from "./Home";
|
|
24
25
|
export { InstagramIcon } from "./Instagram";
|
package/src/icons/registry.ts
CHANGED
|
@@ -12,6 +12,7 @@ import { ChevronLeftIcon } from "./ChevronLeft";
|
|
|
12
12
|
import { ClockIcon } from "./Clock";
|
|
13
13
|
import { FacebookIcon } from "./Facebook";
|
|
14
14
|
import { HeartIcon } from "./Heart";
|
|
15
|
+
import { GlobeIcon } from "./Globe";
|
|
15
16
|
import { HelpCircleIcon } from "./HelpCircle";
|
|
16
17
|
import { HomeIcon } from "./Home";
|
|
17
18
|
import { InstagramIcon } from "./Instagram";
|
|
@@ -50,6 +51,7 @@ export const icons = {
|
|
|
50
51
|
clock: ClockIcon,
|
|
51
52
|
facebook: FacebookIcon,
|
|
52
53
|
heart: HeartIcon,
|
|
54
|
+
globe: GlobeIcon,
|
|
53
55
|
helpCircle: HelpCircleIcon,
|
|
54
56
|
home: HomeIcon,
|
|
55
57
|
instagram: InstagramIcon,
|
|
@@ -98,7 +100,7 @@ export const iconGroups = {
|
|
|
98
100
|
objects: ["bag", "building", "clock", "user", "usersAlt", "video"],
|
|
99
101
|
communication: ["mail", "phone"],
|
|
100
102
|
social: ["facebook", "instagram", "linkedin"],
|
|
101
|
-
interface: ["layoutGrid", "sidebar", "settings", "helpCircle", "logOut"],
|
|
103
|
+
interface: ["globe", "layoutGrid", "sidebar", "settings", "helpCircle", "logOut"],
|
|
102
104
|
} as const satisfies Record<string, readonly IconName[]>;
|
|
103
105
|
|
|
104
106
|
export type IconGroup = keyof typeof iconGroups;
|
package/src/index.ts
CHANGED
|
@@ -3,6 +3,8 @@ export {
|
|
|
3
3
|
Badge,
|
|
4
4
|
Counter,
|
|
5
5
|
SegmentedToggle,
|
|
6
|
+
LanguageSwitcher,
|
|
7
|
+
defaultLanguageOptions,
|
|
6
8
|
StatCard,
|
|
7
9
|
Button,
|
|
8
10
|
Checkbox,
|
|
@@ -22,6 +24,8 @@ export type {
|
|
|
22
24
|
CounterProps,
|
|
23
25
|
SegmentedToggleOption,
|
|
24
26
|
SegmentedToggleProps,
|
|
27
|
+
LanguageOption,
|
|
28
|
+
LanguageSwitcherProps,
|
|
25
29
|
StatCardProps,
|
|
26
30
|
ButtonProps,
|
|
27
31
|
ButtonSize,
|
|
@@ -44,8 +48,14 @@ export type {
|
|
|
44
48
|
CommentCardProps,
|
|
45
49
|
} from "./components";
|
|
46
50
|
|
|
47
|
-
export {
|
|
48
|
-
|
|
51
|
+
export {
|
|
52
|
+
BrandLogo,
|
|
53
|
+
BRAND_LOGO_COLORS,
|
|
54
|
+
BRAND_LOGO_DEFAULT_HEIGHT,
|
|
55
|
+
BRAND_LOGO_DEFAULT_WIDTH,
|
|
56
|
+
MenuItem,
|
|
57
|
+
} from "./primitives";
|
|
58
|
+
export type { BrandLogoProps, BrandLogoVariant, MenuItemProps } from "./primitives";
|
|
49
59
|
|
|
50
60
|
export {
|
|
51
61
|
Icon,
|
|
@@ -68,6 +78,7 @@ export {
|
|
|
68
78
|
ClockIcon,
|
|
69
79
|
FacebookIcon,
|
|
70
80
|
HeartIcon,
|
|
81
|
+
GlobeIcon,
|
|
71
82
|
HelpCircleIcon,
|
|
72
83
|
HomeIcon,
|
|
73
84
|
InstagramIcon,
|
|
@@ -106,6 +117,7 @@ export {
|
|
|
106
117
|
badgeTypography,
|
|
107
118
|
statCardTypography,
|
|
108
119
|
segmentedToggleTypography,
|
|
120
|
+
languageSwitcherTypography,
|
|
109
121
|
counterTypography,
|
|
110
122
|
ratingTypography,
|
|
111
123
|
commentCardTypography,
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
2
|
+
import { StyleSheet, View } from "react-native";
|
|
3
|
+
import { BrandLogo } from "./BrandLogo";
|
|
4
|
+
|
|
5
|
+
const meta = {
|
|
6
|
+
title: "Primitives/BrandLogo",
|
|
7
|
+
component: BrandLogo,
|
|
8
|
+
args: {
|
|
9
|
+
variant: "white",
|
|
10
|
+
width: 62,
|
|
11
|
+
height: 86,
|
|
12
|
+
},
|
|
13
|
+
argTypes: {
|
|
14
|
+
variant: { control: "radio", options: ["white", "brand"] },
|
|
15
|
+
width: { control: { type: "number", min: 1 } },
|
|
16
|
+
height: { control: { type: "number", min: 1 } },
|
|
17
|
+
},
|
|
18
|
+
parameters: {
|
|
19
|
+
backgrounds: { default: "black" },
|
|
20
|
+
},
|
|
21
|
+
} satisfies Meta<typeof BrandLogo>;
|
|
22
|
+
|
|
23
|
+
export default meta;
|
|
24
|
+
type Story = StoryObj<typeof meta>;
|
|
25
|
+
|
|
26
|
+
export const White: Story = {};
|
|
27
|
+
|
|
28
|
+
export const Brand: Story = {
|
|
29
|
+
args: { variant: "brand" },
|
|
30
|
+
parameters: { backgrounds: { default: "white" } },
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export const CustomWidth: Story = {
|
|
34
|
+
render: () => <BrandLogo variant="brand" width={124} />,
|
|
35
|
+
parameters: { backgrounds: { default: "white" } },
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export const BothVariants: Story = {
|
|
39
|
+
render: () => (
|
|
40
|
+
<View style={styles.row}>
|
|
41
|
+
<View style={[styles.surface, styles.darkSurface]}>
|
|
42
|
+
<BrandLogo variant="white" />
|
|
43
|
+
</View>
|
|
44
|
+
<View style={[styles.surface, styles.lightSurface]}>
|
|
45
|
+
<BrandLogo variant="brand" />
|
|
46
|
+
</View>
|
|
47
|
+
</View>
|
|
48
|
+
),
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const styles = StyleSheet.create({
|
|
52
|
+
row: {
|
|
53
|
+
flexDirection: "row",
|
|
54
|
+
gap: 24,
|
|
55
|
+
},
|
|
56
|
+
surface: {
|
|
57
|
+
padding: 24,
|
|
58
|
+
},
|
|
59
|
+
darkSurface: {
|
|
60
|
+
backgroundColor: "#000000",
|
|
61
|
+
},
|
|
62
|
+
lightSurface: {
|
|
63
|
+
backgroundColor: "#FFFFFF",
|
|
64
|
+
},
|
|
65
|
+
});
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
BRAND_LOGO_DEFAULT_HEIGHT,
|
|
4
|
+
BRAND_LOGO_DEFAULT_WIDTH,
|
|
5
|
+
resolveBrandLogoSize,
|
|
6
|
+
} from "./BrandLogo.types";
|
|
7
|
+
|
|
8
|
+
describe("resolveBrandLogoSize", () => {
|
|
9
|
+
it("uses the original dimensions by default", () => {
|
|
10
|
+
expect(resolveBrandLogoSize()).toEqual({
|
|
11
|
+
width: BRAND_LOGO_DEFAULT_WIDTH,
|
|
12
|
+
height: BRAND_LOGO_DEFAULT_HEIGHT,
|
|
13
|
+
});
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it("preserves the aspect ratio when only width is supplied", () => {
|
|
17
|
+
expect(resolveBrandLogoSize(124)).toEqual({ width: 124, height: 172 });
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it("preserves the aspect ratio when only height is supplied", () => {
|
|
21
|
+
const size = resolveBrandLogoSize(undefined, 172);
|
|
22
|
+
expect(size.width).toBeCloseTo(124);
|
|
23
|
+
expect(size.height).toBe(172);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("respects both explicit dimensions", () => {
|
|
27
|
+
expect(resolveBrandLogoSize(100, 100)).toEqual({ width: 100, height: 100 });
|
|
28
|
+
});
|
|
29
|
+
});
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import Svg, { Path } from "react-native-svg";
|
|
2
|
+
import { BRAND_LOGO_PATHS, BRAND_LOGO_VIEW_BOX } from "./brandLogoPaths";
|
|
3
|
+
import { BRAND_LOGO_COLORS, resolveBrandLogoSize, type BrandLogoProps } from "./BrandLogo.types";
|
|
4
|
+
|
|
5
|
+
/** Native Cortel Booking brand mark. */
|
|
6
|
+
export function BrandLogo({
|
|
7
|
+
variant = "white",
|
|
8
|
+
width,
|
|
9
|
+
height,
|
|
10
|
+
accessibilityLabel = "Cortel Booking",
|
|
11
|
+
}: BrandLogoProps) {
|
|
12
|
+
const size = resolveBrandLogoSize(width, height);
|
|
13
|
+
const fill = BRAND_LOGO_COLORS[variant];
|
|
14
|
+
|
|
15
|
+
return (
|
|
16
|
+
<Svg
|
|
17
|
+
width={size.width}
|
|
18
|
+
height={size.height}
|
|
19
|
+
viewBox={BRAND_LOGO_VIEW_BOX}
|
|
20
|
+
preserveAspectRatio="none"
|
|
21
|
+
fill="none"
|
|
22
|
+
accessible
|
|
23
|
+
accessibilityRole="image"
|
|
24
|
+
accessibilityLabel={accessibilityLabel}
|
|
25
|
+
>
|
|
26
|
+
{BRAND_LOGO_PATHS.map((path, index) => (
|
|
27
|
+
<Path key={index} d={path} fill={fill} />
|
|
28
|
+
))}
|
|
29
|
+
</Svg>
|
|
30
|
+
);
|
|
31
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
export type BrandLogoVariant = "white" | "brand";
|
|
2
|
+
|
|
3
|
+
export interface BrandLogoProps {
|
|
4
|
+
/** White for dark surfaces, or the Cortel brand color for light surfaces. */
|
|
5
|
+
variant?: BrandLogoVariant;
|
|
6
|
+
/** Rendered width. Defaults to 62; setting only width preserves the original ratio. */
|
|
7
|
+
width?: number;
|
|
8
|
+
/** Rendered height. Defaults to 86; setting only height preserves the original ratio. */
|
|
9
|
+
height?: number;
|
|
10
|
+
/** Accessible name announced for the logo. */
|
|
11
|
+
accessibilityLabel?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const BRAND_LOGO_DEFAULT_WIDTH = 62;
|
|
15
|
+
export const BRAND_LOGO_DEFAULT_HEIGHT = 86;
|
|
16
|
+
|
|
17
|
+
export const BRAND_LOGO_COLORS = {
|
|
18
|
+
white: "#FFFFFF",
|
|
19
|
+
brand: "#B76533",
|
|
20
|
+
} as const satisfies Record<BrandLogoVariant, string>;
|
|
21
|
+
|
|
22
|
+
export function resolveBrandLogoSize(
|
|
23
|
+
width?: number,
|
|
24
|
+
height?: number,
|
|
25
|
+
): { width: number; height: number } {
|
|
26
|
+
if (width != null && height != null) {
|
|
27
|
+
return { width, height };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (width != null) {
|
|
31
|
+
return {
|
|
32
|
+
width,
|
|
33
|
+
height: width * (BRAND_LOGO_DEFAULT_HEIGHT / BRAND_LOGO_DEFAULT_WIDTH),
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (height != null) {
|
|
38
|
+
return {
|
|
39
|
+
width: height * (BRAND_LOGO_DEFAULT_WIDTH / BRAND_LOGO_DEFAULT_HEIGHT),
|
|
40
|
+
height,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return { width: BRAND_LOGO_DEFAULT_WIDTH, height: BRAND_LOGO_DEFAULT_HEIGHT };
|
|
45
|
+
}
|