@lotics/ui 1.26.0 → 1.27.0

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": "@lotics/ui",
3
- "version": "1.26.0",
3
+ "version": "1.27.0",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  "./tokens": "./src/tokens.ts",
@@ -48,6 +48,11 @@
48
48
  "./text": "./src/text.tsx",
49
49
  "./activity_indicator": "./src/activity_indicator.tsx",
50
50
  "./icon": "./src/icon.tsx",
51
+ "./dynamic_icon": {
52
+ "react-native": "./src/dynamic_icon.tsx",
53
+ "default": "./src/dynamic_icon.web.tsx"
54
+ },
55
+ "./app_icon": "./src/app_icon.tsx",
51
56
  "./tooltip": "./src/tooltip.tsx",
52
57
  "./button": "./src/button.tsx",
53
58
  "./checkbox": "./src/checkbox.tsx",
@@ -0,0 +1,59 @@
1
+ import { View, StyleSheet } from "react-native";
2
+ import { DynamicIcon } from "./dynamic_icon";
3
+ import { colors } from "./colors";
4
+
5
+ interface AppIconProps {
6
+ /**
7
+ * The app's icon — a Lucide icon name (the full library, via `DynamicIcon`).
8
+ * Empty/absent renders the neutral default.
9
+ */
10
+ icon?: string | null;
11
+ /** Palette token name (e.g. "blue"); unknown/empty → neutral zinc. */
12
+ themeColor?: string | null;
13
+ size?: "sm" | "md";
14
+ }
15
+
16
+ const DEFAULT_ICON = "layout-grid";
17
+
18
+ /**
19
+ * The app's launcher tile: a brand-tinted square holding the app's Lucide icon
20
+ * (full library) or its uploaded image. Shared by every surface that shows an app
21
+ * (launcher, list, picker) — frontend and extension alike.
22
+ */
23
+ export function AppIcon({ icon, themeColor, size = "sm" }: AppIconProps) {
24
+ const gradientStart = shade(themeColor, "400") ?? colors.zinc["400"];
25
+ const gradientEnd = shade(themeColor, "500") ?? colors.zinc["500"];
26
+ const iconColor = themeColor ? colors.white : colors.zinc["900"];
27
+ const isMd = size === "md";
28
+ const iconSize = isMd ? 20 : 16;
29
+ const name = icon && icon.trim() !== "" ? icon : DEFAULT_ICON;
30
+
31
+ return (
32
+ <View
33
+ style={[
34
+ styles.container,
35
+ isMd ? styles.md : styles.sm,
36
+ // Solid color is the cross-platform base; web layers the gradient on top.
37
+ { backgroundColor: gradientEnd, backgroundImage: `linear-gradient(135deg, ${gradientStart} 0%, ${gradientEnd} 100%)` },
38
+ ]}
39
+ >
40
+ <DynamicIcon name={name} size={iconSize} color={iconColor} />
41
+ </View>
42
+ );
43
+ }
44
+
45
+ /** Safe palette-token → shade lookup (mirrors the option-color resolver). */
46
+ function shade(token: string | null | undefined, level: "400" | "500"): string | undefined {
47
+ if (!token) return undefined;
48
+ const palette = colors[token as keyof typeof colors];
49
+ if (palette && typeof palette === "object" && level in palette) {
50
+ return (palette as Record<string, string>)[level];
51
+ }
52
+ return undefined;
53
+ }
54
+
55
+ const styles = StyleSheet.create({
56
+ container: { borderRadius: 6, alignItems: "center", justifyContent: "center", overflow: "hidden" },
57
+ sm: { width: 32, height: 32 },
58
+ md: { width: 40, height: 40, borderRadius: 8, boxShadow: "0px 2px 4px rgba(0, 0, 0, 0.1)" },
59
+ });
@@ -0,0 +1,20 @@
1
+ import { Icon, type IconName } from "./icon";
2
+
3
+ interface DynamicIconProps {
4
+ /** Any Lucide icon name (kebab-case). */
5
+ name: string;
6
+ size?: number;
7
+ color?: string;
8
+ testID?: string;
9
+ }
10
+
11
+ /**
12
+ * Native variant of `DynamicIcon`. There's no lazy lucide loader on native, so
13
+ * names resolve against the curated `Icon` set (which falls back to a neutral
14
+ * glyph for names outside it). The web variant (`dynamic_icon.web.tsx`) renders
15
+ * the full Lucide library. The runtime name is a boundary value — `Icon` handles
16
+ * an unknown name gracefully.
17
+ */
18
+ export function DynamicIcon({ name, size, color, testID }: DynamicIconProps) {
19
+ return <Icon name={name as IconName} size={size} color={color} testID={testID} />;
20
+ }
@@ -0,0 +1,40 @@
1
+ import { View } from "react-native";
2
+ import { DynamicIcon as LucideDynamicIcon } from "lucide-react/dynamic";
3
+ import { colors } from "./colors";
4
+
5
+ type LucideIconName = React.ComponentProps<typeof LucideDynamicIcon>["name"];
6
+
7
+ interface DynamicIconProps {
8
+ /** Any Lucide icon name (kebab-case). Unknown names render a blank placeholder. */
9
+ name: string;
10
+ size?: number;
11
+ color?: string;
12
+ testID?: string;
13
+ }
14
+
15
+ /**
16
+ * Renders any icon from the FULL Lucide library by runtime name. Web variant:
17
+ * lucide-react's lazy `DynamicIcon` code-splits per icon, so only icons actually
18
+ * shown are fetched — the full set costs ~nothing in the initial bundle. The
19
+ * native variant (`dynamic_icon.tsx`) resolves against the curated `Icon` set.
20
+ *
21
+ * Icons are decorative — the enclosing control carries the accessible name
22
+ * (matches `Icon`).
23
+ */
24
+ export function DynamicIcon({ name, size = 24, color = colors.zinc["900"], testID }: DynamicIconProps) {
25
+ return (
26
+ <View
27
+ testID={testID}
28
+ accessibilityElementsHidden
29
+ importantForAccessibility="no-hide-descendants"
30
+ aria-hidden
31
+ >
32
+ <LucideDynamicIcon
33
+ name={name as LucideIconName}
34
+ size={size}
35
+ color={color}
36
+ fallback={() => <View style={{ width: size, height: size }} />}
37
+ />
38
+ </View>
39
+ );
40
+ }