Render inputs inline & results in a turbo frame

This commit is contained in:
Stanko K.R.
2025-08-21 14:50:51 +02:00
parent 831bc686de
commit 59a488eb9c
8 changed files with 76 additions and 93 deletions
+21 -37
View File
@@ -1,43 +1,28 @@
@layer components {
.conversation {
font-family: var(--font-sans);
padding-block-end: 6em;
}
.conversation__composer-input {
--focus-ring-size: 0;
--input-border-radius: 0;
--input-border-size: 0;
--input-padding: 0 0.5em;
.conversation__composer {
backdrop-filter: blur(24px);
display: flex;
gap: var(--inline-space);
inset: auto 0 0 0;
padding: var(--block-space);
position: absolute;
&::-webkit-search-cancel-button {
--clear-icon-size: 0.75rem;
.conversation__submit {
display: flex;
flex-direction: column;
}
}
-webkit-appearance: none;
appearance: none;
background-image: url('data:image/svg+xml;utf8,<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="m14.3 12.179a.25.25 0 0 1 0-.354l9.263-9.262a1.5 1.5 0 0 0 -2.124-2.121l-9.262 9.258a.25.25 0 0 1 -.354 0l-9.262-9.258a1.5 1.5 0 0 0 -2.122 2.121l9.261 9.262a.25.25 0 0 1 0 .354l-9.261 9.263a1.5 1.5 0 0 0 2.122 2.121l9.262-9.263a.25.25 0 0 1 .354 0l9.262 9.263a1.5 1.5 0 0 0 2.122-2.121z" fill="%23000"/></svg>');
background-repeat: no-repeat;
background-size: var(--clear-icon-size);
block-size: var(--clear-icon-size);
cursor: pointer;
inline-size: var(--clear-icon-size);
inset: 50% 1ch auto auto;
position: absolute;
translate: 0 -50%;
.conversation__header {
align-items: center;
display: flex;
inset: 0 0 auto 0;
justify-content: flex-end;
padding: var(--block-space-half);
position: absolute;
}
.conversation__input {
align-items: center;
flex-grow: 1;
.input--textarea {
--input-padding: 0.5em !important;
}
.conversation:has(.conversation__thinking-indicator--thinking) & {
animation: pulse 1.5s infinite;
pointer-events: none;
@media (prefers-color-scheme: dark) {
background-image: url('data:image/svg+xml;utf8,<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="m14.3 12.179a.25.25 0 0 1 0-.354l9.263-9.262a1.5 1.5 0 0 0 -2.124-2.121l-9.262 9.258a.25.25 0 0 1 -.354 0l-9.262-9.258a1.5 1.5 0 0 0 -2.122 2.121l9.261 9.262a.25.25 0 0 1 0 .354l-9.261 9.263a1.5 1.5 0 0 0 2.122 2.121l9.262-9.263a.25.25 0 0 1 .354 0l9.262 9.263a1.5 1.5 0 0 0 2.122-2.121z" fill="%23fff"/></svg>');
}
}
}
@@ -45,7 +30,6 @@
display: flex;
.action-text-content {
border-radius: 0.5rem;
inline-size: fit-content;
}
+26 -31
View File
@@ -1,15 +1,12 @@
import { Controller } from "@hotwired/stimulus"
import { post } from "@rails/request.js"
import { isInput } from "helpers/html_helpers"
export default class extends Controller {
static targets = [ "turboFrame" ]
static targets = [ "turboFrame", "searchInput", "askInput", "buttonsContainer" ]
static outlets = [ "dialog" ]
static values = {
searchUrl: String,
searchTurboFrameName: String,
askUrl: String,
askTurboFrameName: String
}
dialogOutletConnected(outlet, element) {
@@ -17,36 +14,34 @@ export default class extends Controller {
this.#clearTurboFrame()
}
closeModal() {
reset() {
this.dialogOutlet.close()
this.#clearTurboFrame()
this.#showItem(this.buttonsContainerTarget)
this.#hideItem(this.askInputTarget)
this.#hideItem(this.searchInputTarget)
}
showModal() {
this.dialogOutlet.open()
}
search(event) {
if (this.#shouldExecuteHotkey) {
event.preventDefault()
this.#openInTurboFrame(this.searchTurboFrameNameValue, this.searchUrlValue)
this.dialogOutlet.open()
}
this.#openInTurboFrame("search", this.searchUrlValue)
this.#showItem(this.searchInputTarget)
this.#hideItem(this.askInputTarget)
this.#hideItem(this.buttonsContainerTarget)
}
ask(event) {
if (this.#shouldExecuteHotkey) {
event.preventDefault()
this.#initializeConversation()
this.#openInTurboFrame(this.askTurboFrameNameValue, this.askUrlValue)
this.dialogOutlet.open()
}
}
this.#initializeConversation()
this.#openInTurboFrame("conversation", this.askUrlValue)
get #shouldExecuteHotkey() {
const activeElement = document.activeElement
if (activeElement) {
return !isInput(activeElement)
} else {
return true
}
this.#showItem(this.askInputTarget)
this.#hideItem(this.searchInputTarget)
this.#hideItem(this.buttonsContainerTarget)
}
#clearTurboFrame() {
@@ -64,11 +59,11 @@ export default class extends Controller {
post(this.askUrlValue)
}
#closeModalIfAlreadyOpen() {
if (this.hasDialogOutlet) {
try {
this.closeModal()
} catch {}
}
#showItem(element) {
element.removeAttribute("hidden")
}
#hideItem(element) {
element.setAttribute("hidden", "hidden")
}
}
@@ -1,5 +1,4 @@
import { Controller } from "@hotwired/stimulus"
import { isInput } from "helpers/html_helpers"
export default class extends Controller {
click(event) {
@@ -10,7 +9,7 @@ export default class extends Controller {
}
#shouldIgnore(event) {
return event.defaultPrevented || isInput(event.target)
return event.defaultPrevented || event.target.closest("input, textarea, lexical-editor")
}
get #isClickable() {
-3
View File
@@ -1,3 +0,0 @@
export function isInput(element) {
return !!element.closest("input, textarea, lexical-editor")
}
+6 -4
View File
@@ -4,9 +4,9 @@
controller: "bar",
bar_dialog_outlet: "#bar-modal",
bar_search_url_value: search_path,
bar_ask_url_value: conversation_path,
bar_ask_url_value: conversation_path
} do %>
<div class="flex justify-center gap">
<div class="flex justify-center gap" data-bar-target="buttonsContainer">
<%= tag.button \
class: "btn btn--plain",
data: {
@@ -28,9 +28,11 @@
<% end %>
</div>
<div hidden>
<div data-bar-target="searchInput" hidden>
<%= render "searches/form", query_terms: "" %>
</div>
<div data-bar-target="askInput" hidden>
<%= render "conversations/composer" %>
</div>
@@ -41,7 +43,7 @@
controller: "dialog",
dialog_target: "dialog",
dialog_modal_value: "false",
action: "keydown.esc->dialog#close" } do %>
action: "keydown.esc->bar#reset" } do %>
<%= turbo_frame_tag "fizzy-bar", data: { bar_target: "turboFrame" } %>
<% end %>
<% end %>
+13 -13
View File
@@ -1,21 +1,21 @@
<%= form_with model: Conversation::Message.new, local: true,
class: "conversation__composer",
class: "conversation__composer flex align-center",
data: {
turbo_frame: "conversation",
action: "turbo:submit-end->conversation--composer#submitEnd",
controller: "conversation--composer",
conversation__composer_conversation__messages_outlet: "#conversation-messages" } do |form| %>
<%= form.hidden_field :client_message_id, data: { conversation__composer_target: "clientMessageIdInput" } %>
<div class="conversation__input">
<%= form.text_field :content,
autofocus: "on",
placeholder: "Ask something…",
class: "input input--textarea",
data: {
action: "
keydown.enter->conversation--composer#submit
keydown.ctrl+enter->conversation--composer#submit",
conversation__composer_target: "textInput" } %>
</div>
<%= form.label :content, "Ask Fizzy", class: "font-weight-black txt-nowrap" %>
<%= form.text_field :content,
autofocus: "on",
placeholder: "Ask something…",
class: "conversation__composer-input input",
data: {
action: "
keydown.esc->bar#reset
keydown.enter->conversation--composer#submit keydown.enter->bar#showModal
keydown.ctrl+enter->conversation--composer#submit keydown.ctrl+enter->bar#showModal",
conversation__composer_target: "textInput" } %>
<% end %>
@@ -1,6 +1,6 @@
<%= turbo_frame_tag dom_id(@conversation, :messages_container) do %>
<%= tag.div \
id: dom_id(@conversation, :messages),
id: "conversation-messages",
class: "conversation__messages",
data: {
controller: "conversation--messages",
+8 -2
View File
@@ -1,5 +1,11 @@
<%= form_with url: search_path, method: :get, class: "search__form flex align-center", data: { turbo_frame: "search" } do |form| %>
<%= form.label :q, "Search Fizzy", class: "font-weight-black txt-nowrap" %>
<%= text_field_tag :q, query_terms, class: "search__form-input input", type: "search",
placeholder: "Find something in Cards and Comments…", autocomplete: "off" %>
<%= text_field_tag :q, query_terms,
class: "search__form-input input",
type: "search",
placeholder: "Find something in Cards and Comments…",
autocomplete: "off",
data: {
action: "keydown.enter->bar#showModal keydown.esc->bar#reset"
} %>
<% end %>