page ChatBubblecomponent { seo { title = "ChatBubble component" description = "Theme-aware, responsive chat bubble component." } view {
W WRNexusJS
base component

ChatBubble

Theme-aware, responsive chat bubble component.

@wrnexus/ui 0.5.011 props1 slots4 events

Usage

Components are auto-discovered. Use the component directly in any .wrn view:

<ChatBubble
  size="default"
  color="primary"
  title="title"
  description="description"
  items="[]"
/>

Legacy mount name: data-component="ChatBubble".

Props and attributes

PropTypeRequirementDefault
sizestringOptional"default"
colorstringOptional"primary"
titlestringOptional""
descriptionstringOptional""
itemsstringOptional[]
oneSidedbooleanOptionalfalse
showAvatarsbooleanOptionalfalse
showMetadatabooleanOptionalfalse
ariaLabelstringOptional"Conversation"
variantstringOptional"default"
classstringOptional""

Slots

  • default

Events

  • action
  • messageClick
  • avatarClick
  • linkClick

Source contract

Installed source: components/ChatBubble.wrn

component ChatBubble {
  props {
    @event action = function
    @event messageClick = function
    @event avatarClick = function
    @event linkClick = function
    size = "default"
    color = "primary"
    title = ""
    description = ""
    items = []
    oneSided = false
    showAvatars = false
    showMetadata = false
    ariaLabel = "Conversation"
    variant = "default"
    class = ""
  }

  functions {
    function messageDirection(item) {
      return item.direction === "outgoing" ? "outgoing" : "incoming"
    }

    function selectMessage(item, index) {
      $emit("messageClick", {
        item: item,
        index: index,
        direction: messageDirection(item)
      })
    }

    function selectAvatar(sourceEvent, item, index) {
      sourceEvent.stopPropagation()
      $emit("avatarClick", {
        item: item,
        index: index,
        direction: messageDirection(item)
      })
    }

    function selectLink(sourceEvent, link, item, index) {
      sourceEvent.stopPropagation()
      $emit("linkClick", {
        link: link,
        item: item,
        index: index
      })
    }

    function selectAction(sourceEvent, item, index) {
      sourceEvent.stopPropagation()
      $emit("action", {
        action: item.action || "retry",
        item: item,
        index: index
      })
    }
  }

  view {
        <section
      {...attrs}
      class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--chat-bubble wire-next--variant-{variant} {class}"
      data-one-sided="{oneSided}"
      role="log"
      aria-label="{ariaLabel}"
      aria-live="polite"
    >
      {#if title || description}
        <header class="wire-next__chat-header">
          {#if title}<h3>{title}</h3>{/if}
          {#if description}<p>{description}</p>{/if}
        </header>
      {/if}

      {#if items.length}
        <div class="wire-next__chat-thread">
          {#each items as item, index}
            <article
              class="wire-next__chat-message"
              data-direction="{messageDirection(item)}"
              aria-label="{messageDirection(item) === 'outgoing' ? 'Sent message' : 'Received message'}"
            >
        <div class="wire-next__chat-row">
                {#if showAvatars && (item.avatarSrc || item.avatarFallback)}
                  <button
                    class="wire-next__chat-avatar"
                    type="button"
                    aria-label="{item.avatarLabel || item.author || 'Message author'}"
                    @click="selectAvatar(event, item, index)"
                  >
                    {#if item.avatarSrc}
                      <img src="{item.avatarSrc}" alt="{item.avatarAlt || ''}" />
                    {:else}
                      <span>{item.avatarFallback}</span>
                    {/if}
                  </button>
                {/if}

                <div
                  class="wire-next__chat-content"
                  role="button"
                  tabindex="0"
                  @click="selectMessage(item, index)"
                  @keydown="if (event.key === 'Enter' || event.key === ' ') { event.preventDefault(); selectMessage(item, index) }"
                >
                  {#if item.title}<h4>{item.title}</h4>{/if}
                  {#if item.text}<p>{item.text}</p>{/if}
                  {#if item.bullets && item.bullets.length}
                    <ul>
                      {#each item.bullets as bullet}<li>{bullet}</li>{/each}
                    </ul>
                  {/if}
                  {#if item.links && item.links.length}
                    <nav aria-label="Message links">
                      {#each item.links as link}
                        <a
                          href="{link.href || '#'}"
                          @click="selectLink(event, link, item, index)"
                        >{link.label}</a>
                      {/each}
                    </nav>
                  {/if}
                </div>
        </div>

              {#if showMetadata && (item.timestamp || item.status || item.actionLabel)}
                <footer class="wire-next__chat-meta" data-tone="{item.statusTone || 'muted'}">
                  {#if item.statusIcon}<span class="{item.statusIcon}" aria-hidden="true">
        </span>{/if}
                  {#if item.status}<span>{item.status}</span>{/if}
                  {#if item.timestamp}<time datetime="{item.datetime || ''}">{item.timestamp}</time>{/if}
                  {#if item.actionLabel}
                    <button type="button" @click="selectAction(event, item, index)">
                      {item.actionLabel}
                    </button>
                  {/if}
                </footer>
              {/if}
            </article>
          {/each}
        </div>
      {/if}
      <slot />
        </section>
    }
}
} }