@scripso-homepad/ui 0.4.21 → 0.4.23

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": "@scripso-homepad/ui",
3
- "version": "0.4.21",
3
+ "version": "0.4.23",
4
4
  "type": "module",
5
5
  "description": "Cross-platform UI components for Homepad (React Web + React Native)",
6
6
  "license": "MIT",
@@ -19,12 +19,12 @@ import {
19
19
  } from "../data/countries";
20
20
  import { ChevronDownIcon } from "../icons/ChevronDownIcon";
21
21
  import { colors, fontSize, fontWeight, radii, spacing, fonts } from "../theme/tokens";
22
- import { countryCodeToFlagEmoji } from "../utils/countryFlag";
23
22
  import {
24
23
  COUNTRY_DROPDOWN_SCROLL_CLASS,
25
24
  ensureCountryDropdownScrollStyles,
26
25
  } from "../utils/countryDropdownScrollStyles";
27
26
  import { useApplyWebClassName } from "../utils/useApplyWebClassName";
27
+ import { CountryFlag } from "./CountryFlag";
28
28
 
29
29
  const DROPDOWN_GAP = 10;
30
30
  const DROPDOWN_HEIGHT = 186;
@@ -126,7 +126,7 @@ export function CountryCodeSelector({
126
126
  !isLast && styles.optionSpacing,
127
127
  ]}
128
128
  >
129
- <Text style={styles.flag}>{countryCodeToFlagEmoji(item.code)}</Text>
129
+ <CountryFlag code={item.code} />
130
130
  <Text style={styles.optionDialCode}>{item.dialCode}</Text>
131
131
  </Pressable>
132
132
  );
@@ -159,7 +159,7 @@ export function CountryCodeSelector({
159
159
  Platform.OS === "web" ? styles.triggerWeb : null,
160
160
  ]}
161
161
  >
162
- <Text style={styles.flag}>{countryCodeToFlagEmoji(selected.code)}</Text>
162
+ <CountryFlag code={selected.code} />
163
163
  <Text style={[styles.dialCode, { color: textColor }]}>{selected.dialCode}</Text>
164
164
  <ChevronDownIcon size={CHEVRON_SIZE} color={colors.stormGray100} />
165
165
  </Pressable>
@@ -219,10 +219,6 @@ const styles = StyleSheet.create({
219
219
  triggerWeb: {
220
220
  width: "max-content" as ViewStyle["width"],
221
221
  },
222
- flag: {
223
- fontSize: 18,
224
- lineHeight: 24,
225
- },
226
222
  dialCode: {
227
223
  fontSize: fontSize.md,
228
224
  fontWeight: fontWeight.medium,
@@ -0,0 +1,37 @@
1
+ import { Image, StyleSheet, type StyleProp, type ImageStyle } from "react-native";
2
+
3
+ import { getCountryFlagImageUri } from "../utils/countryFlag";
4
+
5
+ const FLAG_HEIGHT = 18;
6
+ const FLAG_WIDTH = 24;
7
+
8
+ export type CountryFlagProps = {
9
+ code: string;
10
+ style?: StyleProp<ImageStyle>;
11
+ };
12
+
13
+ /** Renders a country flag image (works on Windows; emoji flags do not). */
14
+ export function CountryFlag({ code, style }: CountryFlagProps) {
15
+ const uri = getCountryFlagImageUri(code);
16
+
17
+ if (uri === null) {
18
+ return null;
19
+ }
20
+
21
+ return (
22
+ <Image
23
+ source={{ uri }}
24
+ style={[styles.flag, style]}
25
+ accessibilityIgnoresInvertColors
26
+ accessibilityLabel={`${code.trim().toUpperCase()} flag`}
27
+ />
28
+ );
29
+ }
30
+
31
+ const styles = StyleSheet.create({
32
+ flag: {
33
+ width: FLAG_WIDTH,
34
+ height: FLAG_HEIGHT,
35
+ borderRadius: 2,
36
+ },
37
+ });
@@ -17,6 +17,7 @@ const meta = {
17
17
  placeholder: "placeholder",
18
18
  leftIcon: <KeyIcon />,
19
19
  options: demoOptions,
20
+ allowDeselect: true,
20
21
  },
21
22
  } satisfies Meta<typeof Select>;
22
23
 
@@ -214,6 +214,11 @@ interface SelectBaseProps {
214
214
  hintClassName?: string;
215
215
  /** Shorter option rows for compact filter-style dropdowns. */
216
216
  compact?: boolean;
217
+ /**
218
+ * When true (default), clicking the already-selected option clears the selection
219
+ * in single-select mode. Set to false for required fields that must keep a value.
220
+ */
221
+ allowDeselect?: boolean;
217
222
  }
218
223
 
219
224
  export interface SingleSelectProps extends SelectBaseProps {
@@ -266,6 +271,7 @@ export function Select({
266
271
  errorClassName,
267
272
  hintClassName,
268
273
  compact = false,
274
+ allowDeselect = true,
269
275
  }: SelectProps) {
270
276
  const wrapperRef = useRef<ComponentRef<typeof View>>(null);
271
277
  const triggerRef = useRef<ComponentRef<typeof Pressable>>(null);
@@ -389,9 +395,10 @@ export function Select({
389
395
  return;
390
396
  }
391
397
 
392
- // Keep the current value when the selected option is clicked again.
393
- // Clearing to "" breaks required selects (e.g. language) and shows the placeholder.
394
398
  if (selectedValues.includes(nextValue)) {
399
+ if (allowDeselect) {
400
+ (onValueChange as SingleSelectProps["onValueChange"] | undefined)?.("");
401
+ }
395
402
  closeMenu();
396
403
  return;
397
404
  }
package/src/index.ts CHANGED
@@ -22,6 +22,14 @@ export type { PhoneInputProps } from "./components/PhoneInput";
22
22
  export { CountryCodeSelector } from "./components/CountryCodeSelector";
23
23
  export type { CountryCodeSelectorProps } from "./components/CountryCodeSelector";
24
24
 
25
+ export { CountryFlag } from "./components/CountryFlag";
26
+ export type { CountryFlagProps } from "./components/CountryFlag";
27
+
28
+ export {
29
+ countryCodeToFlagEmoji,
30
+ getCountryFlagImageUri,
31
+ } from "./utils/countryFlag";
32
+
25
33
  export { Calendar } from "./components/Calendar";
26
34
  export type {
27
35
  CalendarLabels,
@@ -1,4 +1,4 @@
1
- /** ISO 3166-1 alpha-2 country code → flag emoji (cross-platform). */
1
+ /** ISO 3166-1 alpha-2 country code → flag emoji. Prefer CountryFlag images on Windows. */
2
2
  export function countryCodeToFlagEmoji(code: string): string {
3
3
  const upper = code.trim().toUpperCase();
4
4
  if (upper.length !== 2) return "";
@@ -7,3 +7,14 @@ export function countryCodeToFlagEmoji(code: string): string {
7
7
  ...upper.split("").map((char) => 0x1f1e6 - 65 + char.charCodeAt(0)),
8
8
  );
9
9
  }
10
+
11
+ /**
12
+ * PNG flag URL (flagcdn). Windows does not render regional-indicator flag emoji,
13
+ * so UI should use images instead of countryCodeToFlagEmoji.
14
+ */
15
+ export function getCountryFlagImageUri(code: string): string | null {
16
+ const lower = code.trim().toLowerCase();
17
+ if (!/^[a-z]{2}$/.test(lower)) return null;
18
+
19
+ return `https://flagcdn.com/w40/${lower}.png`;
20
+ }
@@ -1,6 +1,6 @@
1
1
  import type { Meta, StoryObj } from '@storybook/react';
2
2
  import { fn } from '@storybook/test';
3
- import { useState } from 'react';
3
+ import { useState, type ReactNode } from 'react';
4
4
 
5
5
  import { Button } from '../../components/Button';
6
6
  import { WebLayoutCanvas } from '../layout/story-helpers';
@@ -132,8 +132,8 @@ function DrawerStoryShell({
132
132
  }: {
133
133
  open: boolean;
134
134
  onClose: () => void;
135
- footer?: React.ReactNode;
136
- title?: string;
135
+ footer?: ReactNode;
136
+ title?: ReactNode;
137
137
  subtitle?: string;
138
138
  }) {
139
139
  return (
@@ -38,7 +38,7 @@ function unlockBodyScroll() {
38
38
 
39
39
  export type DrawerProps = {
40
40
  open: boolean;
41
- title: string;
41
+ title: ReactNode;
42
42
  subtitle?: string;
43
43
  subtitleClassName?: string;
44
44
  headerBottom?: ReactNode;