diff --git a/js/app/components/countdown.tsx b/js/app/components/countdown.tsx index 7248b8ac..b0ba5b12 100644 --- a/js/app/components/countdown.tsx +++ b/js/app/components/countdown.tsx @@ -1,4 +1,5 @@ import { Text, zero } from "@streamplace/components"; +import { fontFamilies } from "@streamplace/components/src/lib/theme/tokens"; import * as chrono from "chrono-node"; import { useEffect, useState } from "react"; import { View, useWindowDimensions } from "react-native"; @@ -96,7 +97,7 @@ export function Countdown({ from, to, small }: CountdownProps) { ]; const timeTextStyle = [ - { fontFamily: "monospace" }, + { fontFamily: fontFamilies.monoRegular }, small ? { fontSize: 18 } : { fontSize: 128 }, small ? { lineHeight: 24 } : { lineHeight: 40 }, ]; diff --git a/js/app/components/emoji-picker/emoji-picker.web.tsx b/js/app/components/emoji-picker/emoji-picker.web.tsx index 85a83672..2c6bf014 100644 --- a/js/app/components/emoji-picker/emoji-picker.web.tsx +++ b/js/app/components/emoji-picker/emoji-picker.web.tsx @@ -1,4 +1,4 @@ -import { Text, useAQState, useTheme } from "@streamplace/components"; +import { hexToRgba, Text, useAQState, useTheme } from "@streamplace/components"; import { EmojiPicker as FrimousseEmojiPicker, SkinTone, @@ -492,7 +492,7 @@ export function EmojiPicker({ }} onMouseEnter={(e) => { (e.currentTarget as HTMLButtonElement).style.background = - theme.colors.secondary + "90"; + hexToRgba(theme.colors.secondary, 0.56); }} onMouseLeave={(e) => { (e.currentTarget as HTMLButtonElement).style.background = diff --git a/js/app/components/login/login-form.tsx b/js/app/components/login/login-form.tsx index 585ad6b7..4ebcb630 100644 --- a/js/app/components/login/login-form.tsx +++ b/js/app/components/login/login-form.tsx @@ -275,7 +275,7 @@ export default function LoginForm({ autoCapitalize="none" autoCorrect={false} keyboardType="url" - placeholderTextColor="#666" + placeholderTextColor={theme.colors.textMuted} containerStyle={{ paddingLeft: 46, }} diff --git a/js/app/components/settings/advanced-category-settings.tsx b/js/app/components/settings/advanced-category-settings.tsx index 86e0f8d0..06256421 100644 --- a/js/app/components/settings/advanced-category-settings.tsx +++ b/js/app/components/settings/advanced-category-settings.tsx @@ -6,6 +6,7 @@ import { MenuGroup, Text, useFetchBranding, + useTheme, View, zero, } from "@streamplace/components"; @@ -21,6 +22,7 @@ import { SettingsRowItem } from "./components/settings-navigation-item"; type Status = "ready" | "active" | "done"; export function AdvancedCategorySettings() { + const { theme } = useTheme(); const url = useStore((state) => state.url); const setURL = useStore((state) => state.setURL); const defaultUrl = DEFAULT_URL; @@ -91,7 +93,7 @@ export function AdvancedCategorySettings() { placeholder={ url != defaultUrl ? url : t("enter-custom-node-url") } - placeholderTextColor="#999" + placeholderTextColor={theme.colors.textMuted} onChangeText={setNewUrl} onSubmitEditing={onSubmitUrl} textContentType="URL" diff --git a/js/app/components/settings/badge-issuer-panel.tsx b/js/app/components/settings/badge-issuer-panel.tsx index 7e2d5d4f..bbace688 100644 --- a/js/app/components/settings/badge-issuer-panel.tsx +++ b/js/app/components/settings/badge-issuer-panel.tsx @@ -1,6 +1,7 @@ import { BlobRef } from "@atproto/lexicon"; import { Button, + hexToRgba, Input, MenuContainer, MenuGroup, @@ -53,13 +54,6 @@ function getDidFromAtUri(uri: string) { return null; } -function withAlpha(hex: string, alpha: number): string { - const r = parseInt(hex.slice(1, 3), 16); - const g = parseInt(hex.slice(3, 5), 16); - const b = parseInt(hex.slice(5, 7), 16); - return `rgba(${r}, ${g}, ${b}, ${alpha})`; -} - function BadgeDefRow({ def, selected, @@ -117,7 +111,7 @@ function BadgeDefRow({ px[4], { backgroundColor: selected - ? withAlpha(theme.colors.primary, 0.09) + ? hexToRgba(theme.colors.primary, 0.09) : "transparent", borderRadius: radiusTokens.md, }, @@ -558,7 +552,7 @@ export function BadgeIssuerPanel() { width: 32, height: 32, borderRadius: radiusTokens.xl, - backgroundColor: withAlpha(theme.colors.primary, 0.13), + backgroundColor: hexToRgba(theme.colors.primary, 0.13), alignItems: "center", justifyContent: "center", }} @@ -585,9 +579,9 @@ export function BadgeIssuerPanel() { p[3], gap.all[1], { - backgroundColor: withAlpha(theme.colors.success, 0.13), + backgroundColor: hexToRgba(theme.colors.success, 0.13), borderWidth: 1, - borderColor: withAlpha(theme.colors.success, 0.27), + borderColor: hexToRgba(theme.colors.success, 0.27), }, ]} > diff --git a/js/app/components/settings/key-manager.tsx b/js/app/components/settings/key-manager.tsx index 8bb590e9..dfb96b2f 100644 --- a/js/app/components/settings/key-manager.tsx +++ b/js/app/components/settings/key-manager.tsx @@ -14,6 +14,7 @@ import { PlaceStreamKey } from "streamplace"; import { timeAgo } from "utils/timeAgo"; import { Text, zero } from "@streamplace/components"; +import { fontFamilies } from "@streamplace/components/src/lib/theme/tokens"; import { X } from "lucide-react-native"; import { useTranslation } from "react-i18next"; @@ -46,7 +47,7 @@ function KeyRow({ URL: {url.length > 50 diff --git a/js/app/components/timer.tsx b/js/app/components/timer.tsx index 0effac06..d126d2c8 100644 --- a/js/app/components/timer.tsx +++ b/js/app/components/timer.tsx @@ -1,4 +1,5 @@ import { Text, zero } from "@streamplace/components"; +import { fontFamilies } from "@streamplace/components/src/lib/theme/tokens"; import { useEffect, useState } from "react"; import { View } from "react-native"; @@ -36,7 +37,7 @@ export default function Timer({ start }: { start: string | Date }) { > diff --git a/js/components/src/components/ui/admonition.tsx b/js/components/src/components/ui/admonition.tsx index d883ed23..731a8668 100644 --- a/js/components/src/components/ui/admonition.tsx +++ b/js/components/src/components/ui/admonition.tsx @@ -1,6 +1,6 @@ import { AlertCircle, CheckCircle, Info, XCircle } from "lucide-react-native"; import { View, ViewStyle } from "react-native"; -import { useTheme } from "../../ui"; +import { hexToRgba, useTheme } from "../../ui"; import { Text } from "./text"; type AdmonitionVariant = "default" | "success" | "error" | "info" | "warning"; @@ -49,19 +49,19 @@ export function Admonition({ borderColor: theme.colors.border, }, success: { - backgroundColor: theme.colors.success + "15", + backgroundColor: hexToRgba(theme.colors.success, 0.08), borderColor: theme.colors.success, }, error: { - backgroundColor: theme.colors.destructive + "15", + backgroundColor: hexToRgba(theme.colors.destructive, 0.08), borderColor: theme.colors.destructive, }, info: { - backgroundColor: theme.colors.info + "15", + backgroundColor: hexToRgba(theme.colors.info, 0.08), borderColor: theme.colors.info, }, warning: { - backgroundColor: theme.colors.warning + "15", + backgroundColor: hexToRgba(theme.colors.warning, 0.08), borderColor: theme.colors.warning, }, }; diff --git a/js/components/src/components/ui/index.ts b/js/components/src/components/ui/index.ts index f1610633..95d66abe 100644 --- a/js/components/src/components/ui/index.ts +++ b/js/components/src/components/ui/index.ts @@ -40,3 +40,5 @@ export type { TextProps } from "./text"; export type { ViewProps } from "./view"; export * from "../../lib/theme"; + +export { hexToRgba } from "../../lib/utils"; diff --git a/js/components/src/components/ui/menu.tsx b/js/components/src/components/ui/menu.tsx index 78703755..93e4de54 100644 --- a/js/components/src/components/ui/menu.tsx +++ b/js/components/src/components/ui/menu.tsx @@ -22,7 +22,7 @@ import { px, py, } from "../../lib/theme/atoms"; -import { mergeStyles, useTheme } from "../../ui"; +import { hexToRgba, mergeStyles, useTheme } from "../../ui"; import { Text } from "./text"; export interface MenuContainerProps { @@ -53,7 +53,7 @@ export const MenuGroup = forwardRef( { error?: boolean; @@ -30,7 +29,6 @@ export interface InputPrimitiveProps onBlur?: (event: BlurEvent) => void; } -// Input root primitive - the main TextInput component export const InputRoot = forwardRef( ( { @@ -99,6 +97,15 @@ export const InputRoot = forwardRef( [onBlur], ); + const colorStyles = { + borderColor: error ? theme.colors.destructive : theme.colors.border, + ...(disabled && { + backgroundColor: theme.colors.muted, + opacity: 0.6, + }), + ...(loading && { opacity: 0.7 }), + }; + return ( ( onBlur={handleBlur} editable={!disabled && !loading && editable} placeholderTextColor={placeholderTextColor} - style={[ - primitiveStyles.input, - style, - error && primitiveStyles.inputError, - disabled && primitiveStyles.inputDisabled, - loading && primitiveStyles.inputLoading, - ]} + style={[structuralStyles.input, colorStyles, style]} {...props} /> ); @@ -123,7 +124,6 @@ export const InputRoot = forwardRef( InputRoot.displayName = "InputRoot"; -// Input container primitive - wraps input with additional elements export interface InputContainerProps extends ViewProps { focused?: boolean; error?: boolean; @@ -142,16 +142,25 @@ export const InputContainer = forwardRef( }, ref, ) => { + const { theme } = useTheme(); + + const colorStyles = { + borderColor: error + ? theme.colors.destructive + : focused + ? theme.colors.ring + : theme.colors.border, + backgroundColor: theme.colors.background, + ...(disabled && { + backgroundColor: theme.colors.muted, + opacity: 0.6, + }), + }; + return ( {children} @@ -162,7 +171,6 @@ export const InputContainer = forwardRef( InputContainer.displayName = "InputContainer"; -// Input label primitive export interface InputLabelProps extends TextProps { required?: boolean; disabled?: boolean; @@ -181,19 +189,27 @@ export const InputLabel = forwardRef( }, ref, ) => { + const { theme } = useTheme(); + + const colorStyle = { + color: error + ? theme.colors.destructive + : disabled + ? theme.colors.textMuted + : theme.colors.text, + ...(disabled && { opacity: 0.6 }), + }; + return ( {children} - {required && *} + {required && ( + * + )} ); }, @@ -201,7 +217,6 @@ export const InputLabel = forwardRef( InputLabel.displayName = "InputLabel"; -// Input description/helper text primitive export interface InputDescriptionProps extends TextProps { error?: boolean; disabled?: boolean; @@ -209,15 +224,21 @@ export interface InputDescriptionProps extends TextProps { export const InputDescription = forwardRef( ({ children, error = false, disabled = false, style, ...props }, ref) => { + const { theme } = useTheme(); + + const colorStyle = { + color: error + ? theme.colors.destructive + : disabled + ? theme.colors.textMuted + : theme.colors.textMuted, + ...(disabled && { opacity: 0.6 }), + }; + return ( {children} @@ -228,17 +249,26 @@ export const InputDescription = forwardRef( InputDescription.displayName = "InputDescription"; -// Input error message primitive export interface InputErrorProps extends TextProps { visible?: boolean; } export const InputError = forwardRef( ({ children, visible = true, style, ...props }, ref) => { + const { theme } = useTheme(); + if (!visible || !children) return null; return ( - + {children} ); @@ -247,7 +277,6 @@ export const InputError = forwardRef( InputError.displayName = "InputError"; -// Input addon primitive (for icons, buttons, etc.) export interface InputAddonProps extends ViewProps { position?: "left" | "right"; touchable?: boolean; @@ -270,14 +299,13 @@ export const InputAddon = forwardRef< ref, ) => { const addonStyle = [ - primitiveStyles.addon, - primitiveStyles[ - `addon${position.charAt(0).toUpperCase() + position.slice(1)}` as keyof typeof primitiveStyles + structuralStyles.addon, + structuralStyles[ + `addon${position.charAt(0).toUpperCase() + position.slice(1)}` as keyof typeof structuralStyles ], style, ]; - // Filter out null event handlers for TouchableOpacity compatibility const { onBlur, onFocus, ...restProps } = props; const touchableProps = { ...restProps, @@ -312,7 +340,6 @@ export const InputAddon = forwardRef< InputAddon.displayName = "InputAddon"; -// Input group primitive - groups label, input, description, error export interface InputGroupProps extends ViewProps { spacing?: number; } @@ -322,7 +349,7 @@ export const InputGroup = forwardRef( return ( {children} @@ -333,15 +360,13 @@ export const InputGroup = forwardRef( InputGroup.displayName = "InputGroup"; -// Primitive styles (minimal, unstyled) -const primitiveStyles = StyleSheet.create({ +const structuralStyles = StyleSheet.create({ input: { - minHeight: 44, // iOS minimum touch target + minHeight: 44, paddingHorizontal: 12, paddingVertical: 8, fontSize: 16, borderWidth: 1, - borderColor: "#d1d5db", borderRadius: 8, boxShadow: "none", fontFamily: tokens.fontFamilies.regular, @@ -355,75 +380,25 @@ const primitiveStyles = StyleSheet.create({ }, }), }, - inputFocused: { - // No focus styles for the actual input - }, - inputError: { - borderColor: "#ef4444", - borderWidth: 1, - }, - inputDisabled: { - backgroundColor: "#f3f4f6", - borderColor: "#e5e7eb", - opacity: 0.6, - }, - inputLoading: { - opacity: 0.7, - }, container: { flexDirection: "row", alignItems: "center", borderWidth: 1, - borderColor: "#d1d5db", borderRadius: 8, - backgroundColor: "white", paddingHorizontal: 12, minHeight: 44, }, - containerFocused: { - borderColor: "#3b82f6", - borderWidth: 1, - }, - containerError: { - borderColor: "#ef4444", - borderWidth: 1, - }, - containerDisabled: { - backgroundColor: "#f3f4f6", - borderColor: "#e5e7eb", - opacity: 0.6, - }, label: { fontSize: 14, fontWeight: "500", - color: "#374151", marginBottom: 4, }, - labelError: { - color: "#ef4444", - }, - labelDisabled: { - color: "#9ca3af", - opacity: 0.6, - }, - required: { - color: "#ef4444", - }, description: { fontSize: 12, - color: "#6b7280", marginTop: 4, }, - descriptionError: { - color: "#ef4444", - }, - descriptionDisabled: { - color: "#9ca3af", - opacity: 0.6, - }, error: { fontSize: 12, - color: "#ef4444", marginTop: 4, }, addon: { @@ -442,7 +417,6 @@ const primitiveStyles = StyleSheet.create({ }, }); -// Export primitive collection export const InputPrimitive = { Root: InputRoot, Container: InputContainer, diff --git a/js/components/src/components/ui/primitives/modal.tsx b/js/components/src/components/ui/primitives/modal.tsx index b1fd83e2..bb0e509d 100644 --- a/js/components/src/components/ui/primitives/modal.tsx +++ b/js/components/src/components/ui/primitives/modal.tsx @@ -13,6 +13,7 @@ import { View, ViewProps, } from "react-native"; +import { useTheme } from "../../../lib/theme/theme"; const { width: screenWidth, height: screenHeight } = Dimensions.get("window"); @@ -200,14 +201,15 @@ export interface ModalHeaderProps extends ViewProps { export const ModalHeader = forwardRef( ({ children, withBorder = false, style, ...props }, ref) => { + const { theme } = useTheme(); + const borderStyle = withBorder + ? { borderBottomWidth: 1, borderBottomColor: theme.colors.border } + : null; + return ( {children} @@ -269,12 +271,17 @@ export const ModalFooter = forwardRef( }, ref, ) => { + const { theme } = useTheme(); + const borderStyle = withBorder + ? { borderTopWidth: 1, borderTopColor: theme.colors.border } + : null; + return (