discourse/app/assets/stylesheets/common/components/docked-composer.scss
Sam 38ab1004f8
UX: Auto-resize the AI bot docked composer with content (#39837)
Adds an `@autoResize` mode to the shared `DockedComposer` so the input
grows with its content up to a viewport-bounded max height instead of
requiring a manual resize handle. The textarea path uses CSS
`field-sizing: content` (with a JS fallback for older browsers), while
the rich editor relies on a capped `max-height` with internal scroll.

The AI bot docked composer adopts the new mode and drops its custom
resize-handle styling. The `--docked-composer-max-resize-offset` custom
property is now kept in sync on viewport and window resize so the cap
tracks available space on mobile keyboards.

Internally renames `textarea` to `inputElement` since the reference can
now point at either a `<textarea>` or the ProseMirror contenteditable.

---------

Co-authored-by: Keegan George <kgeorge13@gmail.com>
Co-authored-by: discourse-patch-triage[bot] <272280883+discourse-patch-triage[bot]@users.noreply.github.com>
2026-05-11 08:49:45 +10:00

258 lines
5.9 KiB
SCSS
Vendored

.docked-composer {
--docked-composer-input-min-height: 2.5em;
--docked-composer-max-width: 100%;
--docked-composer-content-offset: 0;
--docked-composer-drag-offset: 0px;
position: sticky;
bottom: 0;
z-index: 50;
background: var(--secondary);
padding: 0.75em 0 max(env(safe-area-inset-bottom), 0.75em);
margin-top: 1em;
display: flex;
flex-direction: column;
gap: 0.5em;
// fade scrolling content into the sticky composer's solid background
&::before {
content: "";
position: absolute;
left: 0;
right: 0;
top: -2.5em;
height: 2.5em;
background: linear-gradient(to bottom, transparent, var(--secondary));
pointer-events: none;
}
html.keyboard-visible & {
padding-bottom: 0.5em;
}
&__resize-handle {
width: 100%;
max-width: var(--docked-composer-max-width);
padding-left: var(--docked-composer-content-offset);
margin: 0 auto;
box-sizing: border-box;
height: 0.5em;
cursor: ns-resize;
display: flex;
align-items: center;
justify-content: center;
touch-action: none;
user-select: none;
&::after {
content: "";
width: 2em;
height: 3px;
background: var(--primary-low);
border-radius: 3px;
transition: background 0.15s ease-in-out;
}
&:hover::after,
&:active::after,
&:focus-visible::after {
background: var(--primary-medium);
}
&:focus-visible {
outline: 2px solid var(--tertiary);
outline-offset: 2px;
}
}
&__header {
width: 100%;
max-width: var(--docked-composer-max-width);
padding-left: var(--docked-composer-content-offset);
margin: 0 auto;
box-sizing: border-box;
}
&__inner {
display: flex;
align-items: flex-end;
gap: 0.5em;
width: 100%;
max-width: var(--docked-composer-max-width);
padding-left: var(--docked-composer-content-offset);
margin: 0 auto;
box-sizing: border-box;
}
&__editor {
flex: 1 1 auto;
min-width: 0;
contain: inline-size;
position: relative;
.d-editor-container {
background: transparent;
}
.d-editor-button-bar__scroll-btn {
--fade-color: var(--d-input-bg-color);
}
.d-editor-textarea-column {
position: relative;
border: var(--d-input-border);
border-radius: var(--d-input-border-radius);
background: var(--d-input-bg-color);
transition: border-color 0.15s ease-in-out;
&:focus-within {
border-color: var(--d-input-focused-color);
outline: 2px solid var(--d-input-focused-color);
outline-offset: -2px;
}
&:has(.d-editor-input[disabled]) {
background: var(--d-input-bg-color--disabled);
}
}
// we deliberately don't show the preview in the docked composer — users
// can toggle to the rich editor via the toolbar's RTE switch for a
// live preview. `@processPreview={{false}}` on DEditor skips cooking,
// but DEditor still renders a wrapper element that we hide here.
.d-editor-preview-wrapper {
display: none;
}
.d-editor-textarea-wrapper {
border: none;
background: transparent;
box-shadow: none;
&.in-focus {
outline: none;
}
}
.d-editor-input {
border: none;
background: transparent;
max-height: 70vh;
min-height: calc(
var(--docked-composer-input-min-height) +
var(--docked-composer-drag-offset, 0px)
);
resize: none;
// reserve room for the absolutely-positioned send button
padding-right: 3.5em;
&:focus-visible {
outline: none;
}
}
}
&--auto-resize {
--docked-composer-auto-resize-max-height: min(
70vh,
calc(
var(--docked-composer-input-min-height) +
var(--docked-composer-max-resize-offset, 400px)
)
);
}
&--auto-resize &__editor {
.d-editor-input {
flex: none;
height: auto;
max-height: var(--docked-composer-auto-resize-max-height);
min-height: var(--docked-composer-input-min-height);
overflow-y: auto;
}
textarea.d-editor-input {
field-sizing: content;
}
.ProseMirror-container {
height: auto;
max-height: var(--docked-composer-auto-resize-max-height);
}
.ProseMirror.d-editor-input {
flex: none;
overflow-y: visible;
}
}
// submit button yielded into DEditor's textarea column, positioned in
// the bottom-right corner of the input box (ChatGPT-style) — icon only.
// Consumers overriding the `<:submit>` block should reuse this class so
// their buttons inherit positioning + chrome.
&__submit-btn.btn {
position: absolute;
right: 0.5em;
bottom: 0.5em;
z-index: 2;
min-width: auto;
min-height: auto;
padding: 0.35em 0.5em;
border-radius: var(--d-button-border-radius);
background: transparent !important;
.d-icon {
color: var(--tertiary) !important;
font-size: var(--font-up-1);
}
&:hover:not(:disabled),
&:focus-visible {
background: var(--primary-very-low) !important;
.d-icon {
color: var(--tertiary-hover, var(--tertiary)) !important;
}
}
&:disabled,
&[disabled] {
cursor: not-allowed;
.d-icon {
color: var(--primary-low-mid) !important;
}
}
}
&__uploads {
display: flex;
flex-wrap: wrap;
gap: 0.5em;
width: 100%;
max-width: var(--docked-composer-max-width);
padding-block: 1rem;
padding-left: var(--docked-composer-content-offset);
margin: 0 auto;
box-sizing: border-box;
}
&__upload {
display: flex;
align-items: center;
border: 1px solid var(--primary-low);
border-radius: 10em;
padding-left: 0.75em;
color: var(--primary-high);
font-size: var(--font-down-2);
&-progress {
margin-left: 0.5em;
}
&-remove:hover .d-icon,
&-cancel:hover .d-icon {
color: var(--danger);
}
}
}