Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/app/components/shared/components_list.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ def components
{name: "Avatar", path: docs_avatar_path},
{name: "Badge", path: docs_badge_path},
{name: "Breadcrumb", path: docs_breadcrumb_path},
{name: "Bubble", path: docs_bubble_path},
{name: "Button", path: docs_button_path},
{name: "Calendar", path: docs_calendar_path},
{name: "Card", path: docs_card_path},
Expand Down
4 changes: 4 additions & 0 deletions docs/app/controllers/docs_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,10 @@ def breadcrumb
render Views::Docs::Breadcrumb.new
end

def bubble
render Views::Docs::Bubble.new
end

def button
render Views::Docs::Button.new
end
Expand Down
1 change: 1 addition & 0 deletions docs/app/lib/site_files.rb
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ class SiteFiles
{title: "Avatar", path: "/docs/avatar", description: "Image and fallback primitives for representing a user."},
{title: "Badge", path: "/docs/badge", description: "Small status or label element."},
{title: "Breadcrumb", path: "/docs/breadcrumb", description: "Navigation trail showing the current location in a hierarchy."},
{title: "Bubble", path: "/docs/bubble", description: "Chat bubble surface with variants, alignment, grouping, and reactions."},
{title: "Button", path: "/docs/button", description: "Button component and button-like variants."},
{title: "Calendar", path: "/docs/calendar", description: "Date field component for entering and editing dates."},
{title: "Card", path: "/docs/card", description: "Content container with header, content, and footer primitives."},
Expand Down
167 changes: 167 additions & 0 deletions docs/app/views/docs/bubble.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
# frozen_string_literal: true

class Views::Docs::Bubble < Views::Base
def view_template
component = "Bubble"

div(class: "max-w-2xl mx-auto w-full py-10 space-y-10") do
render Docs::Header.new(title: "Bubble", description: "A chat bubble surface for displaying conversational content, with variants, alignment, grouping, and reactions.")

Heading(level: 2) { "Usage" }

render Docs::VisualCodeExample.new(title: "Default", context: self) do
<<~RUBY
Bubble(align: :end) do
BubbleContent { "Hey there! what's up?" }
end
RUBY
end

render Docs::VisualCodeExample.new(title: "Conversation", context: self) do
<<~RUBY
div(class: "flex flex-col gap-8") do
Bubble(align: :end) do
BubbleContent { "Hey there! what's up?" }
end
BubbleGroup do
Bubble(variant: :muted) do
BubbleContent { "Hey! Want to see chat bubbles?" }
end
Bubble(variant: :muted) do
BubbleContent { "I can group messages, switch sides, and keep the whole thread easy to scan." }
BubbleReactions(role: "img", aria_label: "Reaction: thumbs up") do
span { "👍" }
end
end
end
Bubble(align: :end) do
BubbleContent { "Sure. Hit me with your best demo." }
end
end
RUBY
end

Heading(level: 2) { "Variants" }

render Docs::VisualCodeExample.new(title: "Variants", context: self) do
<<~RUBY
div(class: "flex flex-col gap-4 w-full") do
Bubble(variant: :default) { BubbleContent { "Default" } }
Bubble(variant: :secondary) { BubbleContent { "Secondary" } }
Bubble(variant: :muted) { BubbleContent { "Muted" } }
Bubble(variant: :tinted) { BubbleContent { "Tinted" } }
Bubble(variant: :outline) { BubbleContent { "Outline" } }
Bubble(variant: :ghost) { BubbleContent { "Ghost — unframed, full width for assistant text or markdown." } }
Bubble(variant: :destructive) { BubbleContent { "Destructive — something went wrong." } }
end
RUBY
end

Heading(level: 2) { "Alignment" }

render Docs::VisualCodeExample.new(title: "Start and end", context: self) do
<<~RUBY
div(class: "flex flex-col gap-4 w-full") do
Bubble(align: :start, variant: :muted) do
BubbleContent { "Aligned to the start (receiver)." }
end
Bubble(align: :end) do
BubbleContent { "Aligned to the end (sender)." }
end
end
RUBY
end

Heading(level: 2) { "Reactions" }

render Docs::VisualCodeExample.new(title: "Reactions", context: self) do
<<~RUBY
div(class: "flex flex-col gap-10 w-full py-6") do
Bubble(variant: :muted) do
BubbleContent { "Reactions anchor to the bubble edge." }
BubbleReactions(role: "img", aria_label: "Reactions: thumbs up, fire, eyes, and 2 more") do
span { "👍" }
span { "🔥" }
span { "👀" }
span { "+2" }
end
end
Bubble(align: :end) do
BubbleContent { "Place them on top and to the start too." }
BubbleReactions(side: :top, align: :start, role: "img", aria_label: "Reaction: heart") do
span { "❤️" }
end
end
end
RUBY
end

Heading(level: 2) { "Group" }

render Docs::VisualCodeExample.new(title: "Bubble group", context: self) do
<<~RUBY
BubbleGroup do
Bubble(variant: :muted) { BubbleContent { "First message in the group." } }
Bubble(variant: :muted) { BubbleContent { "Second one, tighter spacing." } }
Bubble(variant: :muted) { BubbleContent { "Third, all stacked together." } }
end
RUBY
end

Heading(level: 2) { "Link or button bubble" }

render Docs::VisualCodeExample.new(title: "Interactive content", context: self) do
<<~RUBY
div(class: "flex flex-col gap-4 w-full") do
Bubble(align: :end) do
BubbleContent(as: :a, href: "#") { "Tap to open the link →" }
end
Bubble(variant: :outline) do
BubbleContent(as: :button, type: "button") { "Retry sending" }
end
end
RUBY
end

Heading(level: 2) { "With Tooltip" }

render Docs::VisualCodeExample.new(title: "Reveal metadata on hover", context: self) do
<<~RUBY
Tooltip do
TooltipTrigger(class: "w-fit") do
Bubble(variant: :muted, class: "max-w-none") do
BubbleContent { "Read 9:41 AM" }
end
end
TooltipContent do
Text { "Delivered and read" }
end
end
RUBY
end

Heading(level: 2) { "With Popover" }

render Docs::VisualCodeExample.new(title: "Surface details on demand", context: self) do
<<~RUBY
Popover do
PopoverTrigger do
Bubble(variant: :destructive, class: "max-w-none") do
BubbleContent { "Message failed to send" }
end
end
PopoverContent(class: "w-64") do
Text(weight: :semibold) { "Delivery error" }
Text(size: :sm, class: "text-muted-foreground") { "The recipient's inbox is full. Try again later." }
end
end
RUBY
end

render Components::ComponentSetup::Tabs.new(component_name: component)

# components
render Docs::ComponentsTable.new(component_files(component))
end
end
end
1 change: 1 addition & 0 deletions docs/config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
get "avatar", to: "docs#avatar", as: :docs_avatar
get "badge", to: "docs#badge", as: :docs_badge
get "breadcrumb", to: "docs#breadcrumb", as: :docs_breadcrumb
get "bubble", to: "docs#bubble", as: :docs_bubble
get "button", to: "docs#button", as: :docs_button
get "card", to: "docs#card", as: :docs_card
get "carousel", to: "docs#carousel", as: :docs_carousel
Expand Down
5 changes: 5 additions & 0 deletions docs/public/llms-full.txt
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,11 @@ This file expands the curated /llms.txt map into a compact reference that can be
- URL: https://rubyui.com/docs/breadcrumb
- Summary: Navigation trail showing the current location in a hierarchy.

### Bubble

- URL: https://rubyui.com/docs/bubble
- Summary: Chat bubble surface with variants, alignment, grouping, and reactions.

### Button

- URL: https://rubyui.com/docs/button
Expand Down
1 change: 1 addition & 0 deletions docs/public/llms.txt
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ Use the core docs first for installation, theming, dark mode, and customization
- [Avatar](https://rubyui.com/docs/avatar): Image and fallback primitives for representing a user.
- [Badge](https://rubyui.com/docs/badge): Small status or label element.
- [Breadcrumb](https://rubyui.com/docs/breadcrumb): Navigation trail showing the current location in a hierarchy.
- [Bubble](https://rubyui.com/docs/bubble): Chat bubble surface with variants, alignment, grouping, and reactions.
- [Button](https://rubyui.com/docs/button): Button component and button-like variants.
- [Calendar](https://rubyui.com/docs/calendar): Date field component for entering and editing dates.
- [Card](https://rubyui.com/docs/card): Content container with header, content, and footer primitives.
Expand Down
5 changes: 5 additions & 0 deletions docs/public/sitemap.xml
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,11 @@
<changefreq>monthly</changefreq>
<priority>0.7</priority>
</url>
<url>
<loc>https://rubyui.com/docs/bubble</loc>
<changefreq>monthly</changefreq>
<priority>0.7</priority>
</url>
<url>
<loc>https://rubyui.com/docs/button</loc>
<changefreq>monthly</changefreq>
Expand Down
37 changes: 37 additions & 0 deletions gem/lib/ruby_ui/bubble/bubble.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# frozen_string_literal: true

module RubyUI
class Bubble < Base
VARIANTS = {
default: "*:data-[slot=bubble-content]:bg-primary *:data-[slot=bubble-content]:text-primary-foreground [&>[data-slot=bubble-content]:is(button,a):hover]:bg-primary/80",
secondary: "*:data-[slot=bubble-content]:bg-secondary *:data-[slot=bubble-content]:text-secondary-foreground [&>[data-slot=bubble-content]:is(button,a):hover]:bg-[color-mix(in_oklch,var(--secondary),var(--foreground)_5%)]",
muted: "*:data-[slot=bubble-content]:bg-muted [&>[data-slot=bubble-content]:is(button,a):hover]:bg-[color-mix(in_oklch,var(--muted),var(--foreground)_5%)]",
tinted: "*:data-[slot=bubble-content]:bg-[oklch(from_var(--primary)_0.93_calc(c*0.4)_h)] dark:*:data-[slot=bubble-content]:bg-[oklch(from_var(--primary)_0.3_calc(c*0.4)_h)] *:data-[slot=bubble-content]:text-foreground [&>[data-slot=bubble-content]:is(button,a):hover]:bg-[oklch(from_var(--primary)_0.88_calc(c*0.5)_h)] dark:[&>[data-slot=bubble-content]:is(button,a):hover]:bg-[oklch(from_var(--primary)_0.35_calc(c*0.5)_h)]",
outline: "*:data-[slot=bubble-content]:bg-background *:data-[slot=bubble-content]:border-border [&>[data-slot=bubble-content]:is(button,a):hover]:bg-muted [&>[data-slot=bubble-content]:is(button,a):hover]:text-foreground dark:[&>[data-slot=bubble-content]:is(button,a):hover]:bg-input/30",
ghost: "*:data-[slot=bubble-content]:rounded-none *:data-[slot=bubble-content]:bg-transparent *:data-[slot=bubble-content]:p-0 [&>[data-slot=bubble-content]:is(button,a):hover]:bg-muted [&>[data-slot=bubble-content]:is(button,a):hover]:text-foreground dark:[&>[data-slot=bubble-content]:is(button,a):hover]:bg-muted/50 border-none",
destructive: "*:data-[slot=bubble-content]:bg-destructive/10 dark:*:data-[slot=bubble-content]:bg-destructive/20 *:data-[slot=bubble-content]:text-destructive [&>[data-slot=bubble-content]:is(button,a):hover]:bg-destructive/20 dark:[&>[data-slot=bubble-content]:is(button,a):hover]:bg-destructive/30"
}

def initialize(variant: :default, align: :start, **attrs)
@variant = variant
@align = align
super(**attrs)
end

def view_template(&)
div(**attrs, &)
end

private

def default_attrs
{
data: {slot: "bubble", variant: @variant, align: @align},
class: [
"group/bubble relative flex w-fit min-w-0 flex-col gap-1 max-w-[80%] data-[align=end]:self-end data-[variant=ghost]:max-w-full",
VARIANTS[@variant]
]
}
end
end
end
23 changes: 23 additions & 0 deletions gem/lib/ruby_ui/bubble/bubble_content.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# frozen_string_literal: true

module RubyUI
class BubbleContent < Base
def initialize(as: :div, **attrs)
@as = as
super(**attrs)
end

def view_template(&)
send(@as, **attrs, &)
end

private

def default_attrs
{
data: {slot: "bubble-content"},
class: "w-fit max-w-full min-w-0 overflow-hidden wrap-break-word rounded-3xl border border-transparent px-3 py-2.5 text-sm leading-relaxed group-data-[align=end]/bubble:self-end [&:is(button,a)]:text-left [&:is(button,a)]:outline-none [&:is(button,a)]:transition-colors [&:is(button,a):focus-visible]:border-ring [&:is(button,a):focus-visible]:ring-3 [&:is(button,a):focus-visible]:ring-ring/30"
}
end
end
end
Loading