@drakkar.software/octospaces-ui 0.3.0 → 0.4.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/dist/index.d.ts +245 -3
- package/dist/index.js +284 -0
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/index.ts +16 -1
- package/src/lightbox/Lightbox.tsx +132 -0
- package/src/sidebar/Sidebar.tsx +101 -0
- package/src/sidebar/SidebarActionButton.tsx +86 -0
- package/src/sidebar/SidebarHeader.tsx +111 -0
- package/src/sidebar/SidebarItem.tsx +148 -0
- package/src/sidebar/index.ts +11 -0
- package/src/theme/helpers.test.ts +1 -0
- package/src/theme/types.ts +1 -0
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/theme/provider.tsx","../src/theme/helpers.ts","../src/discover/filter.ts","../src/discover/DiscoverRow.tsx","../src/discover/DiscoverList.tsx","../src/discover/DiscoverScreen.tsx","../src/sidebar/SpacesRail.tsx","../src/sidebar/tile-state.ts"],"sourcesContent":["/**\n * Theme injection plumbing — provider + hook.\n *\n * The package carries ZERO theme values. The host app builds a concrete {@link Theme}\n * and wraps its tree in `<OctoSpacesThemeProvider theme={resolvedTheme}>`.\n * All primitives then call `useOctoSpacesTheme()` to read the active theme.\n */\nimport React, { createContext, useContext } from 'react';\nimport type { Theme } from './types.js';\n\nconst ThemeContext = createContext<Theme | null>(null);\n\nexport interface OctoSpacesThemeProviderProps {\n theme: Theme;\n children: React.ReactNode;\n}\n\n/**\n * Wrap your root component with this provider to inject the resolved Theme into\n * every primitive from `@drakkar.software/octospaces-ui`.\n *\n * @example\n * ```tsx\n * import { OctoSpacesThemeProvider } from '@drakkar.software/octospaces-ui';\n * import { resolvedTheme } from '@/theme'; // your app's theme\n *\n * export default function App() {\n * return (\n * <OctoSpacesThemeProvider theme={resolvedTheme}>\n * <RootNavigator />\n * </OctoSpacesThemeProvider>\n * );\n * }\n * ```\n */\nexport function OctoSpacesThemeProvider({ theme, children }: OctoSpacesThemeProviderProps) {\n return <ThemeContext.Provider value={theme}>{children}</ThemeContext.Provider>;\n}\n\n/**\n * Read the active theme. Throws if called outside an `<OctoSpacesThemeProvider>` —\n * this is intentional: a missing provider means primitives have no colors/spacing,\n * so a hard failure with a clear message is better than a silent rendering bug.\n */\nexport function useOctoSpacesTheme(): Theme {\n const theme = useContext(ThemeContext);\n if (!theme) {\n throw new Error(\n '[octospaces-ui] useOctoSpacesTheme() called outside of <OctoSpacesThemeProvider>. ' +\n 'Wrap your root component with <OctoSpacesThemeProvider theme={…}>.',\n );\n }\n return theme;\n}\n","/**\n * Pure palette-helper functions over a {@link Palette}. No theme values live here —\n * only functions that DERIVE from the injected palette (or from passed-in colors).\n *\n * Import these from `@drakkar.software/octospaces-ui` (re-exported by `src/index.ts`).\n */\nimport type { Palette, ShadowToken, Theme } from './types.js';\n\n// ── Presence ──────────────────────────────────────────────────────────────────\n\n/** Map a presence status string to the corresponding palette color. */\nexport function presenceColor(\n palette: Palette,\n status: 'online' | 'away' | 'busy' | 'offline' | string,\n): string {\n switch (status) {\n case 'online': return palette.presenceOnline;\n case 'away': return palette.presenceAway;\n case 'busy': return palette.presenceBusy;\n default: return palette.presenceOffline;\n }\n}\n\n// ── Verification ──────────────────────────────────────────────────────────────\n\n/** Map a verification level to the corresponding palette color. */\nexport function verificationColor(\n palette: Palette,\n level: 'verified' | 'partial' | 'none' | string,\n): string {\n switch (level) {\n case 'verified': return palette.verificationVerified;\n case 'partial': return palette.verificationPartial;\n default: return palette.verificationNone;\n }\n}\n\n// ── Avatar ────────────────────────────────────────────────────────────────────\n\nconst AVATAR_TINT_KEYS = [\n 'primary', 'success', 'warning', 'danger', 'info',\n] as const;\n\n/** Stable avatar background tint derived from a userId string. */\nexport function avatarTint(palette: Palette, userId: string): string {\n let hash = 0;\n for (let i = 0; i < userId.length; i++) hash = (hash * 31 + userId.charCodeAt(i)) | 0;\n const key = AVATAR_TINT_KEYS[Math.abs(hash) % AVATAR_TINT_KEYS.length];\n return (palette as unknown as Record<string, string>)[key] ?? palette.primary;\n}\n\n// ── Swatch ────────────────────────────────────────────────────────────────────\n\n/** Look up a named swatch; falls back to `palette.primary` if absent. */\nexport function swatch(theme: Theme, name: string): string {\n return theme.swatches[name] ?? theme.colors.primary;\n}\n\n// ── Borders ───────────────────────────────────────────────────────────────────\n\n/** Derive a `borderColor` value for a \"paper\" (elevated surface) border. */\nexport function paperBorder(palette: Palette): string {\n return palette.borderSubtle;\n}\n\n// ── Shadows ───────────────────────────────────────────────────────────────────\n\n/** Build a glow shadow token from a base color (used for focus rings, highlights). */\nexport function glowShadow(color: string, radius = 8, opacity = 0.4): ShadowToken {\n return {\n shadowColor: color,\n shadowOffset: { width: 0, height: 0 },\n shadowOpacity: opacity,\n shadowRadius: radius,\n elevation: 4,\n };\n}\n\n// ── Focus ring ────────────────────────────────────────────────────────────────\n\n/** Style object for a keyboard-focus indicator (web + React Native). */\nexport function focusRingStyle(\n palette: Palette,\n width = 2,\n): {\n borderWidth: number;\n borderColor: string;\n borderStyle: 'solid';\n} {\n return { borderWidth: width, borderColor: palette.focus, borderStyle: 'solid' };\n}\n\n// ── Status color ──────────────────────────────────────────────────────────────\n\n/** Map a semantic status name to its palette color. */\nexport function statusColor(\n palette: Palette,\n status: 'success' | 'warning' | 'danger' | 'info' | string,\n muted = false,\n): string {\n if (muted) {\n switch (status) {\n case 'success': return palette.successMuted;\n case 'warning': return palette.warningMuted;\n case 'danger': return palette.dangerMuted;\n default: return palette.infoMuted;\n }\n }\n switch (status) {\n case 'success': return palette.success;\n case 'warning': return palette.warning;\n case 'danger': return palette.danger;\n default: return palette.info;\n }\n}\n","import type { DiscoverEntry } from './types.js';\n\n/**\n * Case-insensitive substring filter over a `DiscoverEntry[]`.\n *\n * Returns the original array reference unchanged when `query` is blank so the\n * caller can skip a re-render. Pure function — no side effects.\n */\nexport function filterDiscoverEntries(\n entries: DiscoverEntry[],\n query: string,\n): DiscoverEntry[] {\n const q = query.trim().toLowerCase();\n if (!q) return entries;\n return entries.filter((e) => e.title.toLowerCase().includes(q));\n}\n\n/**\n * Sort discover entries by updatedAt descending (most recent first).\n * Entries without an `updatedAt` field sort last. Pure function.\n */\nexport function sortDiscoverEntries(entries: DiscoverEntry[]): DiscoverEntry[] {\n return [...entries].sort((a, b) => (b.updatedAt ?? 0) - (a.updatedAt ?? 0));\n}\n","/**\n * A single row in the Discover list — shows the object's emoji/icon, title,\n * and type. All app-specific behaviour is injected via props:\n * - `renderIcon` — render a type/emoji icon; receives the entry and must\n * return a ReactNode (null for no icon).\n * - `onOpen` — called when the row is pressed.\n *\n * Styled entirely from the injected {@link Theme} via `useOctoSpacesTheme()`.\n */\nimport React, { useCallback } from 'react';\nimport { Pressable, Text, View } from 'react-native';\n\nimport { useOctoSpacesTheme } from '../theme/provider.js';\nimport type { DiscoverEntry } from './types.js';\n\nexport interface DiscoverRowProps {\n entry: DiscoverEntry;\n /** Render a leading icon for the entry. Return `null` to show nothing. */\n renderIcon?: (entry: DiscoverEntry) => React.ReactNode;\n /** Called when the user taps the row. */\n onOpen: (entry: DiscoverEntry) => void;\n}\n\nexport function DiscoverRow({ entry, renderIcon, onOpen }: DiscoverRowProps) {\n const theme = useOctoSpacesTheme();\n\n const handlePress = useCallback(() => {\n onOpen(entry);\n }, [entry, onOpen]);\n\n const icon = renderIcon ? renderIcon(entry) : null;\n const displayEmoji = !icon && entry.emoji ? entry.emoji : null;\n\n return (\n <Pressable\n onPress={handlePress}\n style={({ pressed }) => ({\n flexDirection: 'row',\n alignItems: 'center',\n paddingVertical: (theme.spacing['3'] as number) ?? 12,\n paddingHorizontal: (theme.spacing['4'] as number) ?? 16,\n backgroundColor: pressed\n ? (theme.colors.surface ?? '#f5f5f5')\n : 'transparent',\n borderRadius: (theme.radii['sm'] as number) ?? 6,\n })}\n accessibilityRole=\"button\"\n accessibilityLabel={entry.title || 'Untitled'}\n >\n {/* Leading icon / emoji */}\n {(icon || displayEmoji) && (\n <View\n style={{\n width: 28,\n height: 28,\n marginRight: (theme.spacing['2'] as number) ?? 8,\n alignItems: 'center',\n justifyContent: 'center',\n flexShrink: 0,\n }}\n >\n {icon ?? (\n <Text style={{ fontSize: 18, lineHeight: 24 }}>{displayEmoji}</Text>\n )}\n </View>\n )}\n\n {/* Title + type subtitle */}\n <View style={{ flex: 1, minWidth: 0 }}>\n <Text\n numberOfLines={1}\n style={{\n fontSize: (theme.type['body']?.size ?? 15),\n lineHeight: (theme.type['body']?.lineHeight ?? 22),\n color: entry.title ? theme.colors.text : theme.colors.textTertiary,\n fontFamily: theme.fonts['body'] ?? undefined,\n }}\n >\n {entry.title || 'Untitled'}\n </Text>\n <Text\n numberOfLines={1}\n style={{\n fontSize: (theme.type['caption']?.size ?? 12),\n lineHeight: (theme.type['caption']?.lineHeight ?? 18),\n color: theme.colors.textSecondary,\n marginTop: 1,\n textTransform: 'capitalize',\n }}\n >\n {entry.type}\n </Text>\n </View>\n </Pressable>\n );\n}\n","/**\n * A themed FlatList wrapper that renders a list of {@link DiscoverEntry} rows.\n *\n * All app-specific behaviour is injected via props so this component has zero\n * imports from any specific OctoSpaces app.\n */\nimport React, { useCallback } from 'react';\nimport { FlatList, RefreshControl, Text, View } from 'react-native';\n\nimport { useOctoSpacesTheme } from '../theme/provider.js';\nimport { DiscoverRow } from './DiscoverRow.js';\nimport type { DiscoverEntry } from './types.js';\n\nexport interface DiscoverListProps {\n entries: DiscoverEntry[];\n /** Render a leading icon for each row — see {@link DiscoverRowProps.renderIcon}. */\n renderIcon?: (entry: DiscoverEntry) => React.ReactNode;\n /** Called when a row is tapped. */\n onOpen: (entry: DiscoverEntry) => void;\n /** Text shown when `entries` is empty (default: \"No public objects found\"). */\n emptyMessage?: string;\n /** Whether a pull-to-refresh is currently in progress. */\n refreshing?: boolean;\n /** Called when the user pulls to refresh. */\n onRefresh?: () => void;\n}\n\nexport function DiscoverList({\n entries,\n renderIcon,\n onOpen,\n emptyMessage = 'No public objects found',\n refreshing,\n onRefresh,\n}: DiscoverListProps) {\n const theme = useOctoSpacesTheme();\n\n const renderItem = useCallback(\n ({ item }: { item: DiscoverEntry }) => (\n <DiscoverRow entry={item} renderIcon={renderIcon} onOpen={onOpen} />\n ),\n [renderIcon, onOpen],\n );\n\n const keyExtractor = useCallback(\n (item: DiscoverEntry) => `${item.spaceId}:${item.id}`,\n [],\n );\n\n if (entries.length === 0) {\n return (\n <View\n style={{\n flex: 1,\n alignItems: 'center',\n justifyContent: 'center',\n paddingHorizontal: (theme.spacing['6'] as number) ?? 24,\n }}\n >\n <Text\n style={{\n fontSize: theme.type['body']?.size ?? 15,\n color: theme.colors.textSecondary,\n textAlign: 'center',\n }}\n >\n {emptyMessage}\n </Text>\n </View>\n );\n }\n\n return (\n <FlatList\n data={entries}\n renderItem={renderItem}\n keyExtractor={keyExtractor}\n contentContainerStyle={{ paddingVertical: (theme.spacing['1'] as number) ?? 4 }}\n showsVerticalScrollIndicator={false}\n removeClippedSubviews\n refreshControl={\n onRefresh ? (\n <RefreshControl\n refreshing={refreshing ?? false}\n onRefresh={onRefresh}\n tintColor={theme.colors.primary}\n />\n ) : undefined\n }\n />\n );\n}\n","/**\n * Generic public-object discovery screen.\n *\n * Loads the world-readable public-object directory via `loadEntries`, renders a\n * search bar, and delegates row rendering + tap behaviour to the injected props.\n * No app-specific logic lives here — all customisation is via props:\n *\n * ```tsx\n * <DiscoverScreen\n * loadEntries={readObjectDirectory}\n * renderIcon={(e) => <TypeIcon entry={e} />}\n * onOpen={(e) => router.push({ pathname: routeForNode(e), params: { id: e.id, spaceId: e.spaceId } })}\n * />\n * ```\n *\n * State machine:\n * idle → loading → (ready | error)\n * Any pull of `loadEntries` updates the entries; errors show a retry button.\n */\nimport React, { useCallback, useEffect, useImperativeHandle, useRef, useState } from 'react';\nimport { ActivityIndicator, Pressable, Text, TextInput, View, type TextStyle } from 'react-native';\n\nimport { useOctoSpacesTheme } from '../theme/provider.js';\nimport { DiscoverList } from './DiscoverList.js';\nimport { filterDiscoverEntries, sortDiscoverEntries } from './filter.js';\nimport type { DiscoverEntry } from './types.js';\n\nexport interface DiscoverScreenProps {\n /**\n * Async function that resolves to the current public-object directory.\n * Typically `readObjectDirectory` from `@drakkar.software/octospaces-sdk`.\n * Called on mount and when `refresh()` is triggered.\n */\n loadEntries: () => Promise<DiscoverEntry[]>;\n /** Render a leading icon for each row. */\n renderIcon?: (entry: DiscoverEntry) => React.ReactNode;\n /** Called when the user taps a row — navigate to the object. */\n onOpen: (entry: DiscoverEntry) => void;\n /**\n * Optional heading text shown above the search bar.\n * @default \"Discover\"\n */\n title?: string;\n /**\n * Text shown when the directory is empty after loading.\n * @default \"No public objects yet\"\n */\n emptyMessage?: string;\n /**\n * Text shown when the directory is empty due to an active search query.\n * @default \"No results for «query»\"\n */\n emptySearchMessage?: string;\n /**\n * Whether to show the inline search bar.\n * @default true\n */\n searchEnabled?: boolean;\n /**\n * Optional ref whose `.current` is set to a `reload()` function once mounted.\n * Lets a host (e.g. a tab screen) trigger a soft-refresh on focus without\n * blanking the existing list — identical to pull-to-refresh behaviour.\n *\n * ```tsx\n * const reloadRef = useRef<() => void>(null);\n * useFocusEffect(useCallback(() => { reloadRef.current?.(); }, []));\n * <DiscoverScreen reloadRef={reloadRef} ... />\n * ```\n */\n reloadRef?: React.RefObject<(() => void) | null>;\n}\n\ntype State =\n | { status: 'idle' }\n | { status: 'loading' }\n | { status: 'ready'; entries: DiscoverEntry[] }\n | { status: 'error'; message: string };\n\nexport function DiscoverScreen({\n loadEntries,\n renderIcon,\n onOpen,\n title = 'Discover',\n emptyMessage = 'No public objects yet',\n emptySearchMessage,\n searchEnabled = true,\n reloadRef,\n}: DiscoverScreenProps) {\n const theme = useOctoSpacesTheme();\n const [state, setState] = useState<State>({ status: 'idle' });\n const [query, setQuery] = useState('');\n const [refreshing, setRefreshing] = useState(false);\n const cancelledRef = useRef(false);\n\n const load = useCallback(async () => {\n setState({ status: 'loading' });\n try {\n const raw = await loadEntries();\n if (cancelledRef.current) return;\n setState({ status: 'ready', entries: sortDiscoverEntries(raw) });\n } catch (err) {\n if (cancelledRef.current) return;\n setState({\n status: 'error',\n message: err instanceof Error ? err.message : 'Failed to load directory',\n });\n }\n }, [loadEntries]);\n\n /** Pull-to-refresh: re-fetches without blanking the existing list. */\n const handleRefresh = useCallback(async () => {\n setRefreshing(true);\n try {\n const raw = await loadEntries();\n if (cancelledRef.current) return;\n setState({ status: 'ready', entries: sortDiscoverEntries(raw) });\n } catch {\n // keep the existing list on refresh failure; the retry button remains for error state\n } finally {\n if (!cancelledRef.current) setRefreshing(false);\n }\n }, [loadEntries]);\n\n // Expose handleRefresh via reloadRef so a host can trigger a soft reload on focus.\n useImperativeHandle(reloadRef, () => handleRefresh, [handleRefresh]);\n\n useEffect(() => {\n cancelledRef.current = false;\n void load();\n return () => {\n cancelledRef.current = true;\n };\n }, [load]);\n\n // ── Derived list ─────────────────────────────────────────────────────────\n const allEntries = state.status === 'ready' ? state.entries : [];\n const visibleEntries = filterDiscoverEntries(allEntries, query);\n const noSearchResults = !!query.trim() && visibleEntries.length === 0 && allEntries.length > 0;\n const resolvedEmptyMessage = noSearchResults\n ? (emptySearchMessage ?? `No results for \"${query.trim()}\"`)\n : emptyMessage;\n\n // ── Palette shortcuts ─────────────────────────────────────────────────────\n const sp2 = (theme.spacing['2'] as number) ?? 8;\n const sp3 = (theme.spacing['3'] as number) ?? 12;\n const sp4 = (theme.spacing['4'] as number) ?? 16;\n const radMd = (theme.radii['md'] as number) ?? 8;\n\n return (\n <View style={{ flex: 1, backgroundColor: theme.colors.background }}>\n {/* Header */}\n <View\n style={{\n paddingHorizontal: sp4,\n paddingTop: sp4,\n paddingBottom: sp2,\n }}\n >\n <Text\n style={{\n fontSize: theme.type['title2']?.size ?? 22,\n fontWeight: (theme.type['title2']?.weight as TextStyle['fontWeight']) ?? '700',\n lineHeight: theme.type['title2']?.lineHeight ?? 28,\n color: theme.colors.text,\n fontFamily: theme.fonts['heading'] ?? undefined,\n marginBottom: sp3,\n }}\n >\n {title}\n </Text>\n\n {/* Search bar */}\n {searchEnabled && (\n <View\n style={{\n flexDirection: 'row',\n alignItems: 'center',\n backgroundColor: theme.colors.surfaceInput ?? theme.colors.surface,\n borderRadius: radMd,\n borderWidth: 1,\n borderColor: theme.colors.borderSubtle,\n paddingHorizontal: sp3,\n height: 40,\n }}\n >\n <TextInput\n placeholder=\"Search…\"\n placeholderTextColor={theme.colors.textTertiary}\n value={query}\n onChangeText={setQuery}\n style={{\n flex: 1,\n fontSize: theme.type['body']?.size ?? 15,\n color: theme.colors.text,\n fontFamily: theme.fonts['body'] ?? undefined,\n }}\n returnKeyType=\"search\"\n clearButtonMode=\"while-editing\"\n accessibilityLabel=\"Search discover\"\n />\n </View>\n )}\n </View>\n\n {/* Body */}\n {state.status === 'loading' ? (\n <View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>\n <ActivityIndicator color={theme.colors.primary} />\n </View>\n ) : state.status === 'error' ? (\n <View\n style={{\n flex: 1,\n alignItems: 'center',\n justifyContent: 'center',\n paddingHorizontal: sp4,\n }}\n >\n <Text\n style={{\n color: theme.colors.textSecondary,\n fontSize: theme.type['body']?.size ?? 15,\n textAlign: 'center',\n marginBottom: sp3,\n }}\n >\n {state.message}\n </Text>\n <Pressable\n onPress={load}\n style={{\n paddingHorizontal: sp4,\n paddingVertical: sp2,\n backgroundColor: theme.colors.primary,\n borderRadius: radMd,\n }}\n >\n <Text style={{ color: theme.colors.textOnPrimary, fontWeight: '600' }}>\n Retry\n </Text>\n </Pressable>\n </View>\n ) : (\n <DiscoverList\n entries={visibleEntries}\n renderIcon={renderIcon}\n onOpen={onOpen}\n emptyMessage={resolvedEmptyMessage}\n refreshing={refreshing}\n onRefresh={handleRefresh}\n />\n )}\n </View>\n );\n}\n","/**\n * Headless, abstractly-themed vertical spaces rail.\n *\n * The component reads the injected {@link Theme} via `useOctoSpacesTheme()` and\n * delegates all app-specific concerns to props:\n *\n * - Icons are rendered by `renderIcon` (keeps `@expo/vector-icons` out of this package).\n * - Space tile images are rendered by `renderTileImage` (keeps `expo-image` out too).\n * - Unread badges are rendered by `renderBadge`.\n * - The rail foot (account avatar + menu) is rendered by `renderFoot`.\n * - Web drag-reorder is wired via the `useTileDnd` hook prop (see below).\n *\n * All React Native primitives used here ship with the `react-native` peer dep.\n */\nimport React, { useState } from 'react';\nimport {\n Pressable as RNPressable,\n ScrollView,\n Text,\n View,\n} from 'react-native';\nimport type { PressableProps, View as RNView } from 'react-native';\n\nimport { useOctoSpacesTheme } from '../theme/provider.js';\nimport { railTileState } from './tile-state.js';\nimport type { RailTileTokens } from './tile-state.js';\nimport type { RailIconName, RailSpace } from './types.js';\n\n// ── Pressable with web hover events ───────────────────────────────────────────\n\n// React Native Web supports onMouseEnter/onMouseLeave for hover detection.\n// The peer dep is >=0.75 which includes these events in the ViewProps contract.\n// Cast to ForwardRefExoticComponent so `ref` is a valid JSX prop (Pressable\n// already uses forwardRef internally; this just makes TypeScript aware of it).\ntype HoverProps = { onMouseEnter?: () => void; onMouseLeave?: () => void };\nconst Pressable = RNPressable as React.ForwardRefExoticComponent<\n PressableProps & HoverProps & React.RefAttributes<RNView>\n>;\n\n// ── Props ─────────────────────────────────────────────────────────────────────\n\nexport interface SpacesRailProps {\n /** The spaces to show in the scrollable column. */\n spaces: RailSpace[];\n /** The currently-active space id (or null / undefined for none). */\n activeId?: string | null;\n /** Called when the user selects a space tile. */\n onSelect?: (id: string) => void;\n /** Called when the user taps the \"add\" tile. */\n onAdd?: () => void;\n /** When provided, renders a leading DM-home tile. */\n onSelectDms?: () => void;\n /** Whether the DM-home tile is the active selection. */\n dmsActive?: boolean;\n /** Unread count for the DM-home tile badge. */\n dmUnread?: number;\n /** Accessibility label for the DM-home tile (default: \"Direct messages\"). */\n dmLabel?: string;\n /** Accessibility label for the add-space tile (default: \"Create or join a space\"). */\n addLabel?: string;\n /**\n * Render a named icon at the given size and color. Used for the DM tile icon\n * (`'dm'`), the E2EE lock corner (`'lock'`), the mute corner (`'mute'`),\n * and the add tile icon (`'add'`). Return `null` to suppress the icon slot.\n * If omitted, all icon slots render nothing.\n */\n renderIcon?: (name: RailIconName, size: number, color: string) => React.ReactNode;\n /**\n * Render an image filling the tile background. Only called when `space.image`\n * is set. The component must fill its parent (`StyleSheet.absoluteFill` or\n * equivalent). If omitted, the short-name monogram is shown instead.\n */\n renderTileImage?: (space: RailSpace) => React.ReactNode;\n /**\n * Render an unread badge. Only called when `space.unread > 0`.\n * If omitted, badges are not shown.\n */\n renderBadge?: (count: number) => React.ReactNode;\n /**\n * When `true`, each tile shows a small E2EE-lock corner badge (bottom-right).\n * Requires `renderIcon` to be provided (otherwise the corner renders nothing).\n * Default: `false`.\n */\n showLockCorner?: boolean;\n /**\n * Render the pinned rail foot (e.g. the account avatar and popover).\n * The host app owns this entirely — identity state stays out of the package.\n */\n renderFoot?: () => React.ReactNode;\n /**\n * **Hook injection for web drag-reorder.** When provided, each space tile is\n * wrapped in a `DndTile` that calls `useTileDnd(spaceId)` unconditionally at\n * the top of its render — treat this prop as a React hook and keep it stable\n * for the lifetime of a `SpacesRail` mount (always provided or always absent).\n * Omit on native / in apps that don't need DnD.\n */\n useTileDnd?: (spaceId: string) => { ref?: React.Ref<RNView>; over?: boolean };\n}\n\n// ── Token resolver ─────────────────────────────────────────────────────────────\n\nfunction resolveRailTokens(theme: ReturnType<typeof useOctoSpacesTheme>): RailTileTokens {\n const { colors, swatches } = theme;\n return {\n primary: colors.primary,\n primaryMuted: colors.primaryMuted,\n primarySubtle: colors.primarySubtle,\n surfaceInput: colors.surfaceInput,\n borderSubtle: colors.borderSubtle,\n textOnPrimary: colors.textOnPrimary,\n textSecondary: colors.textSecondary,\n textTertiary: colors.textTertiary,\n railTile: swatches['railTile'] ?? colors.surfaceInput,\n railTileHoverBorder: swatches['railTileHoverBorder'] ?? colors.primarySubtle,\n railGlow: swatches['railGlow'] ?? colors.primary,\n railTileHoverInk: swatches['railTileHoverInk'] ?? colors.primary,\n };\n}\n\n// ── Shared tile dimensions (constant) ────────────────────────────────────────\n\nconst TILE_SIZE = 40;\nconst CORNER_SIZE = 16;\nconst BADGE_OFFSET = -5;\nconst CORNER_OFFSET = -3;\n\n// ── Tile content (non-hook render helper) ─────────────────────────────────────\n\ninterface TileContentProps {\n space: RailSpace;\n labelColor: string;\n fontFamily?: string;\n fontSize: number;\n lineHeight: number;\n cornerBg: string;\n cornerBorder: string;\n cornerIconColor: string;\n renderIcon?: SpacesRailProps['renderIcon'];\n renderTileImage?: SpacesRailProps['renderTileImage'];\n renderBadge?: SpacesRailProps['renderBadge'];\n showLockCorner?: boolean;\n}\n\nfunction TileContent({\n space,\n labelColor,\n fontFamily,\n fontSize,\n lineHeight,\n cornerBg,\n cornerBorder,\n cornerIconColor,\n renderIcon,\n renderTileImage,\n renderBadge,\n showLockCorner,\n}: TileContentProps) {\n return (\n <>\n {/* Image or monogram */}\n {space.image && renderTileImage ? (\n renderTileImage(space)\n ) : (\n <Text\n style={{\n fontSize,\n lineHeight,\n fontWeight: '700',\n fontFamily: fontFamily ?? undefined,\n color: labelColor,\n }}\n numberOfLines={1}\n >\n {space.short}\n </Text>\n )}\n {/* E2EE lock corner (bottom-right) */}\n {showLockCorner && renderIcon ? (\n <View\n style={{\n position: 'absolute',\n bottom: CORNER_OFFSET,\n right: CORNER_OFFSET,\n width: CORNER_SIZE,\n height: CORNER_SIZE,\n borderRadius: CORNER_SIZE / 2,\n borderWidth: 1,\n alignItems: 'center',\n justifyContent: 'center',\n backgroundColor: cornerBg,\n borderColor: cornerBorder,\n }}\n >\n {renderIcon('lock', 9, cornerIconColor)}\n </View>\n ) : null}\n {/* Mute corner (bottom-left) */}\n {space.muted && renderIcon ? (\n <View\n style={{\n position: 'absolute',\n bottom: CORNER_OFFSET,\n left: CORNER_OFFSET,\n width: CORNER_SIZE,\n height: CORNER_SIZE,\n borderRadius: CORNER_SIZE / 2,\n borderWidth: 1,\n alignItems: 'center',\n justifyContent: 'center',\n backgroundColor: cornerBg,\n borderColor: cornerBorder,\n }}\n >\n {renderIcon('mute', 9, cornerIconColor)}\n </View>\n ) : null}\n {/* Unread badge (top-right) */}\n {space.unread ? (\n <View\n style={{\n position: 'absolute',\n top: BADGE_OFFSET,\n right: BADGE_OFFSET,\n }}\n >\n {renderBadge ? renderBadge(space.unread) : null}\n </View>\n ) : null}\n </>\n );\n}\n\n// ── PlainTile — space tile without DnD ────────────────────────────────────────\n\ninterface TileSharedProps {\n space: RailSpace;\n active: boolean;\n onPress?: () => void;\n tokens: RailTileTokens;\n radiusActive: number;\n radiusDefault: number;\n renderIcon?: SpacesRailProps['renderIcon'];\n renderTileImage?: SpacesRailProps['renderTileImage'];\n renderBadge?: SpacesRailProps['renderBadge'];\n showLockCorner?: boolean;\n cornerBg: string;\n cornerBorder: string;\n fontFamily?: string;\n fontSize: number;\n lineHeight: number;\n}\n\nfunction PlainTile({\n space,\n active,\n onPress,\n tokens,\n radiusActive,\n radiusDefault,\n renderIcon,\n renderTileImage,\n renderBadge,\n showLockCorner,\n cornerBg,\n cornerBorder,\n fontFamily,\n fontSize,\n lineHeight,\n}: TileSharedProps) {\n const [hovered, setHovered] = useState(false);\n const s = railTileState({ active, hovered, over: false }, tokens, radiusActive, radiusDefault);\n\n return (\n <Pressable\n onPress={onPress}\n onMouseEnter={() => setHovered(true)}\n onMouseLeave={() => setHovered(false)}\n accessibilityRole=\"button\"\n accessibilityLabel={space.short}\n style={{\n position: 'relative',\n width: TILE_SIZE,\n height: TILE_SIZE,\n alignItems: 'center',\n justifyContent: 'center',\n overflow: 'hidden',\n borderRadius: s.radius,\n backgroundColor: s.bg,\n borderWidth: s.borderWidth,\n borderColor: s.borderColor,\n ...(s.shadow ?? {}),\n }}\n >\n <TileContent\n space={space}\n labelColor={s.labelColor}\n fontFamily={fontFamily}\n fontSize={fontSize}\n lineHeight={lineHeight}\n cornerBg={cornerBg}\n cornerBorder={cornerBorder}\n cornerIconColor={tokens.textTertiary}\n renderIcon={renderIcon}\n renderTileImage={renderTileImage}\n renderBadge={renderBadge}\n showLockCorner={showLockCorner}\n />\n </Pressable>\n );\n}\n\n// ── DndTile — space tile with hook-injected DnD ref + over state ──────────────\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\n\ninterface DndTileProps extends TileSharedProps {\n /** Hook injection: called unconditionally at the top of this component.\n * Treat it as a React hook — always provided for every DndTile. */\n dnd: NonNullable<SpacesRailProps['useTileDnd']>;\n}\n\nfunction DndTile({\n space,\n active,\n onPress,\n tokens,\n radiusActive,\n radiusDefault,\n renderIcon,\n renderTileImage,\n renderBadge,\n showLockCorner,\n cornerBg,\n cornerBorder,\n fontFamily,\n fontSize,\n lineHeight,\n dnd,\n}: DndTileProps) {\n const [hovered, setHovered] = useState(false);\n // Hook injection: useTileDnd is called unconditionally here (it IS a hook).\n // eslint-disable-next-line react-hooks/rules-of-hooks\n const { ref, over = false } = dnd(space.id);\n const s = railTileState({ active, hovered, over }, tokens, radiusActive, radiusDefault);\n\n return (\n <Pressable\n ref={ref as React.Ref<RNView>}\n onPress={onPress}\n onMouseEnter={() => setHovered(true)}\n onMouseLeave={() => setHovered(false)}\n accessibilityRole=\"button\"\n accessibilityLabel={space.short}\n style={{\n position: 'relative',\n width: TILE_SIZE,\n height: TILE_SIZE,\n alignItems: 'center',\n justifyContent: 'center',\n overflow: 'hidden',\n borderRadius: s.radius,\n backgroundColor: s.bg,\n borderWidth: s.borderWidth,\n borderColor: s.borderColor,\n ...(s.shadow ?? {}),\n }}\n >\n <TileContent\n space={space}\n labelColor={s.labelColor}\n fontFamily={fontFamily}\n fontSize={fontSize}\n lineHeight={lineHeight}\n cornerBg={cornerBg}\n cornerBorder={cornerBorder}\n cornerIconColor={tokens.textTertiary}\n renderIcon={renderIcon}\n renderTileImage={renderTileImage}\n renderBadge={renderBadge}\n showLockCorner={showLockCorner}\n />\n </Pressable>\n );\n}\n\n// ── SpacesRail ─────────────────────────────────────────────────────────────────\n\n/**\n * Vertical spaces rail — a 64px-wide column of square space tiles, a DM-home tile,\n * an add-space tile, and a pinned foot for the account widget.\n *\n * Styled entirely from the injected {@link Theme} via `useOctoSpacesTheme()`.\n * All icons, images, badges, and the account foot are provided by the host app.\n */\nexport function SpacesRail({\n spaces,\n activeId,\n onSelect,\n onAdd,\n onSelectDms,\n dmsActive = false,\n dmUnread,\n dmLabel = 'Direct messages',\n addLabel = 'Create or join a space',\n renderIcon,\n renderTileImage,\n renderBadge,\n showLockCorner = false,\n renderFoot,\n useTileDnd,\n}: SpacesRailProps) {\n const theme = useOctoSpacesTheme();\n const { colors, spacing, radii, type: typeScale, fonts, layout } = theme;\n\n const tokens = resolveRailTokens(theme);\n\n // Layout constants with fallbacks for hosts that haven't set them.\n const railWidth = (layout['railWidth'] as number | undefined) ?? 64;\n const spaceV = (spacing['2'] as number | undefined) ?? 8;\n const spaceXs = (spacing['1'] as number | undefined) ?? 4;\n const spaceS = (spacing['2'] as number | undefined) ?? 8;\n const spaceMd = (spacing['3'] as number | undefined) ?? 12;\n\n const radiusActive = (radii['lg'] as number | undefined) ?? 12;\n const radiusDefault = (radii['xl'] as number | undefined) ?? 16;\n\n const footnoteSize = typeScale['footnote']?.size ?? 12;\n const footnoteLineH = typeScale['footnote']?.lineHeight ?? 18;\n const monoFont = fonts['mono'] ?? undefined;\n\n // Corner-badge tokens (background = rail surface, border = rail border).\n const cornerBg = colors.sidebar;\n const cornerBorder = colors.border;\n\n // Shared tile props (passed to every tile variant).\n const tileShared = {\n tokens,\n radiusActive,\n radiusDefault,\n renderIcon,\n renderTileImage,\n renderBadge,\n showLockCorner,\n cornerBg,\n cornerBorder,\n fontFamily: monoFont,\n fontSize: footnoteSize,\n lineHeight: footnoteLineH,\n };\n\n // DM tile hover state (managed here since DM tile is inline, not a separate component).\n const [dmHovered, setDmHovered] = useState(false);\n\n const dmTileStyle = railTileState(\n { active: dmsActive, hovered: dmHovered, over: false },\n tokens,\n radiusActive,\n radiusDefault,\n );\n\n const dmIconColor = dmsActive ? colors.textOnPrimary : dmHovered ? tokens.railTileHoverInk : colors.textSecondary;\n\n // Add-tile hover state.\n const [addHovered, setAddHovered] = useState(false);\n\n // Whether DnD is active (determines which tile variant to render).\n const hasDnd = !!useTileDnd;\n\n return (\n <View\n style={{\n width: railWidth,\n paddingVertical: spaceMd,\n borderRightWidth: 1,\n borderRightColor: colors.border,\n backgroundColor: colors.sidebar,\n alignItems: 'center',\n gap: spaceS,\n }}\n >\n {/* Scrollable tile column */}\n <ScrollView\n style={{ alignSelf: 'stretch', flex: 1 }}\n contentContainerStyle={{\n alignItems: 'center',\n gap: spaceV,\n paddingVertical: spaceXs,\n }}\n showsVerticalScrollIndicator={false}\n >\n {/* DM-home tile (pinned first when provided) */}\n {onSelectDms ? (\n <View style={{ position: 'relative' }}>\n <Pressable\n onPress={onSelectDms}\n onMouseEnter={() => setDmHovered(true)}\n onMouseLeave={() => setDmHovered(false)}\n accessibilityRole=\"button\"\n accessibilityLabel={dmLabel}\n style={{\n width: TILE_SIZE,\n height: TILE_SIZE,\n alignItems: 'center',\n justifyContent: 'center',\n borderRadius: dmTileStyle.radius,\n backgroundColor: dmTileStyle.bg,\n borderWidth: dmTileStyle.borderWidth,\n borderColor: dmTileStyle.borderColor,\n ...(dmTileStyle.shadow ?? {}),\n }}\n >\n {renderIcon ? renderIcon('dm', 20, dmIconColor) : null}\n </Pressable>\n {/* Lock corner */}\n {showLockCorner && renderIcon ? (\n <View\n style={{\n position: 'absolute',\n bottom: CORNER_OFFSET,\n right: CORNER_OFFSET,\n width: CORNER_SIZE,\n height: CORNER_SIZE,\n borderRadius: CORNER_SIZE / 2,\n borderWidth: 1,\n alignItems: 'center',\n justifyContent: 'center',\n backgroundColor: cornerBg,\n borderColor: cornerBorder,\n }}\n >\n {renderIcon('lock', 9, tokens.textTertiary)}\n </View>\n ) : null}\n {/* DM unread badge */}\n {dmUnread ? (\n <View style={{ position: 'absolute', top: BADGE_OFFSET, right: BADGE_OFFSET }}>\n {renderBadge ? renderBadge(dmUnread) : null}\n </View>\n ) : null}\n </View>\n ) : null}\n\n {/* Space tiles */}\n {spaces.map((s) =>\n hasDnd ? (\n <DndTile\n key={s.id}\n space={s}\n active={s.id === activeId}\n onPress={() => onSelect?.(s.id)}\n dnd={useTileDnd!}\n {...tileShared}\n />\n ) : (\n <PlainTile\n key={s.id}\n space={s}\n active={s.id === activeId}\n onPress={() => onSelect?.(s.id)}\n {...tileShared}\n />\n ),\n )}\n\n {/* Add-space tile */}\n <Pressable\n onPress={onAdd}\n onMouseEnter={() => setAddHovered(true)}\n onMouseLeave={() => setAddHovered(false)}\n accessibilityRole=\"button\"\n accessibilityLabel={addLabel}\n style={{\n width: TILE_SIZE,\n height: TILE_SIZE,\n alignItems: 'center',\n justifyContent: 'center',\n borderRadius: radiusDefault,\n borderWidth: 1,\n borderStyle: 'dashed',\n borderColor: addHovered ? colors.border : colors.borderSubtle,\n }}\n >\n {renderIcon ? renderIcon('add', 16, addHovered ? tokens.railTileHoverInk : colors.textTertiary) : null}\n </Pressable>\n </ScrollView>\n\n {/* Pinned foot — account avatar, popover, etc. */}\n {renderFoot ? renderFoot() : null}\n </View>\n );\n}\n","/**\n * Pure helper: maps tile interaction state + resolved theme tokens → visual style.\n * No React or React Native imports — fully testable in isolation.\n */\nimport type { ShadowToken } from '../theme/types.js';\nimport { glowShadow } from '../theme/helpers.js';\n\n// ── Token contract ─────────────────────────────────────────────────────────────\n\n/**\n * Resolved color tokens consumed by {@link railTileState}. Built from a `Theme`\n * object in the component layer (see `resolveRailTokens` in `SpacesRail.tsx`).\n * Swatch entries fall back to palette tokens when the host app hasn't set them.\n */\nexport interface RailTileTokens {\n // Core palette tokens\n primary: string;\n primaryMuted: string;\n primarySubtle: string;\n surfaceInput: string;\n borderSubtle: string;\n textOnPrimary: string;\n textSecondary: string;\n textTertiary: string;\n // Optional swatch overrides (host app can tune rail-specific colors)\n railTile: string; // swatch 'railTile' ?? surfaceInput\n railTileHoverBorder: string; // swatch 'railTileHoverBorder' ?? primarySubtle\n railGlow: string; // swatch 'railGlow' ?? primary\n railTileHoverInk: string; // swatch 'railTileHoverInk' ?? primary\n}\n\n// ── Output ─────────────────────────────────────────────────────────────────────\n\n/** Resolved visual properties for a single rail tile. */\nexport interface RailTileStyle {\n /** Tile background color. */\n bg: string;\n /** Tile border color. */\n borderColor: string;\n /** Tile border width (0 when active, 1 otherwise). */\n borderWidth: number;\n /** Tile border-radius in pixels (squared-off when active/hovered, rounded otherwise). */\n radius: number;\n /** Monogram label color. */\n labelColor: string;\n /** Active glow shadow, or `null` when not active. */\n shadow: ShadowToken | null;\n}\n\n// ── Mapping ────────────────────────────────────────────────────────────────────\n\n/**\n * Map tile interaction state to visual style tokens.\n *\n * @param state Current interaction state.\n * @param tokens Resolved color tokens (see {@link RailTileTokens}).\n * @param radiusActive Border-radius when the tile is active or hovered (squarer look).\n * @param radiusDefault Border-radius for the resting state (rounder look).\n */\nexport function railTileState(\n state: { active: boolean; hovered: boolean; over: boolean },\n tokens: RailTileTokens,\n radiusActive: number,\n radiusDefault: number,\n): RailTileStyle {\n const { active, hovered, over } = state;\n\n let bg: string;\n let borderColor: string;\n let borderWidth: number;\n let labelColor: string;\n\n if (active) {\n bg = tokens.primary;\n borderColor = 'transparent';\n borderWidth = 0;\n labelColor = tokens.textOnPrimary;\n } else if (hovered) {\n bg = tokens.primaryMuted;\n borderColor = tokens.railTileHoverBorder;\n borderWidth = 1;\n labelColor = tokens.railTileHoverInk;\n } else {\n bg = tokens.railTile;\n borderColor = tokens.borderSubtle;\n borderWidth = 1;\n labelColor = tokens.textSecondary;\n }\n\n // Drop-target overlay: accent border when a dragged tile is over this one.\n if (over && !active) {\n borderColor = tokens.primary;\n borderWidth = 1;\n }\n\n const radius = active || hovered || over ? radiusActive : radiusDefault;\n const shadow: ShadowToken | null = active ? glowShadow(tokens.railGlow, 8, 0.3) : null;\n\n return { bg, borderColor, borderWidth, radius, labelColor, shadow };\n}\n"],"mappings":"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","names":["React","React","React","useCallback","Text","View","useCallback","React","View","Text","React","useCallback","Pressable","Text","View","useCallback","React","View","Text","Pressable","React","useState","Text","View","Pressable","React","Text","View","useState"]}
|
|
1
|
+
{"version":3,"sources":["../src/theme/provider.tsx","../src/theme/helpers.ts","../src/discover/filter.ts","../src/discover/DiscoverRow.tsx","../src/discover/DiscoverList.tsx","../src/discover/DiscoverScreen.tsx","../src/sidebar/SpacesRail.tsx","../src/sidebar/tile-state.ts","../src/sidebar/Sidebar.tsx","../src/sidebar/SidebarHeader.tsx","../src/sidebar/SidebarActionButton.tsx","../src/sidebar/SidebarItem.tsx","../src/lightbox/Lightbox.tsx"],"sourcesContent":["/**\n * Theme injection plumbing — provider + hook.\n *\n * The package carries ZERO theme values. The host app builds a concrete {@link Theme}\n * and wraps its tree in `<OctoSpacesThemeProvider theme={resolvedTheme}>`.\n * All primitives then call `useOctoSpacesTheme()` to read the active theme.\n */\nimport React, { createContext, useContext } from 'react';\nimport type { Theme } from './types.js';\n\nconst ThemeContext = createContext<Theme | null>(null);\n\nexport interface OctoSpacesThemeProviderProps {\n theme: Theme;\n children: React.ReactNode;\n}\n\n/**\n * Wrap your root component with this provider to inject the resolved Theme into\n * every primitive from `@drakkar.software/octospaces-ui`.\n *\n * @example\n * ```tsx\n * import { OctoSpacesThemeProvider } from '@drakkar.software/octospaces-ui';\n * import { resolvedTheme } from '@/theme'; // your app's theme\n *\n * export default function App() {\n * return (\n * <OctoSpacesThemeProvider theme={resolvedTheme}>\n * <RootNavigator />\n * </OctoSpacesThemeProvider>\n * );\n * }\n * ```\n */\nexport function OctoSpacesThemeProvider({ theme, children }: OctoSpacesThemeProviderProps) {\n return <ThemeContext.Provider value={theme}>{children}</ThemeContext.Provider>;\n}\n\n/**\n * Read the active theme. Throws if called outside an `<OctoSpacesThemeProvider>` —\n * this is intentional: a missing provider means primitives have no colors/spacing,\n * so a hard failure with a clear message is better than a silent rendering bug.\n */\nexport function useOctoSpacesTheme(): Theme {\n const theme = useContext(ThemeContext);\n if (!theme) {\n throw new Error(\n '[octospaces-ui] useOctoSpacesTheme() called outside of <OctoSpacesThemeProvider>. ' +\n 'Wrap your root component with <OctoSpacesThemeProvider theme={…}>.',\n );\n }\n return theme;\n}\n","/**\n * Pure palette-helper functions over a {@link Palette}. No theme values live here —\n * only functions that DERIVE from the injected palette (or from passed-in colors).\n *\n * Import these from `@drakkar.software/octospaces-ui` (re-exported by `src/index.ts`).\n */\nimport type { Palette, ShadowToken, Theme } from './types.js';\n\n// ── Presence ──────────────────────────────────────────────────────────────────\n\n/** Map a presence status string to the corresponding palette color. */\nexport function presenceColor(\n palette: Palette,\n status: 'online' | 'away' | 'busy' | 'offline' | string,\n): string {\n switch (status) {\n case 'online': return palette.presenceOnline;\n case 'away': return palette.presenceAway;\n case 'busy': return palette.presenceBusy;\n default: return palette.presenceOffline;\n }\n}\n\n// ── Verification ──────────────────────────────────────────────────────────────\n\n/** Map a verification level to the corresponding palette color. */\nexport function verificationColor(\n palette: Palette,\n level: 'verified' | 'partial' | 'none' | string,\n): string {\n switch (level) {\n case 'verified': return palette.verificationVerified;\n case 'partial': return palette.verificationPartial;\n default: return palette.verificationNone;\n }\n}\n\n// ── Avatar ────────────────────────────────────────────────────────────────────\n\nconst AVATAR_TINT_KEYS = [\n 'primary', 'success', 'warning', 'danger', 'info',\n] as const;\n\n/** Stable avatar background tint derived from a userId string. */\nexport function avatarTint(palette: Palette, userId: string): string {\n let hash = 0;\n for (let i = 0; i < userId.length; i++) hash = (hash * 31 + userId.charCodeAt(i)) | 0;\n const key = AVATAR_TINT_KEYS[Math.abs(hash) % AVATAR_TINT_KEYS.length];\n return (palette as unknown as Record<string, string>)[key] ?? palette.primary;\n}\n\n// ── Swatch ────────────────────────────────────────────────────────────────────\n\n/** Look up a named swatch; falls back to `palette.primary` if absent. */\nexport function swatch(theme: Theme, name: string): string {\n return theme.swatches[name] ?? theme.colors.primary;\n}\n\n// ── Borders ───────────────────────────────────────────────────────────────────\n\n/** Derive a `borderColor` value for a \"paper\" (elevated surface) border. */\nexport function paperBorder(palette: Palette): string {\n return palette.borderSubtle;\n}\n\n// ── Shadows ───────────────────────────────────────────────────────────────────\n\n/** Build a glow shadow token from a base color (used for focus rings, highlights). */\nexport function glowShadow(color: string, radius = 8, opacity = 0.4): ShadowToken {\n return {\n shadowColor: color,\n shadowOffset: { width: 0, height: 0 },\n shadowOpacity: opacity,\n shadowRadius: radius,\n elevation: 4,\n };\n}\n\n// ── Focus ring ────────────────────────────────────────────────────────────────\n\n/** Style object for a keyboard-focus indicator (web + React Native). */\nexport function focusRingStyle(\n palette: Palette,\n width = 2,\n): {\n borderWidth: number;\n borderColor: string;\n borderStyle: 'solid';\n} {\n return { borderWidth: width, borderColor: palette.focus, borderStyle: 'solid' };\n}\n\n// ── Status color ──────────────────────────────────────────────────────────────\n\n/** Map a semantic status name to its palette color. */\nexport function statusColor(\n palette: Palette,\n status: 'success' | 'warning' | 'danger' | 'info' | string,\n muted = false,\n): string {\n if (muted) {\n switch (status) {\n case 'success': return palette.successMuted;\n case 'warning': return palette.warningMuted;\n case 'danger': return palette.dangerMuted;\n default: return palette.infoMuted;\n }\n }\n switch (status) {\n case 'success': return palette.success;\n case 'warning': return palette.warning;\n case 'danger': return palette.danger;\n default: return palette.info;\n }\n}\n","import type { DiscoverEntry } from './types.js';\n\n/**\n * Case-insensitive substring filter over a `DiscoverEntry[]`.\n *\n * Returns the original array reference unchanged when `query` is blank so the\n * caller can skip a re-render. Pure function — no side effects.\n */\nexport function filterDiscoverEntries(\n entries: DiscoverEntry[],\n query: string,\n): DiscoverEntry[] {\n const q = query.trim().toLowerCase();\n if (!q) return entries;\n return entries.filter((e) => e.title.toLowerCase().includes(q));\n}\n\n/**\n * Sort discover entries by updatedAt descending (most recent first).\n * Entries without an `updatedAt` field sort last. Pure function.\n */\nexport function sortDiscoverEntries(entries: DiscoverEntry[]): DiscoverEntry[] {\n return [...entries].sort((a, b) => (b.updatedAt ?? 0) - (a.updatedAt ?? 0));\n}\n","/**\n * A single row in the Discover list — shows the object's emoji/icon, title,\n * and type. All app-specific behaviour is injected via props:\n * - `renderIcon` — render a type/emoji icon; receives the entry and must\n * return a ReactNode (null for no icon).\n * - `onOpen` — called when the row is pressed.\n *\n * Styled entirely from the injected {@link Theme} via `useOctoSpacesTheme()`.\n */\nimport React, { useCallback } from 'react';\nimport { Pressable, Text, View } from 'react-native';\n\nimport { useOctoSpacesTheme } from '../theme/provider.js';\nimport type { DiscoverEntry } from './types.js';\n\nexport interface DiscoverRowProps {\n entry: DiscoverEntry;\n /** Render a leading icon for the entry. Return `null` to show nothing. */\n renderIcon?: (entry: DiscoverEntry) => React.ReactNode;\n /** Called when the user taps the row. */\n onOpen: (entry: DiscoverEntry) => void;\n}\n\nexport function DiscoverRow({ entry, renderIcon, onOpen }: DiscoverRowProps) {\n const theme = useOctoSpacesTheme();\n\n const handlePress = useCallback(() => {\n onOpen(entry);\n }, [entry, onOpen]);\n\n const icon = renderIcon ? renderIcon(entry) : null;\n const displayEmoji = !icon && entry.emoji ? entry.emoji : null;\n\n return (\n <Pressable\n onPress={handlePress}\n style={({ pressed }) => ({\n flexDirection: 'row',\n alignItems: 'center',\n paddingVertical: (theme.spacing['3'] as number) ?? 12,\n paddingHorizontal: (theme.spacing['4'] as number) ?? 16,\n backgroundColor: pressed\n ? (theme.colors.surface ?? '#f5f5f5')\n : 'transparent',\n borderRadius: (theme.radii['sm'] as number) ?? 6,\n })}\n accessibilityRole=\"button\"\n accessibilityLabel={entry.title || 'Untitled'}\n >\n {/* Leading icon / emoji */}\n {(icon || displayEmoji) && (\n <View\n style={{\n width: 28,\n height: 28,\n marginRight: (theme.spacing['2'] as number) ?? 8,\n alignItems: 'center',\n justifyContent: 'center',\n flexShrink: 0,\n }}\n >\n {icon ?? (\n <Text style={{ fontSize: 18, lineHeight: 24 }}>{displayEmoji}</Text>\n )}\n </View>\n )}\n\n {/* Title + type subtitle */}\n <View style={{ flex: 1, minWidth: 0 }}>\n <Text\n numberOfLines={1}\n style={{\n fontSize: (theme.type['body']?.size ?? 15),\n lineHeight: (theme.type['body']?.lineHeight ?? 22),\n color: entry.title ? theme.colors.text : theme.colors.textTertiary,\n fontFamily: theme.fonts['body'] ?? undefined,\n }}\n >\n {entry.title || 'Untitled'}\n </Text>\n <Text\n numberOfLines={1}\n style={{\n fontSize: (theme.type['caption']?.size ?? 12),\n lineHeight: (theme.type['caption']?.lineHeight ?? 18),\n color: theme.colors.textSecondary,\n marginTop: 1,\n textTransform: 'capitalize',\n }}\n >\n {entry.type}\n </Text>\n </View>\n </Pressable>\n );\n}\n","/**\n * A themed FlatList wrapper that renders a list of {@link DiscoverEntry} rows.\n *\n * All app-specific behaviour is injected via props so this component has zero\n * imports from any specific OctoSpaces app.\n */\nimport React, { useCallback } from 'react';\nimport { FlatList, RefreshControl, Text, View } from 'react-native';\n\nimport { useOctoSpacesTheme } from '../theme/provider.js';\nimport { DiscoverRow } from './DiscoverRow.js';\nimport type { DiscoverEntry } from './types.js';\n\nexport interface DiscoverListProps {\n entries: DiscoverEntry[];\n /** Render a leading icon for each row — see {@link DiscoverRowProps.renderIcon}. */\n renderIcon?: (entry: DiscoverEntry) => React.ReactNode;\n /** Called when a row is tapped. */\n onOpen: (entry: DiscoverEntry) => void;\n /** Text shown when `entries` is empty (default: \"No public objects found\"). */\n emptyMessage?: string;\n /** Whether a pull-to-refresh is currently in progress. */\n refreshing?: boolean;\n /** Called when the user pulls to refresh. */\n onRefresh?: () => void;\n}\n\nexport function DiscoverList({\n entries,\n renderIcon,\n onOpen,\n emptyMessage = 'No public objects found',\n refreshing,\n onRefresh,\n}: DiscoverListProps) {\n const theme = useOctoSpacesTheme();\n\n const renderItem = useCallback(\n ({ item }: { item: DiscoverEntry }) => (\n <DiscoverRow entry={item} renderIcon={renderIcon} onOpen={onOpen} />\n ),\n [renderIcon, onOpen],\n );\n\n const keyExtractor = useCallback(\n (item: DiscoverEntry) => `${item.spaceId}:${item.id}`,\n [],\n );\n\n if (entries.length === 0) {\n return (\n <View\n style={{\n flex: 1,\n alignItems: 'center',\n justifyContent: 'center',\n paddingHorizontal: (theme.spacing['6'] as number) ?? 24,\n }}\n >\n <Text\n style={{\n fontSize: theme.type['body']?.size ?? 15,\n color: theme.colors.textSecondary,\n textAlign: 'center',\n }}\n >\n {emptyMessage}\n </Text>\n </View>\n );\n }\n\n return (\n <FlatList\n data={entries}\n renderItem={renderItem}\n keyExtractor={keyExtractor}\n contentContainerStyle={{ paddingVertical: (theme.spacing['1'] as number) ?? 4 }}\n showsVerticalScrollIndicator={false}\n removeClippedSubviews\n refreshControl={\n onRefresh ? (\n <RefreshControl\n refreshing={refreshing ?? false}\n onRefresh={onRefresh}\n tintColor={theme.colors.primary}\n />\n ) : undefined\n }\n />\n );\n}\n","/**\n * Generic public-object discovery screen.\n *\n * Loads the world-readable public-object directory via `loadEntries`, renders a\n * search bar, and delegates row rendering + tap behaviour to the injected props.\n * No app-specific logic lives here — all customisation is via props:\n *\n * ```tsx\n * <DiscoverScreen\n * loadEntries={readObjectDirectory}\n * renderIcon={(e) => <TypeIcon entry={e} />}\n * onOpen={(e) => router.push({ pathname: routeForNode(e), params: { id: e.id, spaceId: e.spaceId } })}\n * />\n * ```\n *\n * State machine:\n * idle → loading → (ready | error)\n * Any pull of `loadEntries` updates the entries; errors show a retry button.\n */\nimport React, { useCallback, useEffect, useImperativeHandle, useRef, useState } from 'react';\nimport { ActivityIndicator, Pressable, Text, TextInput, View, type TextStyle } from 'react-native';\n\nimport { useOctoSpacesTheme } from '../theme/provider.js';\nimport { DiscoverList } from './DiscoverList.js';\nimport { filterDiscoverEntries, sortDiscoverEntries } from './filter.js';\nimport type { DiscoverEntry } from './types.js';\n\nexport interface DiscoverScreenProps {\n /**\n * Async function that resolves to the current public-object directory.\n * Typically `readObjectDirectory` from `@drakkar.software/octospaces-sdk`.\n * Called on mount and when `refresh()` is triggered.\n */\n loadEntries: () => Promise<DiscoverEntry[]>;\n /** Render a leading icon for each row. */\n renderIcon?: (entry: DiscoverEntry) => React.ReactNode;\n /** Called when the user taps a row — navigate to the object. */\n onOpen: (entry: DiscoverEntry) => void;\n /**\n * Optional heading text shown above the search bar.\n * @default \"Discover\"\n */\n title?: string;\n /**\n * Text shown when the directory is empty after loading.\n * @default \"No public objects yet\"\n */\n emptyMessage?: string;\n /**\n * Text shown when the directory is empty due to an active search query.\n * @default \"No results for «query»\"\n */\n emptySearchMessage?: string;\n /**\n * Whether to show the inline search bar.\n * @default true\n */\n searchEnabled?: boolean;\n /**\n * Optional ref whose `.current` is set to a `reload()` function once mounted.\n * Lets a host (e.g. a tab screen) trigger a soft-refresh on focus without\n * blanking the existing list — identical to pull-to-refresh behaviour.\n *\n * ```tsx\n * const reloadRef = useRef<() => void>(null);\n * useFocusEffect(useCallback(() => { reloadRef.current?.(); }, []));\n * <DiscoverScreen reloadRef={reloadRef} ... />\n * ```\n */\n reloadRef?: React.RefObject<(() => void) | null>;\n}\n\ntype State =\n | { status: 'idle' }\n | { status: 'loading' }\n | { status: 'ready'; entries: DiscoverEntry[] }\n | { status: 'error'; message: string };\n\nexport function DiscoverScreen({\n loadEntries,\n renderIcon,\n onOpen,\n title = 'Discover',\n emptyMessage = 'No public objects yet',\n emptySearchMessage,\n searchEnabled = true,\n reloadRef,\n}: DiscoverScreenProps) {\n const theme = useOctoSpacesTheme();\n const [state, setState] = useState<State>({ status: 'idle' });\n const [query, setQuery] = useState('');\n const [refreshing, setRefreshing] = useState(false);\n const cancelledRef = useRef(false);\n\n const load = useCallback(async () => {\n setState({ status: 'loading' });\n try {\n const raw = await loadEntries();\n if (cancelledRef.current) return;\n setState({ status: 'ready', entries: sortDiscoverEntries(raw) });\n } catch (err) {\n if (cancelledRef.current) return;\n setState({\n status: 'error',\n message: err instanceof Error ? err.message : 'Failed to load directory',\n });\n }\n }, [loadEntries]);\n\n /** Pull-to-refresh: re-fetches without blanking the existing list. */\n const handleRefresh = useCallback(async () => {\n setRefreshing(true);\n try {\n const raw = await loadEntries();\n if (cancelledRef.current) return;\n setState({ status: 'ready', entries: sortDiscoverEntries(raw) });\n } catch {\n // keep the existing list on refresh failure; the retry button remains for error state\n } finally {\n if (!cancelledRef.current) setRefreshing(false);\n }\n }, [loadEntries]);\n\n // Expose handleRefresh via reloadRef so a host can trigger a soft reload on focus.\n useImperativeHandle(reloadRef, () => handleRefresh, [handleRefresh]);\n\n useEffect(() => {\n cancelledRef.current = false;\n void load();\n return () => {\n cancelledRef.current = true;\n };\n }, [load]);\n\n // ── Derived list ─────────────────────────────────────────────────────────\n const allEntries = state.status === 'ready' ? state.entries : [];\n const visibleEntries = filterDiscoverEntries(allEntries, query);\n const noSearchResults = !!query.trim() && visibleEntries.length === 0 && allEntries.length > 0;\n const resolvedEmptyMessage = noSearchResults\n ? (emptySearchMessage ?? `No results for \"${query.trim()}\"`)\n : emptyMessage;\n\n // ── Palette shortcuts ─────────────────────────────────────────────────────\n const sp2 = (theme.spacing['2'] as number) ?? 8;\n const sp3 = (theme.spacing['3'] as number) ?? 12;\n const sp4 = (theme.spacing['4'] as number) ?? 16;\n const radMd = (theme.radii['md'] as number) ?? 8;\n\n return (\n <View style={{ flex: 1, backgroundColor: theme.colors.background }}>\n {/* Header */}\n <View\n style={{\n paddingHorizontal: sp4,\n paddingTop: sp4,\n paddingBottom: sp2,\n }}\n >\n <Text\n style={{\n fontSize: theme.type['title2']?.size ?? 22,\n fontWeight: (theme.type['title2']?.weight as TextStyle['fontWeight']) ?? '700',\n lineHeight: theme.type['title2']?.lineHeight ?? 28,\n color: theme.colors.text,\n fontFamily: theme.fonts['heading'] ?? undefined,\n marginBottom: sp3,\n }}\n >\n {title}\n </Text>\n\n {/* Search bar */}\n {searchEnabled && (\n <View\n style={{\n flexDirection: 'row',\n alignItems: 'center',\n backgroundColor: theme.colors.surfaceInput ?? theme.colors.surface,\n borderRadius: radMd,\n borderWidth: 1,\n borderColor: theme.colors.borderSubtle,\n paddingHorizontal: sp3,\n height: 40,\n }}\n >\n <TextInput\n placeholder=\"Search…\"\n placeholderTextColor={theme.colors.textTertiary}\n value={query}\n onChangeText={setQuery}\n style={{\n flex: 1,\n fontSize: theme.type['body']?.size ?? 15,\n color: theme.colors.text,\n fontFamily: theme.fonts['body'] ?? undefined,\n }}\n returnKeyType=\"search\"\n clearButtonMode=\"while-editing\"\n accessibilityLabel=\"Search discover\"\n />\n </View>\n )}\n </View>\n\n {/* Body */}\n {state.status === 'loading' ? (\n <View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>\n <ActivityIndicator color={theme.colors.primary} />\n </View>\n ) : state.status === 'error' ? (\n <View\n style={{\n flex: 1,\n alignItems: 'center',\n justifyContent: 'center',\n paddingHorizontal: sp4,\n }}\n >\n <Text\n style={{\n color: theme.colors.textSecondary,\n fontSize: theme.type['body']?.size ?? 15,\n textAlign: 'center',\n marginBottom: sp3,\n }}\n >\n {state.message}\n </Text>\n <Pressable\n onPress={load}\n style={{\n paddingHorizontal: sp4,\n paddingVertical: sp2,\n backgroundColor: theme.colors.primary,\n borderRadius: radMd,\n }}\n >\n <Text style={{ color: theme.colors.textOnPrimary, fontWeight: '600' }}>\n Retry\n </Text>\n </Pressable>\n </View>\n ) : (\n <DiscoverList\n entries={visibleEntries}\n renderIcon={renderIcon}\n onOpen={onOpen}\n emptyMessage={resolvedEmptyMessage}\n refreshing={refreshing}\n onRefresh={handleRefresh}\n />\n )}\n </View>\n );\n}\n","/**\n * Headless, abstractly-themed vertical spaces rail.\n *\n * The component reads the injected {@link Theme} via `useOctoSpacesTheme()` and\n * delegates all app-specific concerns to props:\n *\n * - Icons are rendered by `renderIcon` (keeps `@expo/vector-icons` out of this package).\n * - Space tile images are rendered by `renderTileImage` (keeps `expo-image` out too).\n * - Unread badges are rendered by `renderBadge`.\n * - The rail foot (account avatar + menu) is rendered by `renderFoot`.\n * - Web drag-reorder is wired via the `useTileDnd` hook prop (see below).\n *\n * All React Native primitives used here ship with the `react-native` peer dep.\n */\nimport React, { useState } from 'react';\nimport {\n Pressable as RNPressable,\n ScrollView,\n Text,\n View,\n} from 'react-native';\nimport type { PressableProps, View as RNView } from 'react-native';\n\nimport { useOctoSpacesTheme } from '../theme/provider.js';\nimport { railTileState } from './tile-state.js';\nimport type { RailTileTokens } from './tile-state.js';\nimport type { RailIconName, RailSpace } from './types.js';\n\n// ── Pressable with web hover events ───────────────────────────────────────────\n\n// React Native Web supports onMouseEnter/onMouseLeave for hover detection.\n// The peer dep is >=0.75 which includes these events in the ViewProps contract.\n// Cast to ForwardRefExoticComponent so `ref` is a valid JSX prop (Pressable\n// already uses forwardRef internally; this just makes TypeScript aware of it).\ntype HoverProps = { onMouseEnter?: () => void; onMouseLeave?: () => void };\nconst Pressable = RNPressable as React.ForwardRefExoticComponent<\n PressableProps & HoverProps & React.RefAttributes<RNView>\n>;\n\n// ── Props ─────────────────────────────────────────────────────────────────────\n\nexport interface SpacesRailProps {\n /** The spaces to show in the scrollable column. */\n spaces: RailSpace[];\n /** The currently-active space id (or null / undefined for none). */\n activeId?: string | null;\n /** Called when the user selects a space tile. */\n onSelect?: (id: string) => void;\n /** Called when the user taps the \"add\" tile. */\n onAdd?: () => void;\n /** When provided, renders a leading DM-home tile. */\n onSelectDms?: () => void;\n /** Whether the DM-home tile is the active selection. */\n dmsActive?: boolean;\n /** Unread count for the DM-home tile badge. */\n dmUnread?: number;\n /** Accessibility label for the DM-home tile (default: \"Direct messages\"). */\n dmLabel?: string;\n /** Accessibility label for the add-space tile (default: \"Create or join a space\"). */\n addLabel?: string;\n /**\n * Render a named icon at the given size and color. Used for the DM tile icon\n * (`'dm'`), the E2EE lock corner (`'lock'`), the mute corner (`'mute'`),\n * and the add tile icon (`'add'`). Return `null` to suppress the icon slot.\n * If omitted, all icon slots render nothing.\n */\n renderIcon?: (name: RailIconName, size: number, color: string) => React.ReactNode;\n /**\n * Render an image filling the tile background. Only called when `space.image`\n * is set. The component must fill its parent (`StyleSheet.absoluteFill` or\n * equivalent). If omitted, the short-name monogram is shown instead.\n */\n renderTileImage?: (space: RailSpace) => React.ReactNode;\n /**\n * Render an unread badge. Only called when `space.unread > 0`.\n * If omitted, badges are not shown.\n */\n renderBadge?: (count: number) => React.ReactNode;\n /**\n * When `true`, each tile shows a small E2EE-lock corner badge (bottom-right).\n * Requires `renderIcon` to be provided (otherwise the corner renders nothing).\n * Default: `false`.\n */\n showLockCorner?: boolean;\n /**\n * Render the pinned rail foot (e.g. the account avatar and popover).\n * The host app owns this entirely — identity state stays out of the package.\n */\n renderFoot?: () => React.ReactNode;\n /**\n * **Hook injection for web drag-reorder.** When provided, each space tile is\n * wrapped in a `DndTile` that calls `useTileDnd(spaceId)` unconditionally at\n * the top of its render — treat this prop as a React hook and keep it stable\n * for the lifetime of a `SpacesRail` mount (always provided or always absent).\n * Omit on native / in apps that don't need DnD.\n */\n useTileDnd?: (spaceId: string) => { ref?: React.Ref<RNView>; over?: boolean };\n}\n\n// ── Token resolver ─────────────────────────────────────────────────────────────\n\nfunction resolveRailTokens(theme: ReturnType<typeof useOctoSpacesTheme>): RailTileTokens {\n const { colors, swatches } = theme;\n return {\n primary: colors.primary,\n primaryMuted: colors.primaryMuted,\n primarySubtle: colors.primarySubtle,\n surfaceInput: colors.surfaceInput,\n borderSubtle: colors.borderSubtle,\n textOnPrimary: colors.textOnPrimary,\n textSecondary: colors.textSecondary,\n textTertiary: colors.textTertiary,\n railTile: swatches['railTile'] ?? colors.surfaceInput,\n railTileHoverBorder: swatches['railTileHoverBorder'] ?? colors.primarySubtle,\n railGlow: swatches['railGlow'] ?? colors.primary,\n railTileHoverInk: swatches['railTileHoverInk'] ?? colors.primary,\n };\n}\n\n// ── Shared tile dimensions (constant) ────────────────────────────────────────\n\nconst TILE_SIZE = 40;\nconst CORNER_SIZE = 16;\nconst BADGE_OFFSET = -5;\nconst CORNER_OFFSET = -3;\n\n// ── Tile content (non-hook render helper) ─────────────────────────────────────\n\ninterface TileContentProps {\n space: RailSpace;\n labelColor: string;\n fontFamily?: string;\n fontSize: number;\n lineHeight: number;\n cornerBg: string;\n cornerBorder: string;\n cornerIconColor: string;\n renderIcon?: SpacesRailProps['renderIcon'];\n renderTileImage?: SpacesRailProps['renderTileImage'];\n renderBadge?: SpacesRailProps['renderBadge'];\n showLockCorner?: boolean;\n}\n\nfunction TileContent({\n space,\n labelColor,\n fontFamily,\n fontSize,\n lineHeight,\n cornerBg,\n cornerBorder,\n cornerIconColor,\n renderIcon,\n renderTileImage,\n renderBadge,\n showLockCorner,\n}: TileContentProps) {\n return (\n <>\n {/* Image or monogram */}\n {space.image && renderTileImage ? (\n renderTileImage(space)\n ) : (\n <Text\n style={{\n fontSize,\n lineHeight,\n fontWeight: '700',\n fontFamily: fontFamily ?? undefined,\n color: labelColor,\n }}\n numberOfLines={1}\n >\n {space.short}\n </Text>\n )}\n {/* E2EE lock corner (bottom-right) */}\n {showLockCorner && renderIcon ? (\n <View\n style={{\n position: 'absolute',\n bottom: CORNER_OFFSET,\n right: CORNER_OFFSET,\n width: CORNER_SIZE,\n height: CORNER_SIZE,\n borderRadius: CORNER_SIZE / 2,\n borderWidth: 1,\n alignItems: 'center',\n justifyContent: 'center',\n backgroundColor: cornerBg,\n borderColor: cornerBorder,\n }}\n >\n {renderIcon('lock', 9, cornerIconColor)}\n </View>\n ) : null}\n {/* Mute corner (bottom-left) */}\n {space.muted && renderIcon ? (\n <View\n style={{\n position: 'absolute',\n bottom: CORNER_OFFSET,\n left: CORNER_OFFSET,\n width: CORNER_SIZE,\n height: CORNER_SIZE,\n borderRadius: CORNER_SIZE / 2,\n borderWidth: 1,\n alignItems: 'center',\n justifyContent: 'center',\n backgroundColor: cornerBg,\n borderColor: cornerBorder,\n }}\n >\n {renderIcon('mute', 9, cornerIconColor)}\n </View>\n ) : null}\n {/* Unread badge (top-right) */}\n {space.unread ? (\n <View\n style={{\n position: 'absolute',\n top: BADGE_OFFSET,\n right: BADGE_OFFSET,\n }}\n >\n {renderBadge ? renderBadge(space.unread) : null}\n </View>\n ) : null}\n </>\n );\n}\n\n// ── PlainTile — space tile without DnD ────────────────────────────────────────\n\ninterface TileSharedProps {\n space: RailSpace;\n active: boolean;\n onPress?: () => void;\n tokens: RailTileTokens;\n radiusActive: number;\n radiusDefault: number;\n renderIcon?: SpacesRailProps['renderIcon'];\n renderTileImage?: SpacesRailProps['renderTileImage'];\n renderBadge?: SpacesRailProps['renderBadge'];\n showLockCorner?: boolean;\n cornerBg: string;\n cornerBorder: string;\n fontFamily?: string;\n fontSize: number;\n lineHeight: number;\n}\n\nfunction PlainTile({\n space,\n active,\n onPress,\n tokens,\n radiusActive,\n radiusDefault,\n renderIcon,\n renderTileImage,\n renderBadge,\n showLockCorner,\n cornerBg,\n cornerBorder,\n fontFamily,\n fontSize,\n lineHeight,\n}: TileSharedProps) {\n const [hovered, setHovered] = useState(false);\n const s = railTileState({ active, hovered, over: false }, tokens, radiusActive, radiusDefault);\n\n return (\n <Pressable\n onPress={onPress}\n onMouseEnter={() => setHovered(true)}\n onMouseLeave={() => setHovered(false)}\n accessibilityRole=\"button\"\n accessibilityLabel={space.short}\n style={{\n position: 'relative',\n width: TILE_SIZE,\n height: TILE_SIZE,\n alignItems: 'center',\n justifyContent: 'center',\n overflow: 'hidden',\n borderRadius: s.radius,\n backgroundColor: s.bg,\n borderWidth: s.borderWidth,\n borderColor: s.borderColor,\n ...(s.shadow ?? {}),\n }}\n >\n <TileContent\n space={space}\n labelColor={s.labelColor}\n fontFamily={fontFamily}\n fontSize={fontSize}\n lineHeight={lineHeight}\n cornerBg={cornerBg}\n cornerBorder={cornerBorder}\n cornerIconColor={tokens.textTertiary}\n renderIcon={renderIcon}\n renderTileImage={renderTileImage}\n renderBadge={renderBadge}\n showLockCorner={showLockCorner}\n />\n </Pressable>\n );\n}\n\n// ── DndTile — space tile with hook-injected DnD ref + over state ──────────────\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\n\ninterface DndTileProps extends TileSharedProps {\n /** Hook injection: called unconditionally at the top of this component.\n * Treat it as a React hook — always provided for every DndTile. */\n dnd: NonNullable<SpacesRailProps['useTileDnd']>;\n}\n\nfunction DndTile({\n space,\n active,\n onPress,\n tokens,\n radiusActive,\n radiusDefault,\n renderIcon,\n renderTileImage,\n renderBadge,\n showLockCorner,\n cornerBg,\n cornerBorder,\n fontFamily,\n fontSize,\n lineHeight,\n dnd,\n}: DndTileProps) {\n const [hovered, setHovered] = useState(false);\n // Hook injection: useTileDnd is called unconditionally here (it IS a hook).\n // eslint-disable-next-line react-hooks/rules-of-hooks\n const { ref, over = false } = dnd(space.id);\n const s = railTileState({ active, hovered, over }, tokens, radiusActive, radiusDefault);\n\n return (\n <Pressable\n ref={ref as React.Ref<RNView>}\n onPress={onPress}\n onMouseEnter={() => setHovered(true)}\n onMouseLeave={() => setHovered(false)}\n accessibilityRole=\"button\"\n accessibilityLabel={space.short}\n style={{\n position: 'relative',\n width: TILE_SIZE,\n height: TILE_SIZE,\n alignItems: 'center',\n justifyContent: 'center',\n overflow: 'hidden',\n borderRadius: s.radius,\n backgroundColor: s.bg,\n borderWidth: s.borderWidth,\n borderColor: s.borderColor,\n ...(s.shadow ?? {}),\n }}\n >\n <TileContent\n space={space}\n labelColor={s.labelColor}\n fontFamily={fontFamily}\n fontSize={fontSize}\n lineHeight={lineHeight}\n cornerBg={cornerBg}\n cornerBorder={cornerBorder}\n cornerIconColor={tokens.textTertiary}\n renderIcon={renderIcon}\n renderTileImage={renderTileImage}\n renderBadge={renderBadge}\n showLockCorner={showLockCorner}\n />\n </Pressable>\n );\n}\n\n// ── SpacesRail ─────────────────────────────────────────────────────────────────\n\n/**\n * Vertical spaces rail — a 64px-wide column of square space tiles, a DM-home tile,\n * an add-space tile, and a pinned foot for the account widget.\n *\n * Styled entirely from the injected {@link Theme} via `useOctoSpacesTheme()`.\n * All icons, images, badges, and the account foot are provided by the host app.\n */\nexport function SpacesRail({\n spaces,\n activeId,\n onSelect,\n onAdd,\n onSelectDms,\n dmsActive = false,\n dmUnread,\n dmLabel = 'Direct messages',\n addLabel = 'Create or join a space',\n renderIcon,\n renderTileImage,\n renderBadge,\n showLockCorner = false,\n renderFoot,\n useTileDnd,\n}: SpacesRailProps) {\n const theme = useOctoSpacesTheme();\n const { colors, spacing, radii, type: typeScale, fonts, layout } = theme;\n\n const tokens = resolveRailTokens(theme);\n\n // Layout constants with fallbacks for hosts that haven't set them.\n const railWidth = (layout['railWidth'] as number | undefined) ?? 64;\n const spaceV = (spacing['2'] as number | undefined) ?? 8;\n const spaceXs = (spacing['1'] as number | undefined) ?? 4;\n const spaceS = (spacing['2'] as number | undefined) ?? 8;\n const spaceMd = (spacing['3'] as number | undefined) ?? 12;\n\n const radiusActive = (radii['lg'] as number | undefined) ?? 12;\n const radiusDefault = (radii['xl'] as number | undefined) ?? 16;\n\n const footnoteSize = typeScale['footnote']?.size ?? 12;\n const footnoteLineH = typeScale['footnote']?.lineHeight ?? 18;\n const monoFont = fonts['mono'] ?? undefined;\n\n // Corner-badge tokens (background = rail surface, border = rail border).\n const cornerBg = colors.sidebar;\n const cornerBorder = colors.border;\n\n // Shared tile props (passed to every tile variant).\n const tileShared = {\n tokens,\n radiusActive,\n radiusDefault,\n renderIcon,\n renderTileImage,\n renderBadge,\n showLockCorner,\n cornerBg,\n cornerBorder,\n fontFamily: monoFont,\n fontSize: footnoteSize,\n lineHeight: footnoteLineH,\n };\n\n // DM tile hover state (managed here since DM tile is inline, not a separate component).\n const [dmHovered, setDmHovered] = useState(false);\n\n const dmTileStyle = railTileState(\n { active: dmsActive, hovered: dmHovered, over: false },\n tokens,\n radiusActive,\n radiusDefault,\n );\n\n const dmIconColor = dmsActive ? colors.textOnPrimary : dmHovered ? tokens.railTileHoverInk : colors.textSecondary;\n\n // Add-tile hover state.\n const [addHovered, setAddHovered] = useState(false);\n\n // Whether DnD is active (determines which tile variant to render).\n const hasDnd = !!useTileDnd;\n\n return (\n <View\n style={{\n width: railWidth,\n paddingVertical: spaceMd,\n borderRightWidth: 1,\n borderRightColor: colors.border,\n backgroundColor: colors.sidebar,\n alignItems: 'center',\n gap: spaceS,\n }}\n >\n {/* Scrollable tile column */}\n <ScrollView\n style={{ alignSelf: 'stretch', flex: 1 }}\n contentContainerStyle={{\n alignItems: 'center',\n gap: spaceV,\n paddingVertical: spaceXs,\n }}\n showsVerticalScrollIndicator={false}\n >\n {/* DM-home tile (pinned first when provided) */}\n {onSelectDms ? (\n <View style={{ position: 'relative' }}>\n <Pressable\n onPress={onSelectDms}\n onMouseEnter={() => setDmHovered(true)}\n onMouseLeave={() => setDmHovered(false)}\n accessibilityRole=\"button\"\n accessibilityLabel={dmLabel}\n style={{\n width: TILE_SIZE,\n height: TILE_SIZE,\n alignItems: 'center',\n justifyContent: 'center',\n borderRadius: dmTileStyle.radius,\n backgroundColor: dmTileStyle.bg,\n borderWidth: dmTileStyle.borderWidth,\n borderColor: dmTileStyle.borderColor,\n ...(dmTileStyle.shadow ?? {}),\n }}\n >\n {renderIcon ? renderIcon('dm', 20, dmIconColor) : null}\n </Pressable>\n {/* Lock corner */}\n {showLockCorner && renderIcon ? (\n <View\n style={{\n position: 'absolute',\n bottom: CORNER_OFFSET,\n right: CORNER_OFFSET,\n width: CORNER_SIZE,\n height: CORNER_SIZE,\n borderRadius: CORNER_SIZE / 2,\n borderWidth: 1,\n alignItems: 'center',\n justifyContent: 'center',\n backgroundColor: cornerBg,\n borderColor: cornerBorder,\n }}\n >\n {renderIcon('lock', 9, tokens.textTertiary)}\n </View>\n ) : null}\n {/* DM unread badge */}\n {dmUnread ? (\n <View style={{ position: 'absolute', top: BADGE_OFFSET, right: BADGE_OFFSET }}>\n {renderBadge ? renderBadge(dmUnread) : null}\n </View>\n ) : null}\n </View>\n ) : null}\n\n {/* Space tiles */}\n {spaces.map((s) =>\n hasDnd ? (\n <DndTile\n key={s.id}\n space={s}\n active={s.id === activeId}\n onPress={() => onSelect?.(s.id)}\n dnd={useTileDnd!}\n {...tileShared}\n />\n ) : (\n <PlainTile\n key={s.id}\n space={s}\n active={s.id === activeId}\n onPress={() => onSelect?.(s.id)}\n {...tileShared}\n />\n ),\n )}\n\n {/* Add-space tile */}\n <Pressable\n onPress={onAdd}\n onMouseEnter={() => setAddHovered(true)}\n onMouseLeave={() => setAddHovered(false)}\n accessibilityRole=\"button\"\n accessibilityLabel={addLabel}\n style={{\n width: TILE_SIZE,\n height: TILE_SIZE,\n alignItems: 'center',\n justifyContent: 'center',\n borderRadius: radiusDefault,\n borderWidth: 1,\n borderStyle: 'dashed',\n borderColor: addHovered ? colors.border : colors.borderSubtle,\n }}\n >\n {renderIcon ? renderIcon('add', 16, addHovered ? tokens.railTileHoverInk : colors.textTertiary) : null}\n </Pressable>\n </ScrollView>\n\n {/* Pinned foot — account avatar, popover, etc. */}\n {renderFoot ? renderFoot() : null}\n </View>\n );\n}\n","/**\n * Pure helper: maps tile interaction state + resolved theme tokens → visual style.\n * No React or React Native imports — fully testable in isolation.\n */\nimport type { ShadowToken } from '../theme/types.js';\nimport { glowShadow } from '../theme/helpers.js';\n\n// ── Token contract ─────────────────────────────────────────────────────────────\n\n/**\n * Resolved color tokens consumed by {@link railTileState}. Built from a `Theme`\n * object in the component layer (see `resolveRailTokens` in `SpacesRail.tsx`).\n * Swatch entries fall back to palette tokens when the host app hasn't set them.\n */\nexport interface RailTileTokens {\n // Core palette tokens\n primary: string;\n primaryMuted: string;\n primarySubtle: string;\n surfaceInput: string;\n borderSubtle: string;\n textOnPrimary: string;\n textSecondary: string;\n textTertiary: string;\n // Optional swatch overrides (host app can tune rail-specific colors)\n railTile: string; // swatch 'railTile' ?? surfaceInput\n railTileHoverBorder: string; // swatch 'railTileHoverBorder' ?? primarySubtle\n railGlow: string; // swatch 'railGlow' ?? primary\n railTileHoverInk: string; // swatch 'railTileHoverInk' ?? primary\n}\n\n// ── Output ─────────────────────────────────────────────────────────────────────\n\n/** Resolved visual properties for a single rail tile. */\nexport interface RailTileStyle {\n /** Tile background color. */\n bg: string;\n /** Tile border color. */\n borderColor: string;\n /** Tile border width (0 when active, 1 otherwise). */\n borderWidth: number;\n /** Tile border-radius in pixels (squared-off when active/hovered, rounded otherwise). */\n radius: number;\n /** Monogram label color. */\n labelColor: string;\n /** Active glow shadow, or `null` when not active. */\n shadow: ShadowToken | null;\n}\n\n// ── Mapping ────────────────────────────────────────────────────────────────────\n\n/**\n * Map tile interaction state to visual style tokens.\n *\n * @param state Current interaction state.\n * @param tokens Resolved color tokens (see {@link RailTileTokens}).\n * @param radiusActive Border-radius when the tile is active or hovered (squarer look).\n * @param radiusDefault Border-radius for the resting state (rounder look).\n */\nexport function railTileState(\n state: { active: boolean; hovered: boolean; over: boolean },\n tokens: RailTileTokens,\n radiusActive: number,\n radiusDefault: number,\n): RailTileStyle {\n const { active, hovered, over } = state;\n\n let bg: string;\n let borderColor: string;\n let borderWidth: number;\n let labelColor: string;\n\n if (active) {\n bg = tokens.primary;\n borderColor = 'transparent';\n borderWidth = 0;\n labelColor = tokens.textOnPrimary;\n } else if (hovered) {\n bg = tokens.primaryMuted;\n borderColor = tokens.railTileHoverBorder;\n borderWidth = 1;\n labelColor = tokens.railTileHoverInk;\n } else {\n bg = tokens.railTile;\n borderColor = tokens.borderSubtle;\n borderWidth = 1;\n labelColor = tokens.textSecondary;\n }\n\n // Drop-target overlay: accent border when a dragged tile is over this one.\n if (over && !active) {\n borderColor = tokens.primary;\n borderWidth = 1;\n }\n\n const radius = active || hovered || over ? radiusActive : radiusDefault;\n const shadow: ShadowToken | null = active ? glowShadow(tokens.railGlow, 8, 0.3) : null;\n\n return { bg, borderColor, borderWidth, radius, labelColor, shadow };\n}\n","/**\n * Headless themed sidebar panel shell.\n *\n * Renders the 240–248px wide panel: a background surface (colors.sidebarPanel),\n * a right border (colors.border), an optional header slot, the item list\n * (scrollable by default), and an optional footer slot.\n *\n * All content is delegated to the host via slots — only the chrome (bg, border,\n * width) and the ScrollView wrapper are shared.\n *\n * ```tsx\n * <Sidebar\n * header={\n * <SidebarHeader\n * leading={<SpaceSwitcher variant=\"sidebar\" />}\n * actions={<>\n * <IconButton name=\"search\" ... />\n * <IconButton name=\"plus\" ... />\n * </>}\n * />\n * }\n * contentContainerStyle={{ paddingHorizontal: 8 }}\n * >\n * <WorkObjects ... />\n * </Sidebar>\n * ```\n */\nimport React from 'react';\nimport { ScrollView, View } from 'react-native';\nimport type { StyleProp, ViewStyle } from 'react-native';\n\nimport { useOctoSpacesTheme } from '../theme/provider.js';\n\nexport interface SidebarProps {\n /** Header slot — render a {@link SidebarHeader} or custom content above the list. */\n header?: React.ReactNode;\n /** Footer slot — pinned below the scroll area. */\n footer?: React.ReactNode;\n /** The item list. Wrapped in a ScrollView unless `scrollable` is `false`. */\n children: React.ReactNode;\n /** Panel width in pixels. Defaults to `theme.layout.sidebarWidth ?? 248`. */\n width?: number;\n /**\n * When `false`, children are rendered in a plain `View` instead of a `ScrollView`.\n * Use when the host manages its own scroll (e.g. multiple independent lists).\n * @default true\n */\n scrollable?: boolean;\n /**\n * Passed to the `ScrollView`'s `contentContainerStyle` (or to the body `View`\n * style when `scrollable` is `false`).\n */\n contentContainerStyle?: StyleProp<ViewStyle>;\n /**\n * Override the panel background color.\n * Defaults to `colors.sidebarPanel` from the injected theme.\n */\n background?: string;\n}\n\nexport function Sidebar({\n header,\n footer,\n children,\n width,\n scrollable = true,\n contentContainerStyle,\n background,\n}: SidebarProps) {\n const theme = useOctoSpacesTheme();\n const { colors, layout } = theme;\n\n const panelWidth = width ?? (layout['sidebarWidth'] as number | undefined) ?? 248;\n const bg = background ?? colors.sidebarPanel;\n\n return (\n <View\n style={{\n width: panelWidth,\n backgroundColor: bg,\n borderRightWidth: 1,\n borderRightColor: colors.border,\n flexDirection: 'column',\n }}\n >\n {header ?? null}\n {scrollable ? (\n <ScrollView\n style={{ flex: 1 }}\n contentContainerStyle={contentContainerStyle ?? undefined}\n showsVerticalScrollIndicator={false}\n >\n {children}\n </ScrollView>\n ) : (\n <View style={[{ flex: 1 }, contentContainerStyle]}>{children}</View>\n )}\n {footer ?? null}\n </View>\n );\n}\n","/**\n * Headless themed sidebar header strip.\n *\n * Row 1: a `leading` slot (flex:1, typically a space selector or name) + an\n * optional `actions` row (right-aligned command buttons). An optional `extra`\n * slot below row 1 accepts additional controls (OctoChat uses this for its\n * `ModeSwitcher` + jump-to search bar). An optional bottom hairline divider.\n *\n * ```tsx\n * // OctoVault — space switcher + command icons\n * <SidebarHeader\n * leading={<SpaceSwitcher variant=\"sidebar\" />}\n * actions={<>\n * <IconButton name=\"search\" onPress={openSearch} tooltip=\"Search\" shortcut=\"⌘K\" />\n * <IconButton name=\"plus\" onPress={newPage} tooltip=\"New page\" shortcut=\"⌘N\" />\n * <IconButton name=\"sidebar\" onPress={collapse} tooltip=\"Hide sidebar\" shortcut=\"⌘\\\\\" />\n * </>}\n * />\n *\n * // OctoChat — space menu + mode switcher + jump-to bar\n * <SidebarHeader\n * leading={<Pressable onPress={onOpenSpaceMenu}>…</Pressable>}\n * extra={<><ModeSwitcher /><JumpToBar /></>}\n * divider\n * />\n * ```\n */\nimport React from 'react';\nimport { StyleSheet, View } from 'react-native';\nimport type { StyleProp, ViewStyle } from 'react-native';\n\nimport { useOctoSpacesTheme } from '../theme/provider.js';\n\nexport interface SidebarHeaderProps {\n /** Leading content (flex:1) — typically the space selector / space name. */\n leading: React.ReactNode;\n /**\n * Right-aligned action buttons row.\n * OctoVault passes its own `IconButton` components here (with tooltips +\n * keyboard shortcuts). For simpler headless consumers use `SidebarActionButton`.\n */\n actions?: React.ReactNode;\n /**\n * Extra slot rendered below the leading+actions row.\n * Use for secondary controls like a mode switcher or a search bar.\n */\n extra?: React.ReactNode;\n /**\n * Render a hairline bottom divider in `colors.borderSubtle`.\n * @default false\n */\n divider?: boolean;\n /**\n * Style applied to the outer container. Use to set host-specific padding,\n * gap, background override, etc.\n */\n style?: StyleProp<ViewStyle>;\n}\n\nexport function SidebarHeader({\n leading,\n actions,\n extra,\n divider = false,\n style,\n}: SidebarHeaderProps) {\n const theme = useOctoSpacesTheme();\n const { colors } = theme;\n\n return (\n <View\n style={[\n styles.root,\n style,\n divider\n ? {\n borderBottomWidth: StyleSheet.hairlineWidth,\n borderBottomColor: colors.borderSubtle,\n }\n : undefined,\n ]}\n >\n {/* Row 1: leading + actions */}\n <View style={styles.row}>\n <View style={styles.leading}>{leading}</View>\n {actions != null ? <View style={styles.actions}>{actions}</View> : null}\n </View>\n {/* Extra slot (ModeSwitcher, jump-to bar, etc.) */}\n {extra != null ? <View>{extra}</View> : null}\n </View>\n );\n}\n\nconst styles = StyleSheet.create({\n root: {\n flexDirection: 'column',\n },\n row: {\n flexDirection: 'row',\n alignItems: 'center',\n },\n leading: {\n flex: 1,\n minWidth: 0,\n },\n actions: {\n flexDirection: 'row',\n alignItems: 'center',\n flexShrink: 0,\n },\n});\n","/**\n * Headless themed icon-button primitive for sidebar action slots.\n *\n * Renders a square `Pressable` with hover/press wash from the injected theme.\n * The icon is provided by the host app as a `ReactNode` — this keeps the package\n * free of `@expo/vector-icons`, `Tooltip`, and keyboard-shortcut concerns.\n *\n * Host apps with richer icon buttons (OctoVault's `IconButton` has tooltips,\n * keyboard-shortcut labels, and haptics) can slot those directly into\n * `SidebarHeader.actions` instead — this primitive is for simpler headless\n * consumers.\n *\n * ```tsx\n * <SidebarActionButton\n * icon={<Icon name=\"search\" size={15} color={theme.colors.textSecondary} />}\n * onPress={openSearch}\n * accessibilityLabel=\"Search\"\n * />\n * ```\n */\nimport React, { useState } from 'react';\nimport { Pressable as RNPressable } from 'react-native';\nimport type { PressableProps, View as RNView } from 'react-native';\n\nimport { useOctoSpacesTheme } from '../theme/provider.js';\n\n// React Native Web supports onMouseEnter/onMouseLeave.\ntype HoverProps = { onMouseEnter?: () => void; onMouseLeave?: () => void };\nconst Pressable = RNPressable as React.ForwardRefExoticComponent<\n PressableProps & HoverProps & React.RefAttributes<RNView>\n>;\n\nexport interface SidebarActionButtonProps {\n /** Icon element to render — the host provides the icon component, size, and color. */\n icon: React.ReactNode;\n onPress: () => void;\n accessibilityLabel: string;\n /**\n * Width and height of the pressable target in pixels.\n * @default 32\n */\n size?: number;\n}\n\nexport function SidebarActionButton({\n icon,\n onPress,\n accessibilityLabel,\n size = 32,\n}: SidebarActionButtonProps) {\n const theme = useOctoSpacesTheme();\n const { colors, radii } = theme;\n\n const [hovered, setHovered] = useState(false);\n const [pressed, setPressed] = useState(false);\n\n const bg = pressed\n ? (colors.primaryMuted ?? 'rgba(0,0,0,0.10)')\n : hovered\n ? (colors.primarySubtle ?? 'rgba(0,0,0,0.05)')\n : 'transparent';\n\n const radius = (radii['sm'] as number | undefined) ?? 4;\n\n return (\n <Pressable\n onPress={onPress}\n onPressIn={() => setPressed(true)}\n onPressOut={() => setPressed(false)}\n onMouseEnter={() => setHovered(true)}\n onMouseLeave={() => setHovered(false)}\n accessibilityRole=\"button\"\n accessibilityLabel={accessibilityLabel}\n style={{\n width: size,\n height: size,\n alignItems: 'center',\n justifyContent: 'center',\n borderRadius: radius,\n backgroundColor: bg,\n }}\n >\n {icon}\n </Pressable>\n );\n}\n","/**\n * Generic themed sidebar navigation row.\n *\n * The headless analog of `DiscoverRow` for the sidebar panel. Suitable for\n * simple link-style rows (explore, threads, pinned, …). Complex item lists\n * (OctoVault's `ObjectTree`, OctoChat's `RoomCategoryList`) use their own row\n * components — this primitive is for straightforward nav items.\n *\n * Active rows are highlighted with `colors.sidebarActive`. Hovered rows receive\n * a subtle `colors.primarySubtle` wash.\n *\n * ```tsx\n * <SidebarItem\n * label=\"Threads\"\n * icon={<Icon name=\"thread\" size={15} color={colors.textSecondary} />}\n * active={threadsActive}\n * onPress={onOpenThreads}\n * />\n * ```\n */\nimport React, { useState } from 'react';\nimport { Pressable as RNPressable, StyleSheet, Text, View } from 'react-native';\nimport type { PressableProps, TextStyle, View as RNView } from 'react-native';\n\nimport { useOctoSpacesTheme } from '../theme/provider.js';\n\ntype HoverProps = { onMouseEnter?: () => void; onMouseLeave?: () => void };\nconst Pressable = RNPressable as React.ForwardRefExoticComponent<\n PressableProps & HoverProps & React.RefAttributes<RNView>\n>;\n\nexport interface SidebarItemProps {\n label: string;\n /** Leading icon element — the host provides the icon component. */\n icon?: React.ReactNode;\n /** Highlight the row as the current destination. */\n active?: boolean;\n /** Badge shown at the trailing edge — a number or short string. */\n badge?: number | string;\n onPress: () => void;\n onLongPress?: () => void;\n /** Additional trailing element (e.g. an action button). */\n trailing?: React.ReactNode;\n /**\n * Left indentation level for nested items. Each level adds 16 px.\n * @default 0\n */\n indent?: number;\n}\n\nexport function SidebarItem({\n label,\n icon,\n active = false,\n badge,\n onPress,\n onLongPress,\n trailing,\n indent = 0,\n}: SidebarItemProps) {\n const theme = useOctoSpacesTheme();\n const { colors, type: typeScale, fonts, spacing, radii } = theme;\n\n const [hovered, setHovered] = useState(false);\n\n const sp1 = (spacing['1'] as number | undefined) ?? 4;\n const sp2 = (spacing['2'] as number | undefined) ?? 8;\n const sp3 = (spacing['3'] as number | undefined) ?? 12;\n const radSm = (radii['sm'] as number | undefined) ?? 4;\n const indentPx = indent * 16;\n\n const bg = active\n ? colors.sidebarActive\n : hovered\n ? (colors.primarySubtle ?? 'rgba(0,0,0,0.04)')\n : 'transparent';\n\n const textColor = active ? colors.primary : colors.text;\n\n return (\n <Pressable\n onPress={onPress}\n onLongPress={onLongPress}\n onMouseEnter={() => setHovered(true)}\n onMouseLeave={() => setHovered(false)}\n accessibilityRole=\"button\"\n accessibilityLabel={label}\n style={{\n flexDirection: 'row',\n alignItems: 'center',\n gap: sp2,\n paddingVertical: sp1 + 2,\n paddingLeft: sp3 + indentPx,\n paddingRight: sp3,\n borderRadius: radSm,\n backgroundColor: bg,\n }}\n >\n {icon != null ? <View style={styles.iconSlot}>{icon}</View> : null}\n <Text\n style={\n {\n flex: 1,\n fontSize: typeScale['callout']?.size ?? 13,\n lineHeight: typeScale['callout']?.lineHeight ?? 18,\n fontWeight: active ? '600' : '400',\n color: textColor,\n fontFamily: fonts['body'] ?? undefined,\n } as TextStyle\n }\n numberOfLines={1}\n >\n {label}\n </Text>\n {badge != null ? (\n <View\n style={{\n minWidth: 16,\n height: 16,\n borderRadius: 8,\n backgroundColor: active ? colors.primary : colors.textTertiary,\n alignItems: 'center',\n justifyContent: 'center',\n paddingHorizontal: sp1,\n }}\n >\n <Text\n style={\n {\n fontSize: typeScale['micro']?.size ?? 10,\n lineHeight: typeScale['micro']?.lineHeight ?? 14,\n fontWeight: '700',\n color: active ? colors.textOnPrimary : colors.textInverse,\n } as TextStyle\n }\n >\n {String(badge)}\n </Text>\n </View>\n ) : null}\n {trailing != null ? trailing : null}\n </Pressable>\n );\n}\n\nconst styles = StyleSheet.create({\n iconSlot: { width: 18, alignItems: 'center', justifyContent: 'center' },\n});\n","/**\n * Full-screen scrim overlay that centers its content. Tapping the backdrop, the\n * close button, the Escape key (web) or the hardware back (Android) dismisses it.\n *\n * All interactive chrome (close button, action buttons) is injected via render\n * props so this package remains free of @expo/vector-icons, expo-image, and\n * reanimated. The host app renders its own icon buttons and images.\n *\n * @example\n * ```tsx\n * import { Lightbox } from '@drakkar.software/octospaces-ui';\n *\n * <Lightbox\n * visible={zoomed}\n * onClose={() => setZoomed(false)}\n * renderCloseButton={(onClose) => (\n * <IconButton name=\"x\" color={colors.onScrim} onPress={onClose} />\n * )}\n * renderActions={() => (\n * <IconButton name=\"share\" color={colors.onScrim} onPress={handleShare} />\n * )}\n * >\n * <Image source={{ uri }} style={{ width: w * 0.92, height: h * 0.82 }} />\n * </Lightbox>\n * ```\n */\nimport React, { useEffect } from 'react';\nimport type { ReactNode } from 'react';\nimport { Modal, Platform, Pressable, View } from 'react-native';\n\nimport { useOctoSpacesTheme } from '../theme/provider.js';\n\nexport interface LightboxProps {\n visible: boolean;\n onClose: () => void;\n /** Centered content — the host renders the full-size image here. */\n children: ReactNode;\n /** Accessible label for the backdrop tap-to-close. Default: \"Close preview\". */\n closeLabel?: string;\n /**\n * Render the close affordance pinned to the top-right corner.\n * Receives `onClose` so the button can dismiss the overlay.\n * If omitted, tapping the backdrop or hardware back still closes it.\n */\n renderCloseButton?: (onClose: () => void) => ReactNode;\n /**\n * Render additional action(s) pinned to the bottom-right corner,\n * e.g. a save/share button. Return `null` to show nothing.\n */\n renderActions?: () => ReactNode;\n}\n\n/**\n * Full-screen scrim overlay that centers its children. Headless: all buttons\n * are injected via render props; the package has no icon or image dependencies.\n *\n * Dismissal: backdrop tap · `renderCloseButton` · hardware back (Android) ·\n * Escape key (web).\n */\nexport function Lightbox({\n visible,\n onClose,\n children,\n closeLabel = 'Close preview',\n renderCloseButton,\n renderActions,\n}: LightboxProps) {\n const theme = useOctoSpacesTheme();\n\n // Web has no hardware back button; close on Escape to match the native affordance.\n useEffect(() => {\n if (!visible || Platform.OS !== 'web') return;\n const onKey = (e: KeyboardEvent) => {\n if (e.key === 'Escape') onClose();\n };\n window.addEventListener('keydown', onKey);\n return () => window.removeEventListener('keydown', onKey);\n }, [visible, onClose]);\n\n // Spacing token lookups use numeric keys (OctoChat) with named-key fallbacks\n // (OctoVault) so the overlay is correctly inset in either host. The `?? n`\n // values match the underlying spacing scale (xl=24, xxl=32, lg=16).\n const pad = (theme.spacing['6'] as number) ?? 24;\n const insetV = (theme.spacing['8'] as number) ?? 32;\n const insetH = (theme.spacing['4'] as number) ?? 16;\n\n return (\n <Modal\n visible={visible}\n transparent\n animationType=\"fade\"\n onRequestClose={onClose}\n statusBarTranslucent\n >\n {/* Backdrop — full-screen scrim, tap anywhere to close */}\n <Pressable\n style={{\n flex: 1,\n alignItems: 'center',\n justifyContent: 'center',\n padding: pad,\n backgroundColor: theme.colors.overlay ?? 'rgba(0,0,0,0.85)',\n }}\n onPress={onClose}\n accessibilityLabel={closeLabel}\n >\n {/* Content — pointerEvents=\"box-none\" so the View itself doesn't intercept\n taps (they fall through to the backdrop), but its children still can. */}\n <View\n style={{ alignItems: 'center', justifyContent: 'center' }}\n pointerEvents=\"box-none\"\n >\n {children}\n </View>\n\n {/* Close slot — top-right corner */}\n {renderCloseButton ? (\n <View style={{ position: 'absolute', top: insetV, right: insetH }}>\n {renderCloseButton(onClose)}\n </View>\n ) : null}\n\n {/* Action slot — bottom-right corner */}\n {renderActions ? (\n <View style={{ position: 'absolute', bottom: insetV, right: insetH }}>\n {renderActions()}\n </View>\n ) : null}\n </Pressable>\n </Modal>\n );\n}\n"],"mappings":"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","names":["React","React","React","useCallback","Text","View","useCallback","React","View","Text","React","useCallback","Pressable","Text","View","useCallback","React","View","Text","Pressable","React","useState","Text","View","Pressable","React","Text","View","useState","React","ScrollView","View","React","View","ScrollView","React","View","React","View","React","useState","RNPressable","Pressable","RNPressable","useState","React","React","useState","RNPressable","StyleSheet","Text","View","Pressable","RNPressable","useState","React","View","styles","Text","StyleSheet","React","useEffect","Pressable","View","useEffect","React","Pressable","View"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@drakkar.software/octospaces-ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Shared headless-themed UI primitives for OctoSpaces apps. Theme values are injected by the host app; this package ships only types + plumbing + primitives.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|
package/src/index.ts
CHANGED
|
@@ -50,9 +50,24 @@ export { DiscoverList } from './discover/DiscoverList.js';
|
|
|
50
50
|
export type { DiscoverScreenProps } from './discover/DiscoverScreen.js';
|
|
51
51
|
export { DiscoverScreen } from './discover/DiscoverScreen.js';
|
|
52
52
|
|
|
53
|
-
// Sidebar surface — the vertical spaces rail (icon tiles + DM home + add + foot)
|
|
53
|
+
// Sidebar surface — the vertical spaces rail (icon tiles + DM home + add + foot)
|
|
54
|
+
// AND the sidebar panel shell + header strip (Sidebar / SidebarHeader / SidebarItem).
|
|
54
55
|
// Headless: icons, images, badges and the account foot are injected via props so
|
|
55
56
|
// the package stays free of expo-image / @expo/vector-icons / reanimated.
|
|
56
57
|
export type { RailSpace, RailIconName } from './sidebar/types.js';
|
|
57
58
|
export type { SpacesRailProps } from './sidebar/SpacesRail.js';
|
|
58
59
|
export { SpacesRail } from './sidebar/SpacesRail.js';
|
|
60
|
+
export type { SidebarProps } from './sidebar/Sidebar.js';
|
|
61
|
+
export { Sidebar } from './sidebar/Sidebar.js';
|
|
62
|
+
export type { SidebarHeaderProps } from './sidebar/SidebarHeader.js';
|
|
63
|
+
export { SidebarHeader } from './sidebar/SidebarHeader.js';
|
|
64
|
+
export type { SidebarActionButtonProps } from './sidebar/SidebarActionButton.js';
|
|
65
|
+
export { SidebarActionButton } from './sidebar/SidebarActionButton.js';
|
|
66
|
+
export type { SidebarItemProps } from './sidebar/SidebarItem.js';
|
|
67
|
+
export { SidebarItem } from './sidebar/SidebarItem.js';
|
|
68
|
+
|
|
69
|
+
// Lightbox surface — full-screen scrim overlay for media previews.
|
|
70
|
+
// Headless: the image and all button chrome are injected by the host app so
|
|
71
|
+
// this package stays free of expo-image / @expo/vector-icons / reanimated.
|
|
72
|
+
export type { LightboxProps } from './lightbox/Lightbox.js';
|
|
73
|
+
export { Lightbox } from './lightbox/Lightbox.js';
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Full-screen scrim overlay that centers its content. Tapping the backdrop, the
|
|
3
|
+
* close button, the Escape key (web) or the hardware back (Android) dismisses it.
|
|
4
|
+
*
|
|
5
|
+
* All interactive chrome (close button, action buttons) is injected via render
|
|
6
|
+
* props so this package remains free of @expo/vector-icons, expo-image, and
|
|
7
|
+
* reanimated. The host app renders its own icon buttons and images.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```tsx
|
|
11
|
+
* import { Lightbox } from '@drakkar.software/octospaces-ui';
|
|
12
|
+
*
|
|
13
|
+
* <Lightbox
|
|
14
|
+
* visible={zoomed}
|
|
15
|
+
* onClose={() => setZoomed(false)}
|
|
16
|
+
* renderCloseButton={(onClose) => (
|
|
17
|
+
* <IconButton name="x" color={colors.onScrim} onPress={onClose} />
|
|
18
|
+
* )}
|
|
19
|
+
* renderActions={() => (
|
|
20
|
+
* <IconButton name="share" color={colors.onScrim} onPress={handleShare} />
|
|
21
|
+
* )}
|
|
22
|
+
* >
|
|
23
|
+
* <Image source={{ uri }} style={{ width: w * 0.92, height: h * 0.82 }} />
|
|
24
|
+
* </Lightbox>
|
|
25
|
+
* ```
|
|
26
|
+
*/
|
|
27
|
+
import React, { useEffect } from 'react';
|
|
28
|
+
import type { ReactNode } from 'react';
|
|
29
|
+
import { Modal, Platform, Pressable, View } from 'react-native';
|
|
30
|
+
|
|
31
|
+
import { useOctoSpacesTheme } from '../theme/provider.js';
|
|
32
|
+
|
|
33
|
+
export interface LightboxProps {
|
|
34
|
+
visible: boolean;
|
|
35
|
+
onClose: () => void;
|
|
36
|
+
/** Centered content — the host renders the full-size image here. */
|
|
37
|
+
children: ReactNode;
|
|
38
|
+
/** Accessible label for the backdrop tap-to-close. Default: "Close preview". */
|
|
39
|
+
closeLabel?: string;
|
|
40
|
+
/**
|
|
41
|
+
* Render the close affordance pinned to the top-right corner.
|
|
42
|
+
* Receives `onClose` so the button can dismiss the overlay.
|
|
43
|
+
* If omitted, tapping the backdrop or hardware back still closes it.
|
|
44
|
+
*/
|
|
45
|
+
renderCloseButton?: (onClose: () => void) => ReactNode;
|
|
46
|
+
/**
|
|
47
|
+
* Render additional action(s) pinned to the bottom-right corner,
|
|
48
|
+
* e.g. a save/share button. Return `null` to show nothing.
|
|
49
|
+
*/
|
|
50
|
+
renderActions?: () => ReactNode;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Full-screen scrim overlay that centers its children. Headless: all buttons
|
|
55
|
+
* are injected via render props; the package has no icon or image dependencies.
|
|
56
|
+
*
|
|
57
|
+
* Dismissal: backdrop tap · `renderCloseButton` · hardware back (Android) ·
|
|
58
|
+
* Escape key (web).
|
|
59
|
+
*/
|
|
60
|
+
export function Lightbox({
|
|
61
|
+
visible,
|
|
62
|
+
onClose,
|
|
63
|
+
children,
|
|
64
|
+
closeLabel = 'Close preview',
|
|
65
|
+
renderCloseButton,
|
|
66
|
+
renderActions,
|
|
67
|
+
}: LightboxProps) {
|
|
68
|
+
const theme = useOctoSpacesTheme();
|
|
69
|
+
|
|
70
|
+
// Web has no hardware back button; close on Escape to match the native affordance.
|
|
71
|
+
useEffect(() => {
|
|
72
|
+
if (!visible || Platform.OS !== 'web') return;
|
|
73
|
+
const onKey = (e: KeyboardEvent) => {
|
|
74
|
+
if (e.key === 'Escape') onClose();
|
|
75
|
+
};
|
|
76
|
+
window.addEventListener('keydown', onKey);
|
|
77
|
+
return () => window.removeEventListener('keydown', onKey);
|
|
78
|
+
}, [visible, onClose]);
|
|
79
|
+
|
|
80
|
+
// Spacing token lookups use numeric keys (OctoChat) with named-key fallbacks
|
|
81
|
+
// (OctoVault) so the overlay is correctly inset in either host. The `?? n`
|
|
82
|
+
// values match the underlying spacing scale (xl=24, xxl=32, lg=16).
|
|
83
|
+
const pad = (theme.spacing['6'] as number) ?? 24;
|
|
84
|
+
const insetV = (theme.spacing['8'] as number) ?? 32;
|
|
85
|
+
const insetH = (theme.spacing['4'] as number) ?? 16;
|
|
86
|
+
|
|
87
|
+
return (
|
|
88
|
+
<Modal
|
|
89
|
+
visible={visible}
|
|
90
|
+
transparent
|
|
91
|
+
animationType="fade"
|
|
92
|
+
onRequestClose={onClose}
|
|
93
|
+
statusBarTranslucent
|
|
94
|
+
>
|
|
95
|
+
{/* Backdrop — full-screen scrim, tap anywhere to close */}
|
|
96
|
+
<Pressable
|
|
97
|
+
style={{
|
|
98
|
+
flex: 1,
|
|
99
|
+
alignItems: 'center',
|
|
100
|
+
justifyContent: 'center',
|
|
101
|
+
padding: pad,
|
|
102
|
+
backgroundColor: theme.colors.overlay ?? 'rgba(0,0,0,0.85)',
|
|
103
|
+
}}
|
|
104
|
+
onPress={onClose}
|
|
105
|
+
accessibilityLabel={closeLabel}
|
|
106
|
+
>
|
|
107
|
+
{/* Content — pointerEvents="box-none" so the View itself doesn't intercept
|
|
108
|
+
taps (they fall through to the backdrop), but its children still can. */}
|
|
109
|
+
<View
|
|
110
|
+
style={{ alignItems: 'center', justifyContent: 'center' }}
|
|
111
|
+
pointerEvents="box-none"
|
|
112
|
+
>
|
|
113
|
+
{children}
|
|
114
|
+
</View>
|
|
115
|
+
|
|
116
|
+
{/* Close slot — top-right corner */}
|
|
117
|
+
{renderCloseButton ? (
|
|
118
|
+
<View style={{ position: 'absolute', top: insetV, right: insetH }}>
|
|
119
|
+
{renderCloseButton(onClose)}
|
|
120
|
+
</View>
|
|
121
|
+
) : null}
|
|
122
|
+
|
|
123
|
+
{/* Action slot — bottom-right corner */}
|
|
124
|
+
{renderActions ? (
|
|
125
|
+
<View style={{ position: 'absolute', bottom: insetV, right: insetH }}>
|
|
126
|
+
{renderActions()}
|
|
127
|
+
</View>
|
|
128
|
+
) : null}
|
|
129
|
+
</Pressable>
|
|
130
|
+
</Modal>
|
|
131
|
+
);
|
|
132
|
+
}
|