Ctrl K
ComunidadGuías…

features/features.yaml

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
# Ethora SDK Feature Matrix — source of truth
#
# This file drives the rendered matrices in features/README.md and features/views/.
# See features/README.md for the human-readable output and features/scripts/ for the renderer.
#
# Statuses:
#   present  — fully implemented and working
#   partial  — partially implemented, limited scope, or config-only
#   roadmap  — referenced in code/docs but not yet working
#   absent   — not present
#
# Evidence is a file path (and optional line range) — it's a snapshot from the audit
# date and will drift. Phase 2 will add an automated verifier.

meta:
  version: "26.04"
  last_reviewed: "2026-04-21"
  methodology: >
    Statuses are based on direct code audit (file-level inspection, not just README
    claims). Each feature has a stable ID so it survives renames. "Partial" is used
    when a config / model / stub exists but the end-to-end flow isn't wired.
  next_review: "2026-05-01 (monthly via CI)"

sdks:
  reactjs:
    label: "React.js (Web)"
    repo: "https://github.com/dappros/ethora-chat-component"
    notes: "Flagship SDK, fullest feature coverage. Effectively the JS/TS reference."
    order: 1
  reactnative:
    label: "React Native"
    repo: "https://github.com/dappros/ethora-chat-component-rn"
    notes: "Mobile React Native. Shares architecture with React.js but trails on reactions and notifications."
    order: 2
  android:
    label: "Android (Kotlin)"
    repo: "https://github.com/dappros/ethora-sdk-android"
    notes: "Native Kotlin + Jetpack Compose. JitPack-distributed. No account provisioning APIs."
    order: 3
  swift:
    label: "iOS (Swift)"
    repo: "https://github.com/dappros/ethora-sdk-swift"
    notes: "Native SwiftUI via SPM. Tracks Android closely; no logout helper or dark mode yet."
    order: 4
  wordpress:
    label: "WordPress"
    repo: "https://github.com/dappros/ethora-wp-plugin"
    notes: "Bot-widget plugin, not a full chat SDK. Embeds a single bot-to-user widget."
    order: 5

categories:
  - id: auth
    label: "Authentication"
    order: 1
  - id: messaging
    label: "Messaging"
    order: 2
  - id: media
    label: "Media"
    order: 3
  - id: rooms
    label: "Rooms"
    order: 4
  - id: presence
    label: "Presence & Unread"
    order: 5
  - id: notifications
    label: "Notifications"
    order: 6
  - id: ai
    label: "AI / Bots"
    order: 7
  - id: ui
    label: "UI"
    order: 8
  - id: platform
    label: "Platform"
    order: 9

features:
  # ── Authentication ─────────────────────────────────────────────────────────
  - id: auth_signup_email
    category: auth
    label: "Email/password signup"
    description: "Sign up a new user with email + password directly from the SDK."
    sdks:
      reactjs: { status: present, evidence: "src/networking/api-requests/auth.api.ts" }
      reactnative: { status: partial, evidence: "src/components/AuthForms/Register.tsx", notes: "Form stub, not fully wired." }
      android: { status: absent }
      swift: { status: absent }
      wordpress: { status: absent }

  - id: auth_login_email
    category: auth
    label: "Email/password login"
    description: "Sign in with email + password."
    sdks:
      reactjs: { status: present, evidence: "src/networking/api-requests/auth.api.ts" }
      reactnative: { status: present, evidence: "src/networking/api-requests/auth.api.ts:15-30" }
      android: { status: present, evidence: "chat-core/src/main/java/com/ethora/chat/core/networking/AuthAPI.kt:19" }
      swift: { status: present, evidence: "Sources/XMPPChatCore/Networking/AuthAPI.swift:79-217" }
      wordpress: { status: absent }

  - id: auth_jwt
    category: auth
    label: "JWT login (bring your own token)"
    description: "Authenticate with a pre-signed JWT from your own backend."
    sdks:
      reactjs: { status: present, evidence: "src/networking/api-requests/auth.api.ts (loginViaJwt)" }
      reactnative: { status: present, evidence: "src/networking/api-requests/auth.api.ts:78-90" }
      android: { status: present, evidence: "chat-core/src/main/java/com/ethora/chat/core/networking/AuthAPI.kt:26" }
      swift: { status: present, evidence: "Sources/XMPPChatCore/Networking/AuthAPI.swift:349-402" }
      wordpress: { status: absent }

  - id: auth_oauth
    category: auth
    label: "OAuth / social login"
    description: "Sign in with Google/Apple/other OAuth providers."
    sdks:
      reactjs: { status: present, evidence: "src/networking/api-requests/auth.api.ts (signInWithGoogle, loginSocial)" }
      reactnative: { status: partial, evidence: "src/networking/api-requests/auth.api.ts:32-48", notes: "Functions exist, implementation incomplete." }
      android: { status: roadmap, evidence: "chat-core/src/main/java/com/ethora/chat/core/config/LoginConfig.kt:8", notes: "GoogleLoginConfig defined, not wired." }
      swift: { status: partial, evidence: "Sources/XMPPChatCore/Models/Config/LoginConfig.swift", notes: "Firebase/Google only." }
      wordpress: { status: partial, evidence: "assets/js/ethora_assistant_source.js:160-163", notes: "OAuth scopes defined, stub only." }

  - id: auth_anonymous
    category: auth
    label: "Anonymous / guest login"
    description: "Allow visitors to join without registering."
    sdks:
      reactjs: { status: absent }
      reactnative: { status: roadmap, evidence: "src/components/AuthForms/LoginAnonym.tsx", notes: "Form stub." }
      android: { status: absent }
      swift: { status: absent }
      wordpress: { status: absent }

  - id: auth_logout
    category: auth
    label: "Logout"
    description: "Clear credentials and disconnect from chat."
    sdks:
      reactjs: { status: partial, evidence: "src/main.ts (logoutService export)", notes: "Handler exists, no explicit endpoint call." }
      reactnative: { status: present, evidence: "src/roomStore/chatSettingsSlice.ts + src/networking/apiClient.ts:37" }
      android: { status: present, evidence: "chat-core/src/main/java/com/ethora/chat/core/service/LogoutService.kt:72" }
      swift: { status: absent, notes: "No dedicated logout method." }
      wordpress: { status: partial, evidence: "assets/js/ethora_assistant_source.js:333", notes: "Stub function." }

  - id: auth_token_refresh
    category: auth
    label: "Token refresh"
    description: "Automatically refresh expiring auth tokens."
    sdks:
      reactjs: { status: present, evidence: "src/types/models/config.model.ts (refreshTokens) + xmppClient reconnect" }
      reactnative: { status: present, evidence: "src/networking/apiClient.ts:12-40" }
      android: { status: present, evidence: "chat-core/src/main/java/com/ethora/chat/core/networking/TokenManager.kt:37" }
      swift: { status: present, evidence: "Sources/XMPPChatCore/Networking/AuthAPI.swift:228-339" }
      wordpress: { status: absent }

  # ── Messaging ──────────────────────────────────────────────────────────────
  - id: msg_text_send
    category: messaging
    label: "Send text message"
    description: "Send a plain text message to a room."
    sdks:
      reactjs: { status: present, evidence: "src/networking/xmpp/sendTextMessage.xmpp.ts" }
      reactnative: { status: present, evidence: "src/networking/xmpp/sendTextMessage.xmpp.ts + useSendMessage.tsx" }
      android: { status: present, evidence: "chat-core/src/main/java/com/ethora/chat/core/xmpp/XMPPOperations.kt:15" }
      swift: { status: present, evidence: "Sources/XMPPChatCore/XMPPOperations/SendTextMessage.swift:16-69" }
      wordpress: { status: partial, evidence: "assets/js/ethora_assistant_source.js:258-273", notes: "Basic text input, simulated response." }

  - id: msg_text_edit
    category: messaging
    label: "Edit message"
    description: "Modify a previously sent message."
    sdks:
      reactjs: { status: present, evidence: "src/networking/xmpp/editMessage.xmpp.ts" }
      reactnative: { status: present, evidence: "src/networking/xmpp/editMessage.xmpp.ts" }
      android: { status: present, evidence: "chat-core/src/main/java/com/ethora/chat/core/xmpp/XMPPClient.kt (editMessage)" }
      swift: { status: present, evidence: "Sources/XMPPChatCore/XMPPOperations/SendTextMessage.swift:148-171" }
      wordpress: { status: absent }

  - id: msg_text_delete
    category: messaging
    label: "Delete message"
    description: "Remove a previously sent message."
    sdks:
      reactjs: { status: present, evidence: "src/networking/xmpp/deleteMessage.xmpp.ts" }
      reactnative: { status: present, evidence: "src/networking/xmpp/deleteMessage.xmpp.ts" }
      android: { status: present, evidence: "chat-core/.../XMPPClient.kt (deleteMessage)" }
      swift: { status: present, evidence: "Sources/XMPPChatCore/XMPPOperations/SendTextMessage.swift:128-146" }
      wordpress: { status: absent }

  - id: msg_reply
    category: messaging
    label: "Reply to message"
    description: "Quote and reply to a specific message."
    sdks:
      reactjs: { status: present, evidence: "src/networking/xmpp/sendTextMessage.xmpp.ts (isReply)" }
      reactnative: { status: present, evidence: "src/types/types.ts:29 + src/types/messages.ts:27-29" }
      android: { status: present, evidence: "chat-core/.../models/Message.kt:29 (reply, mainMessage)" }
      swift: { status: present, evidence: "Sources/XMPPChatCore/XMPPOperations/SendTextMessage.swift (isReply param)" }
      wordpress: { status: absent }

  - id: msg_reactions
    category: messaging
    label: "Message reactions"
    description: "React to messages with emoji."
    sdks:
      reactjs: { status: present, evidence: "src/networking/xmpp/sendMessageReaction.xmpp.ts" }
      reactnative: { status: absent }
      android: { status: present, evidence: "chat-core/.../models/Message.kt:30 (reaction Map)" }
      swift: { status: present, evidence: "Sources/XMPPChatCore/XMPPOperations/SendTextMessage.swift:173-220" }
      wordpress: { status: absent }

  - id: msg_mentions
    category: messaging
    label: "@mentions"
    description: "Tag users with @mentions in messages."
    sdks:
      reactjs: { status: absent }
      reactnative: { status: absent }
      android: { status: absent }
      swift: { status: absent }
      wordpress: { status: absent }

  - id: msg_typing
    category: messaging
    label: "Typing indicator"
    description: "Show when other users are typing."
    sdks:
      reactjs: { status: present, evidence: "src/networking/xmpp/sendTypingRequest.xmpp.ts" }
      reactnative: { status: present, evidence: "src/networking/xmpp/sendTypingRequest.xmpp.ts" }
      android: { status: present, evidence: "chat-core/.../XMPPOperations.kt:47 (sendTypingIndicator)" }
      swift: { status: present, evidence: "Sources/XMPPChatCore/XMPPOperations/SendTextMessage.swift:222-248" }
      wordpress: { status: absent }

  - id: msg_delivery_status
    category: messaging
    label: "Delivery status indicators"
    description: "Per-message status UI — single tick (sent), double tick (delivered), accent colour when read. Shows the conversation participant whether their message has made it through."
    sdks:
      reactjs: { status: present, evidence: "src/components/MessageBubble/Message.tsx:317-318 (DoubleTick + isPending)" }
      reactnative: { status: absent }
      android: { status: absent }
      swift: { status: present, evidence: "Sources/XMPPChatUI/ChatRoom/MessageBubbleView.swift" }
      wordpress: { status: absent }

  - id: msg_history_pagination
    category: messaging
    label: "Message history (MAM pagination)"
    description: "Load earlier messages via XEP-0313 MAM with paging."
    sdks:
      reactjs: { status: present, evidence: "src/networking/xmpp/getHistory.xmpp.ts (RSM)" }
      reactnative: { status: present, evidence: "src/networking/xmpp/getHistory.xmpp.ts" }
      android: { status: present, evidence: "chat-core/.../XMPPClient.kt (registerMAMCollector)" }
      swift: { status: present, evidence: "Sources/XMPPChatCore/XMPPOperations/SendGetHistory.swift" }
      wordpress: { status: absent }

  # ── Media ──────────────────────────────────────────────────────────────────
  - id: media_image
    category: media
    label: "Send/receive images"
    description: "Upload and display image messages."
    sdks:
      reactjs: { status: present, evidence: "src/networking/xmpp/sendMediaMessage.xmpp.ts + uploadFile" }
      reactnative: { status: present, evidence: "src/components/styled/MessageImage.tsx" }
      android: { status: present, evidence: "chat-core/.../AuthAPI.kt:239 (uploadFile)" }
      swift: { status: present, evidence: "Sources/XMPPChatCore/XMPPOperations/SendTextMessage.swift:71-126" }
      wordpress: { status: absent }

  - id: media_video
    category: media
    label: "Send/receive video"
    description: "Upload and play back video messages."
    sdks:
      reactjs: { status: present, evidence: "sendMediaMessage.xmpp.ts (video mimetype)" }
      reactnative: { status: present, evidence: "src/components/styled/VideoMessage.tsx" }
      android: { status: present, evidence: "chat-ui/.../VideoPlayerView.kt" }
      swift: { status: present, evidence: "sendMediaMessage supports video mimetype" }
      wordpress: { status: absent }

  - id: media_file
    category: media
    label: "Send/receive files"
    description: "Upload arbitrary file attachments."
    sdks:
      reactjs: { status: present, evidence: "sendMediaMessage + uploadFile" }
      reactnative: { status: present, evidence: "src/types/messages.ts:37-48 (FileAttachmentParams)" }
      android: { status: present, evidence: "chat-core/.../AuthAPI.kt:36 (POST /files multipart)" }
      swift: { status: present, evidence: "Sources/XMPPChatCore/Networking/AuthAPI.swift:467-538 (uploadFile)" }
      wordpress: { status: absent }

  - id: media_audio
    category: media
    label: "Audio messages"
    description: "Record and play voice notes with waveform/duration."
    sdks:
      reactjs: { status: present, evidence: "sendMediaMessage (waveForm, duration)" }
      reactnative: { status: present, evidence: "src/components/InputComponents/AudioRecorder.tsx" }
      android: { status: present, evidence: "chat-ui/.../AudioPlayerView.kt" }
      swift: { status: present, evidence: "MediaMessageData (duration, waveForm)" }
      wordpress: { status: absent }

  - id: media_inline_preview
    category: media
    label: "Inline media preview"
    description: "Render images/video/links inline in the message stream."
    sdks:
      reactjs: { status: partial, notes: "locationPreview exists; explicit inline image rendering limited." }
      reactnative: { status: present, evidence: "src/types/messages.ts:42 (imagePreview) + MessageImage.tsx" }
      android: { status: present, evidence: "chat-core/.../Message.kt:19 (locationPreview)" }
      swift: { status: partial, evidence: "Sources/XMPPChatUI/Components/URLPreviewCard.swift", notes: "Link previews only, not inline media." }
      wordpress: { status: absent }

  # ── Rooms ──────────────────────────────────────────────────────────────────
  - id: room_1on1
    category: rooms
    label: "1:1 direct messaging"
    description: "Private one-to-one conversations."
    sdks:
      reactjs: { status: present, evidence: "src/networking/xmpp/createPrivateRoom.xmpp.ts" }
      reactnative: { status: present, evidence: "XMPP direct recipient rooms" }
      android: { status: present, evidence: "chat-core/.../RoomsAPI.kt:35 (createPrivateRoom)" }
      swift: { status: present, evidence: "Sources/XMPPChatCore/XMPPOperations/RoomOperations.swift:46-71" }
      wordpress: { status: partial, notes: "1:1 bot-to-user widget only." }

  - id: room_group
    category: rooms
    label: "Group chat (MUC)"
    description: "Multi-user chat rooms via XEP-0045."
    sdks:
      reactjs: { status: present, evidence: "src/networking/xmpp/createRoom.xmpp.ts" }
      reactnative: { status: present, evidence: "src/networking/xmpp/createRoom.xmpp.ts" }
      android: { status: present, evidence: "chat-core/.../XMPPClient.kt (MUC support)" }
      swift: { status: present, evidence: "RoomOperations.swift:16-43" }
      wordpress: { status: absent }

  - id: room_single_mode
    category: rooms
    label: "Single-room mode"
    description: "Pin the SDK to one pre-configured room (chat widget flow)."
    sdks:
      reactjs: { status: present, evidence: "config.model.ts:76 (customRooms.singleRoom)" }
      reactnative: { status: present, evidence: "roomJID parameter (README)" }
      android: { status: present, evidence: "disableRooms + roomJID (README)" }
      swift: { status: present, evidence: "Sources/XMPPChatCore/Models/ChatConfig.swift:33 (disableRooms)" }
      wordpress: { status: present, evidence: "Widget is single-room by design." }

  - id: room_create
    category: rooms
    label: "Create room"
    description: "Programmatically create a new chat room."
    sdks:
      reactjs: { status: present, evidence: "createRoom.xmpp.ts + createPrivateRoom.xmpp.ts" }
      reactnative: { status: present, evidence: "src/networking/xmpp/createRoom.xmpp.ts" }
      android: { status: present, evidence: "chat-core/.../RoomsAPI.kt:24 (createRoom)" }
      swift: { status: present, evidence: "RoomOperations.swift (createRoom, createPrivateRoom)" }
      wordpress: { status: absent }

  - id: room_invite
    category: rooms
    label: "Invite users to room"
    description: "Invite other users to an existing room."
    sdks:
      reactjs: { status: present, evidence: "src/networking/xmpp/inviteRoomRequest.xmpp.ts" }
      reactnative: { status: present, evidence: "src/networking/xmpp/inviteRoomRequest.xmpp.ts" }
      android: { status: partial, notes: "members field exists, no dedicated invite API." }
      swift: { status: partial, notes: "Referenced in RoomOperations but incomplete." }
      wordpress: { status: absent }

  - id: room_leave
    category: rooms
    label: "Leave room"
    description: "Leave a chat room."
    sdks:
      reactjs: { status: present, evidence: "src/networking/xmpp/leaveTheRoom.xmpp.ts" }
      reactnative: { status: present, evidence: "src/networking/xmpp/leaveTheRoom.xmpp.ts" }
      android: { status: present, evidence: "chat-core/.../XMPPOperations.kt:36 (leaveRoom)" }
      swift: { status: absent, notes: "No leave-room method found." }
      wordpress: { status: absent }

  - id: room_metadata
    category: rooms
    label: "Room metadata"
    description: "Room name, avatar, description, title."
    sdks:
      reactjs: { status: present, evidence: "setRoomImage.xmpp.ts + room config files" }
      reactnative: { status: partial, evidence: "setRoomImage (avatar only, no name/description update)" }
      android: { status: present, evidence: "chat-core/.../models/Room.kt:39 (name, title, description, picture)" }
      swift: { status: present, evidence: "Room model (name, avatar, description)" }
      wordpress: { status: absent }

  # ── Presence & Unread ──────────────────────────────────────────────────────
  - id: presence_online
    category: presence
    label: "Online/offline presence"
    description: "Show which users are currently online."
    sdks:
      reactjs: { status: present, evidence: "src/networking/xmpp/presenceInRoom.xmpp.ts" }
      reactnative: { status: present, evidence: "presenceInRoom + xmppClient status" }
      android: { status: present, evidence: "chat-core/.../XMPPClient.kt (presence)" }
      swift: { status: present, evidence: "Sources/XMPPChatCore/XMPPOperations/PresenceOperations.swift:13-62" }
      wordpress: { status: absent }

  - id: presence_last_seen
    category: presence
    label: "Last-seen timestamp"
    description: "Show when a user was last active."
    sdks:
      reactjs: { status: absent }
      reactnative: { status: present, evidence: "src/types/types.ts:63 (last_active) + ChatProfileModal.tsx" }
      android: { status: partial, evidence: "models/RoomMember.kt:15 (lastActive)", notes: "Field present, not fully integrated." }
      swift: { status: absent }
      wordpress: { status: absent }

  - id: unread_counter
    category: presence
    label: "Unread message counter"
    description: "Track unread messages per room + total badge."
    sdks:
      reactjs: { status: present, evidence: "src/hooks/useUnreadMessagesCounter.ts" }
      reactnative: { status: present, evidence: "src/roomStore/Middleware/unreadMidlleware.tsx" }
      android: { status: present, evidence: "chat-ui/.../UnreadHook.kt (useUnread)" }
      swift: { status: present, evidence: "Sources/XMPPChatCore/Store/RoomStore.swift (totalUnreadCount)" }
      wordpress: { status: absent }

  # ── Notifications ──────────────────────────────────────────────────────────
  - id: push_fcm_apns
    category: notifications
    label: "Push notifications (FCM/APNS/Web Push)"
    description: "Deliver notifications when the app is backgrounded."
    sdks:
      reactjs: { status: present, evidence: "src/networking/api-requests/push.api.ts + Firebase/Web Push VAPID" }
      reactnative: { status: partial, evidence: "messages.ts:21-23 (push flag)", notes: "Protocol flag only, no FCM/APNS SDK integration." }
      android: { status: present, evidence: "chat-core/.../push/PushNotificationManager.kt:18 (FCM)" }
      swift: { status: present, evidence: "Sources/XMPPChatCore/Networking/PushAPI.swift:34 (APNS)" }
      wordpress: { status: absent }

  - id: notification_in_app
    category: notifications
    label: "In-app notifications"
    description: "Show incoming-message toasts or sounds inside the app."
    sdks:
      reactjs: { status: present, evidence: "src/context/MessageNotificationContext.tsx" }
      reactnative: { status: absent }
      android: { status: partial, evidence: "chat-core/.../config/MessageNotificationConfig.kt", notes: "Config exists, integration limited." }
      swift: { status: partial, evidence: "Sources/XMPPChatCore/Models/MessageNotification.swift", notes: "Config present, sound not wired." }
      wordpress: { status: absent }

  # ── AI / Bots ──────────────────────────────────────────────────────────────
  - id: ai_rag_bot
    category: ai
    label: "RAG bot integration"
    description: "Chat against a retrieval-augmented bot grounded in your knowledge base."
    sdks:
      reactjs: { status: absent, notes: "Bot lives server-side; SDKs just render messages." }
      reactnative: { status: absent }
      android: { status: absent }
      swift: { status: absent }
      wordpress: { status: absent, notes: "Widget connects to bot but no RAG config in plugin." }

  - id: ai_bot_trigger
    category: ai
    label: "AI auto-response triggers"
    description: "Configure when AI responds (every message / @mention / keyword)."
    sdks:
      reactjs: { status: absent, notes: "Configured server-side on the bot, not in SDK." }
      reactnative: { status: absent }
      android: { status: absent }
      swift: { status: absent }
      wordpress: { status: partial, notes: "Bot ID parameter auto-responds, no trigger config." }

  - id: ai_translation
    category: ai
    label: "Message translation"
    description: "Translate messages into the reader's preferred language."
    sdks:
      reactjs: { status: partial, evidence: "src/networking/xmpp/sendTextMessageWithTranslateTag.xmpp.ts", notes: "Send-with-translate-tag exists, config only otherwise." }
      reactnative: { status: absent }
      android: { status: partial, evidence: "chat-core/.../config/TranslationsConfig.kt", notes: "Config enabled with language codes." }
      swift: { status: partial, evidence: "Sources/XMPPChatCore/Models/MessageTranslation.swift + TranslationConfig.swift" }
      wordpress: { status: absent }

  # ── UI ─────────────────────────────────────────────────────────────────────
  - id: ui_prebuilt_component
    category: ui
    label: "Prebuilt chat component"
    description: "Drop-in chat UI (room list + message stream + input)."
    sdks:
      reactjs: { status: present, evidence: "src/components/MainComponents (Chat, ChatRoom, MessageList, SendInput)" }
      reactnative: { status: present, evidence: "src/components/MainComponents/Chat.tsx" }
      android: { status: present, evidence: "chat-ui/.../EthoraChat.kt (Chat composable + RoomListView)" }
      swift: { status: present, evidence: "Sources/XMPPChatUI/Components/ChatWrapperView.swift" }
      wordpress: { status: present, evidence: "assets/js/ethora_assistant_source.js:200-240 (widget)" }

  - id: ui_theming
    category: ui
    label: "Theme customization"
    description: "Customize colors, typography, spacing."
    sdks:
      reactjs: { status: partial, evidence: "config.model.ts (colors.primary/secondary)", notes: "Limited — primary/secondary only." }
      reactnative: { status: partial, evidence: "src/types/types.ts:137 (colors)", notes: "Limited palette." }
      android: { status: present, evidence: "chat-ui/.../ChatTheme.kt (full ChatTheme + customizable colors)" }
      swift: { status: present, evidence: "Sources/XMPPChatUI/Styling/ChatStyles.swift (ChatColors + RoomListStyles + ChatRoomStyles)" }
      wordpress: { status: absent }

  - id: ui_dark_mode
    category: ui
    label: "Dark mode"
    description: "First-class dark color scheme."
    sdks:
      reactjs: { status: absent }
      reactnative: { status: absent }
      android: { status: present, evidence: "chat-ui/.../ChatTheme.kt:32 (darkChatColorScheme)" }
      swift: { status: absent }
      wordpress: { status: absent }

  - id: ui_slots_override
    category: ui
    label: "Custom component overrides"
    description: "Replace message bubble, input, scroll area with your own components."
    sdks:
      reactjs: { status: present, evidence: "CustomMessageComponent + CustomInputComponent + CustomComponentsContext.tsx" }
      reactnative: { status: present, evidence: "CustomMessageComponent prop (README)" }
      android: { status: present, evidence: "chat-core/.../models/CustomComponents.kt:80 (CustomComponents interface)" }
      swift: { status: partial, evidence: "CustomComponentsProtocol in ChatConfig", notes: "Limited scope." }
      wordpress: { status: absent }

  - id: ui_localization
    category: ui
    label: "i18n / localization"
    description: "UI string translation for app localization."
    sdks:
      reactjs: { status: absent, notes: "translates config exists but no i18n library integration." }
      reactnative: { status: absent }
      android: { status: partial, notes: "TranslationsConfig for message translation, no UI string resources." }
      swift: { status: absent }
      wordpress: { status: partial, evidence: "ethora-assistant-plugin.php:14 (text domain)", notes: "Text domain only, no language files." }

  # ── Platform ───────────────────────────────────────────────────────────────
  - id: platform_selfhosted
    category: platform
    label: "Self-hosted server support"
    description: "Point the SDK at your own Ethora server deployment."
    sdks:
      reactjs: { status: present, evidence: "baseUrl + xmppSettings config" }
      reactnative: { status: present, evidence: "README: override xmppSettings + baseUrl" }
      android: { status: present, evidence: "chat-core/.../XMPPSettings.kt (configurable URL)" }
      swift: { status: present, evidence: "Sources/XMPPChatCore/Models/ChatConfig.swift:28 (baseUrl)" }
      wordpress: { status: present, evidence: "readme.txt (data-bot-id parameter)" }

  - id: platform_create_app
    category: platform
    label: "Provision new app via SDK"
    description: "Create a new Ethora app (tenant) programmatically from the SDK."
    sdks:
      reactjs: { status: absent, notes: "Handled by @ethora/setup CLI, not SDKs." }
      reactnative: { status: absent }
      android: { status: absent }
      swift: { status: absent }
      wordpress: { status: absent }

  - id: platform_offline_cache
    category: platform
    label: "Offline message cache"
    description: "Persist messages locally so they're visible without network."
    sdks:
      reactjs: { status: present, evidence: "redux-persist with encrypted storage" }
      reactnative: { status: partial, evidence: "src/hooks/useLocalStorage.tsx + async-storage", notes: "User data only, not full message sync." }
      android: { status: present, evidence: "chat-core/.../persistence/ChatPersistenceManager.kt" }
      swift: { status: present, evidence: "Sources/XMPPChatCore/Persistence/MessageCache.swift (LRU eviction)" }
      wordpress: { status: absent }