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
2 changes: 2 additions & 0 deletions docs/app/components/shared/components_list.rb
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,14 @@ def components
{name: "Date Picker", path: docs_date_picker_path},
{name: "Dialog / Modal", path: docs_dialog_path},
{name: "Dropdown Menu", path: docs_dropdown_menu_path},
{name: "Empty", path: docs_empty_path},
{name: "Form", path: docs_form_path},
{name: "Hover Card", path: docs_hover_card_path},
{name: "Input", path: docs_input_path},
{name: "Link", path: docs_link_path},
{name: "Masked Input", path: masked_input_path},
{name: "Message", path: docs_message_path},
{name: "Message Scroller", path: docs_message_scroller_path},
{name: "Pagination", path: docs_pagination_path},
{name: "Popover", path: docs_popover_path},
{name: "Progress", path: docs_progress_path},
Expand Down
8 changes: 8 additions & 0 deletions docs/app/controllers/docs_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,10 @@ def dropdown_menu
render Views::Docs::DropdownMenu.new
end

def empty
render Views::Docs::Empty.new
end

def form
render Views::Docs::Form.new
end
Expand All @@ -170,6 +174,10 @@ def message
render Views::Docs::Message.new
end

def message_scroller
render Views::Docs::MessageScroller.new
end

def pagination
render Views::Docs::Pagination.new
end
Expand Down
10 changes: 8 additions & 2 deletions docs/app/javascript/controllers/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ import { application } from "./application"
import IframeThemeController from "./iframe_theme_controller"
application.register("iframe-theme", IframeThemeController)

import ToastDemoController from "./toast_demo_controller"
application.register("toast-demo", ToastDemoController)
import MessageScrollerChatController from "./message_scroller_chat_controller"
application.register("message-scroller-chat", MessageScrollerChatController)

import RubyUi__AccordionController from "./ruby_ui/accordion_controller"
application.register("ruby-ui--accordion", RubyUi__AccordionController)
Expand Down Expand Up @@ -76,6 +76,9 @@ application.register("ruby-ui--hover-card", RubyUi__HoverCardController)
import RubyUi__MaskedInputController from "./ruby_ui/masked_input_controller"
application.register("ruby-ui--masked-input", RubyUi__MaskedInputController)

import RubyUi__MessageScrollerController from "./ruby_ui/message_scroller_controller"
application.register("ruby-ui--message-scroller", RubyUi__MessageScrollerController)

import RubyUi__PopoverController from "./ruby_ui/popover_controller"
application.register("ruby-ui--popover", RubyUi__PopoverController)

Expand Down Expand Up @@ -117,3 +120,6 @@ application.register("ruby-ui--tooltip", RubyUi__TooltipController)

import SidebarMenuController from "./sidebar_menu_controller"
application.register("sidebar-menu", SidebarMenuController)

import ToastDemoController from "./toast_demo_controller"
application.register("toast-demo", ToastDemoController)
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { Controller } from "@hotwired/stimulus";

// Docs-only demo harness for the Message Scroller chat window.
//
// On submit it clones the server-rendered user/assistant <template> rows into
// the scroller content and clears the empty state. The MessageScroller
// controller observes the content and handles autoscroll/anchoring — this
// controller only produces rows, the way a real ActionCable/streaming source
// would. Connects to data-controller="message-scroller-chat".
export default class extends Controller {
static targets = ["content", "empty", "scroller", "input", "userTemplate", "assistantTemplate"];
static values = {
replies: { type: Array, default: [] },
};

connect() {
this.turn = 0;
}

send(event) {
event.preventDefault();
const text = this.hasInputTarget ? this.inputTarget.value.trim() : "";
if (!text) return;

this.reveal();
this.appendRow(this.userTemplateTarget, text);
this.inputTarget.value = "";

const reply = this.repliesValue[this.turn % this.repliesValue.length] || "Got it — thanks!";
this.turn += 1;
// Let the assistant reply land a beat later, like a streamed response.
this.replyTimer = setTimeout(() => {
this.appendRow(this.assistantTemplateTarget, reply);
}, 500);
}

reset() {
if (this.replyTimer) clearTimeout(this.replyTimer);
if (this.hasContentTarget) this.contentTarget.replaceChildren();
this.scrollerTarget?.classList.add("hidden");
this.emptyTarget?.classList.remove("hidden");
this.turn = 0;
}

reveal() {
this.emptyTarget?.classList.add("hidden");
this.scrollerTarget?.classList.remove("hidden");
}

appendRow(template, text) {
if (!template || !this.hasContentTarget) return;
const row = template.content.firstElementChild.cloneNode(true);
const content = row.querySelector("[data-slot=bubble-content]");
if (content) content.textContent = text;
this.contentTarget.appendChild(row);
}

disconnect() {
if (this.replyTimer) clearTimeout(this.replyTimer);
}
}
Loading