@@ -49,6 +49,18 @@
|
||||
to { transform: translateY(2rem); }
|
||||
}
|
||||
|
||||
@keyframes submitting {
|
||||
0% { -webkit-mask-position: 0% 0%, 50% 0%, 100% 0% }
|
||||
12.5% { -webkit-mask-position: 0% 50%, 50% 0%, 100% 0% }
|
||||
25% { -webkit-mask-position: 0% 100%, 50% 50%, 100% 0% }
|
||||
37.5% { -webkit-mask-position: 0% 100%, 50% 100%, 100% 50% }
|
||||
50% { -webkit-mask-position: 0% 100%, 50% 100%, 100% 100% }
|
||||
62.5% { -webkit-mask-position: 0% 50%, 50% 100%, 100% 100% }
|
||||
75% { -webkit-mask-position: 0% 0%, 50% 50%, 100% 100% }
|
||||
87.5% { -webkit-mask-position: 0% 0%, 50% 0%, 100% 50% }
|
||||
100% { -webkit-mask-position: 0% 0%, 50% 0%, 100% 0% }
|
||||
}
|
||||
|
||||
@keyframes success {
|
||||
0% { background-color: var(--color-border-darker); scale: 0.8; }
|
||||
33% { background-color: var(--color-border-darker); scale: 1; }
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
@layer components {
|
||||
.bar {
|
||||
--row-gap: 0.2lh;
|
||||
|
||||
background-color: var(--color-terminal-bg);
|
||||
color: var(--color-terminal-text);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
font-size: 0.9em;
|
||||
inset: auto 0 0 0;
|
||||
max-block-size: 100%;
|
||||
padding: var(--block-space) calc(var(--tray-size) + calc(var(--inline-space) * 3)) calc(var(--block-space) + env(safe-area-inset-bottom));
|
||||
place-content: center;
|
||||
position: fixed;
|
||||
z-index: var(--z-terminal);
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
border-block: 1px solid var(--color-ink-lighter);
|
||||
}
|
||||
}
|
||||
|
||||
.bar__input {
|
||||
transform: translateX(50%);
|
||||
transition: transform 350ms cubic-bezier(0.25, 1.25, 0.5, 1);
|
||||
|
||||
.bar:has(.bar__placeholder[hidden]) & {
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
|
||||
.bar__modal {
|
||||
background-color: var(--color-terminal-bg);
|
||||
block-size: 75dvh;
|
||||
border-block: 1px solid var(--color-ink-lighter);
|
||||
inline-size: 100vw;
|
||||
inset: auto 0 0 0;
|
||||
max-inline-size: 100vw;
|
||||
margin-block-end: calc(var(--footer-height) - 0.3rem);
|
||||
position: fixed;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
.bar__placeholder {
|
||||
font-weight: 500;
|
||||
opacity: 0.66;
|
||||
|
||||
.btn--plain {
|
||||
font-weight: 500;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -460,36 +460,4 @@
|
||||
margin-block-end: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.card--search {
|
||||
--card-color: var(--color-ink);
|
||||
--card-padding-block: 0.75rem;
|
||||
--card-padding-inline: 0.75rem;
|
||||
|
||||
background-color: var(--color-canvas);
|
||||
|
||||
&:not(&:hover) {
|
||||
box-shadow: 0 0 0 1px var(--color-ink-lighter);
|
||||
}
|
||||
|
||||
.card__collection {
|
||||
background-color: var(--color-canvas);
|
||||
color: var(--color-ink-medium);
|
||||
font-size: var(--text-x-small);
|
||||
font-weight: 600;
|
||||
padding-block-end: 1ch;
|
||||
}
|
||||
|
||||
.card__id {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.card__body {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.card__title {
|
||||
font-size: var(--text-medium);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,52 +1,27 @@
|
||||
@layer components {
|
||||
.conversation {
|
||||
font-family: var(--font-sans);
|
||||
padding-block-end: 6em;
|
||||
}
|
||||
|
||||
.conversation__composer {
|
||||
backdrop-filter: blur(24px);
|
||||
display: flex;
|
||||
gap: var(--inline-space);
|
||||
inset: auto 0 0 0;
|
||||
padding: var(--block-space);
|
||||
position: absolute;
|
||||
|
||||
.conversation__submit {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
.conversation__header {
|
||||
align-items: center;
|
||||
backdrop-filter: blur(24px);
|
||||
display: flex;
|
||||
inset: 0 0 auto 0;
|
||||
justify-content: space-between;
|
||||
padding: var(--block-space);
|
||||
justify-content: flex-end;
|
||||
padding: var(--block-space-half) var(--block-space);
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.conversation__input {
|
||||
align-items: center;
|
||||
flex-grow: 1;
|
||||
.conversation__composer-input.input {
|
||||
--focus-ring-size: 0;
|
||||
--input-border-color: var(--color-ink-light);
|
||||
--input-border-radius: 0;
|
||||
--input-padding: 0.1em;
|
||||
|
||||
.input--textarea {
|
||||
--input-padding: 0.5em !important;
|
||||
}
|
||||
|
||||
.conversation:has(.conversation__thinking-indicator--thinking) & {
|
||||
animation: pulse 1.5s infinite;
|
||||
pointer-events: none;
|
||||
}
|
||||
border-width: 0 0 1px;
|
||||
max-inline-size: 50ch;
|
||||
}
|
||||
|
||||
.conversation__message {
|
||||
display: flex;
|
||||
|
||||
.action-text-content {
|
||||
|
||||
border-radius: 0.5rem;
|
||||
inline-size: fit-content;
|
||||
}
|
||||
@@ -62,6 +37,19 @@
|
||||
}
|
||||
}
|
||||
|
||||
.conversation__messages {
|
||||
--gap: 4vmin;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--gap);
|
||||
margin-inline: auto;
|
||||
max-block-size: 75dvh;
|
||||
overflow-y: auto;
|
||||
overscroll-behavior: contain;
|
||||
padding: var(--block-space) calc(var(--block-space-double) + var(--btn-size));
|
||||
}
|
||||
|
||||
.conversation__message--assistant {
|
||||
justify-content: flex-start;
|
||||
|
||||
@@ -83,20 +71,32 @@
|
||||
justify-content: flex-end;
|
||||
|
||||
.action-text-content {
|
||||
background-color: var(--color-ink-lightest);
|
||||
background-color: var(--color-selected);
|
||||
margin-inline-start: 15%;
|
||||
padding: var(--block-space-half) var(--inline-space-double);
|
||||
text-align: end;
|
||||
}
|
||||
}
|
||||
|
||||
.conversation__thinking-indicator {
|
||||
inset: auto auto var(--footer-height) 50%;
|
||||
position: absolute;
|
||||
transform: translateX(-50%);
|
||||
|
||||
.spinner {
|
||||
display: none;
|
||||
|
||||
.conversation__thinking-indicator--thinking & {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.conversation__transcript {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--block-space);
|
||||
margin-inline: calc(var(--block-space-half) * -0.5);
|
||||
max-block-size: calc(100dvh - 6em - var(--block-space-double) - 2 * var(--footer-height));
|
||||
overflow-y: auto;
|
||||
padding: var(--block-space-half) var(--block-space);
|
||||
margin-inline: auto;
|
||||
max-inline-size: 67ch;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,51 +1,37 @@
|
||||
@layer components {
|
||||
.search {
|
||||
--gap: 4vmin;
|
||||
--width: 80ch;
|
||||
|
||||
display: grid;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--gap);
|
||||
max-inline-size: 960px;
|
||||
margin-inline: auto;
|
||||
|
||||
@media (min-width: 640px) {
|
||||
grid-template-columns: 2fr 1fr;
|
||||
}
|
||||
max-block-size: 100%;
|
||||
overflow-y: auto;
|
||||
overscroll-behavior: contain;
|
||||
padding: var(--block-space) calc(var(--block-space-double) + var(--btn-size));
|
||||
}
|
||||
|
||||
/* Form
|
||||
/* ------------------------------------------------------------------------ */
|
||||
|
||||
.search__form {
|
||||
--search-form-padding: 1ch;
|
||||
--search-btn-size: 2rem;
|
||||
|
||||
background-color: var(--color-canvas);
|
||||
inset-block-start: 0;
|
||||
grid-column: 1 / -1;
|
||||
margin-inline: calc(-1 * var(--search-form-padding));
|
||||
padding: var(--search-form-padding);
|
||||
position: sticky;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.search__form-icon {
|
||||
border-radius: 0;
|
||||
inset: 50% auto auto calc(var(--search-form-padding) * 2);
|
||||
opacity: 0.66;
|
||||
position: absolute;
|
||||
translate: 0 -50%;
|
||||
}
|
||||
|
||||
.search__form-input {
|
||||
padding-inline: var(--search-btn-size);
|
||||
position: relative;
|
||||
--clear-icon-size: 1em;
|
||||
--focus-ring-size: 0;
|
||||
--input-border-color: var(--color-ink-light);
|
||||
--input-border-radius: 0;
|
||||
--input-padding: 0.1em;
|
||||
|
||||
border-width: 0 0 1px;
|
||||
max-inline-size: 50ch;
|
||||
|
||||
&::-webkit-search-cancel-button {
|
||||
--clear-icon-size: 0.75rem;
|
||||
|
||||
-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"/></svg>');
|
||||
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);
|
||||
@@ -54,58 +40,44 @@
|
||||
inset: 50% 1ch auto auto;
|
||||
position: absolute;
|
||||
translate: 0 -50%;
|
||||
|
||||
@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>');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Content
|
||||
/* ------------------------------------------------------------------------ */
|
||||
|
||||
.search__header {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
inset: 0 0 auto 0;
|
||||
justify-content: flex-end;
|
||||
padding: var(--block-space-half) var(--block-space);
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.search__list {
|
||||
display: grid;
|
||||
gap: 1ch;
|
||||
gap: var(--block-space);
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
margin: 0 auto;
|
||||
max-inline-size: 80ch;
|
||||
padding: 0;
|
||||
text-align: start;
|
||||
}
|
||||
|
||||
.search__results {
|
||||
/* Full-width when there isn't a search history */
|
||||
@media (min-width: 640px) {
|
||||
&:not(:has(+ .search__history li)) {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.search__empty {
|
||||
margin-block: 3em;
|
||||
opacity: 0.66;
|
||||
border: 2px dashed var(--color-ink-light);
|
||||
border-radius: 1ch;
|
||||
padding: calc(var(--gap) / 2);
|
||||
}
|
||||
|
||||
.search__history {
|
||||
align-self: start;
|
||||
background-color: var(--color-ink-lightest);
|
||||
border-radius: 0.5em;
|
||||
display: none;
|
||||
flex-direction: column;
|
||||
gap: 1ch;
|
||||
padding: 1em;
|
||||
text-align: start;
|
||||
|
||||
/* Only show on larger screens when there's a search history to show */
|
||||
@media (min-width: 640px) {
|
||||
&:has(li) {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.search__excerpt {
|
||||
color: var(--color-ink);
|
||||
font-size: var(--text-small);
|
||||
margin-block-start: 0.5ch;
|
||||
}
|
||||
|
||||
.search__excerpt--comment {
|
||||
@@ -113,15 +85,28 @@
|
||||
--comment-avatar-size: 32px;
|
||||
--padding: 1ch;
|
||||
|
||||
align-items: center;
|
||||
background-color: var(--color-ink-lightest);
|
||||
border-radius: 1ch;
|
||||
display: flex;
|
||||
gap: 1ch;
|
||||
margin-left: calc(var(--comment-avatar-size) / 2);
|
||||
margin-inline-start: calc(var(--comment-avatar-size) / 2);
|
||||
padding-block: 0.5ch;
|
||||
|
||||
.avatar {
|
||||
margin-left: calc(-0.5 * var(--comment-avatar-size));
|
||||
margin-inline-start: calc(-0.5 * var(--comment-avatar-size));
|
||||
}
|
||||
}
|
||||
|
||||
.search__result {
|
||||
color: var(--color-link);
|
||||
|
||||
&:not(&:hover) {
|
||||
box-shadow: 0 0 0 1px var(--color-ink-lighter);
|
||||
}
|
||||
}
|
||||
|
||||
.search__title {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,260 +0,0 @@
|
||||
@layer components {
|
||||
.terminal {
|
||||
--row-gap: 0.2lh;
|
||||
|
||||
background-color: var(--color-terminal-bg);
|
||||
color: var(--color-terminal-text);
|
||||
font-family: var(--font-mono);
|
||||
font-size: 0.9em;
|
||||
inset: auto 0 0 0;
|
||||
max-block-size: 100%;
|
||||
padding: var(--block-space) calc(var(--tray-size) + calc(var(--inline-space) * 3)) calc(var(--block-space) + env(safe-area-inset-bottom));
|
||||
position: fixed;
|
||||
z-index: var(--z-terminal);
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
border-block-start: 1px solid var(--color-ink-lighter);
|
||||
}
|
||||
}
|
||||
|
||||
.terminal__button {
|
||||
--btn-background: var(--color-terminal-bg);
|
||||
--btn-color: var(--color-terminal-text);
|
||||
--btn-border-radius: 0;
|
||||
--btn-border-color: var(--color-terminal-text);
|
||||
--btn-padding: 0.1em 0.7em;
|
||||
--btn-border-size: 1px;
|
||||
|
||||
font-family: var(--font-mono);
|
||||
font-size: var(--text-x-small);
|
||||
white-space: nowrap;
|
||||
|
||||
&:where(:not(:active)):focus-visible {
|
||||
outline: 2px solid var(--color-terminal-text);
|
||||
}
|
||||
}
|
||||
|
||||
.terminal__command {
|
||||
--btn-background: transparent;
|
||||
--btn-color: var(--color-terminal-text);
|
||||
|
||||
font-weight: normal;
|
||||
overflow: hidden;
|
||||
transition: unset;
|
||||
|
||||
span:first-letter {
|
||||
text-transform: capitalize;
|
||||
}
|
||||
}
|
||||
|
||||
.terminal__container {
|
||||
max-block-size: calc(100vh - var(--btn-size) - var(--block-space));
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.terminal__help {
|
||||
background: var(--color-terminal-bg);
|
||||
border-radius: 1em;
|
||||
display: none;
|
||||
|
||||
inline-size: auto;
|
||||
inset: 50% auto auto 50%;
|
||||
max-inline-size: 90vw;
|
||||
|
||||
padding: calc(var(--block-space) * 1.5) var(--block-space-double);
|
||||
position: fixed;
|
||||
transform: translate(-50%, -50%);
|
||||
|
||||
.terminal--showing-help &,
|
||||
.terminal--open .terminal__menu:empty ~ & {
|
||||
display: block;
|
||||
}
|
||||
|
||||
h2 {
|
||||
border-block-end: 1px dashed;
|
||||
font-size: var(--text-normal);
|
||||
font-weight: normal;
|
||||
margin-block: 0 0.5lh;
|
||||
padding-block-end: 0.5lh;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: var(--text-small);
|
||||
margin-block: 0;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
dl {
|
||||
display: grid;
|
||||
grid-template-columns: min-content 1fr;
|
||||
gap: var(--inline-space);
|
||||
font-size: var(--text-small);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
dt {
|
||||
grid-column: 1;
|
||||
margin-inline-start: var(--inline-space-double);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
dd {
|
||||
grid-column: 2;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
.terminal__input {
|
||||
--input-border-color: transparent;
|
||||
--input-border-radius: 0.3em;
|
||||
--input-color: var(--color-terminal-text);
|
||||
|
||||
accent-color: var(--color-terminal-text);
|
||||
caret-color: var(--color-terminal-text);
|
||||
font-family: inherit;
|
||||
font-weight: 500;
|
||||
text-align: start;
|
||||
|
||||
&.lexical-editor--empty {
|
||||
overflow: hidden;
|
||||
text-align: center;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
|
||||
&:focus-within {
|
||||
text-align: start;
|
||||
|
||||
.lexical-editor__content::before {
|
||||
inset-inline-start: 0;
|
||||
translate: 0 -50%;
|
||||
}
|
||||
}
|
||||
|
||||
.lexical-editor__content::before {
|
||||
color: var(--color-ink);
|
||||
inset: 50% auto auto 50%;
|
||||
opacity: 0.5;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
transition: 200ms var(--ease-out-expo);
|
||||
transition-property: inset-inline-start, translate;
|
||||
translate: -50% -50%;
|
||||
white-space: nowrap;
|
||||
|
||||
@starting-style {
|
||||
inset: 0 0 0 50%;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.terminal--busy:is(:not(.terminal--confirmation)) & {
|
||||
animation: pulse 1.5s infinite;
|
||||
}
|
||||
|
||||
.terminal--confirmation & {
|
||||
color: var(--color-terminal-text-light);
|
||||
}
|
||||
|
||||
.terminal--error & {
|
||||
accent-color: var(--color-negative);
|
||||
caret-color: var(--color-negative);
|
||||
color: var(--color-negative);
|
||||
}
|
||||
|
||||
.lexical-editor__content {
|
||||
min-block-size: initial;
|
||||
margin: 0;
|
||||
|
||||
b, strong, i, em {
|
||||
font-weight: inherit;
|
||||
font-style: inherit;
|
||||
}
|
||||
|
||||
ul, li {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.terminal__menu {
|
||||
display: none;
|
||||
list-style: none;
|
||||
|
||||
.terminal--open & {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.terminal--showing-help & {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.terminal__modal {
|
||||
--panel-border-radius: 1.5em;
|
||||
--panel-padding: var(--block-space-half);
|
||||
|
||||
max-block-size: calc(100dvh - var(--block-space-double) - 2 * var(--footer-height));
|
||||
overflow: clip;
|
||||
}
|
||||
|
||||
.terminal__item {
|
||||
&[aria-selected] {
|
||||
--btn-color: var(--color-terminal-bg);
|
||||
|
||||
background: var(--color-terminal-text);
|
||||
color: var(--color-terminal-bg);
|
||||
outline: 2px solid var(--color-terminal-text);
|
||||
|
||||
.terminal__command {
|
||||
color: var(--color-terminal-bg);
|
||||
}
|
||||
}
|
||||
|
||||
@media (any-hover: hover) {
|
||||
&:where(:not(:active):hover) {
|
||||
background: color-mix(in srgb, var(--color-terminal-text) 20%, var(--color-terminal-bg));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.terminal__output {
|
||||
display: none;
|
||||
overflow-y: auto;
|
||||
padding-inline: var(--inline-space);
|
||||
|
||||
.terminal--confirmation & {
|
||||
color: var(--color-terminal-text-light);
|
||||
}
|
||||
|
||||
.terminal--error & {
|
||||
color: var(--color-negative);
|
||||
}
|
||||
|
||||
.terminal--showing-output & {
|
||||
display: block;
|
||||
}
|
||||
|
||||
p {
|
||||
+ ul {
|
||||
margin-block-start: 0;
|
||||
}
|
||||
|
||||
&:has(+ ul) {
|
||||
margin-block-end: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Lexical prompt insertions
|
||||
/* ------------------------------------------------------------------------ */
|
||||
|
||||
.terminal {
|
||||
action-text-attachment:not(.node--selected) {
|
||||
--attachment-bg-color: oklch(var(--lch-white) / 15%);
|
||||
--attachment-text-color: var(--color-terminal-text);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,7 @@
|
||||
inset-block: auto env(safe-area-inset-bottom);
|
||||
inline-size: var(--tray-size);
|
||||
position: fixed;
|
||||
transition: inset var(--tray-duration) var(--tray-timing-function);
|
||||
z-index: var(--z-tray);
|
||||
|
||||
/* Make the dialog, expander, and actions inhabit the same space */
|
||||
@@ -330,6 +331,10 @@
|
||||
.tray--pins {
|
||||
inset-inline: var(--tray-margin) auto;
|
||||
|
||||
#footer:has(.bar__placeholder[hidden]) & {
|
||||
inset-inline-start: -100%;
|
||||
}
|
||||
|
||||
.tray__item {
|
||||
--tray-item-z: calc(10 - var(--tray-item-index));
|
||||
|
||||
@@ -464,6 +469,10 @@
|
||||
.tray--notifications {
|
||||
inset-inline: auto var(--tray-margin);
|
||||
|
||||
#footer:has(.bar__placeholder[hidden]) & {
|
||||
inset-inline-end: -100%;
|
||||
}
|
||||
|
||||
.tray__item,
|
||||
[data-navigable-list-target~=item] {
|
||||
[open] &[aria-selected] {
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
class CommandsController < ApplicationController
|
||||
def create
|
||||
command = parse_command(params[:command])
|
||||
result = command.execute
|
||||
respond_with_execution_result(result)
|
||||
end
|
||||
|
||||
private
|
||||
def parse_command(string)
|
||||
command_parser.parse(string)
|
||||
end
|
||||
|
||||
def command_parser
|
||||
@command_parser ||= Command::Parser.new(user: Current.user, script_name: request.script_name)
|
||||
end
|
||||
|
||||
def respond_with_execution_result(result)
|
||||
# This saves unnecessary back and forth between server and client (e.g: to redirect)-
|
||||
result = result.is_a?(Array) && result.one? ? result.first : result
|
||||
|
||||
case result
|
||||
when Command::Result::Redirection
|
||||
redirect_to result.url
|
||||
when Command::Result::ShowModal
|
||||
respond_with_turbo_frame_modal(result.turbo_frame, result.url)
|
||||
else
|
||||
redirect_back_or_to root_path
|
||||
end
|
||||
end
|
||||
|
||||
def respond_with_turbo_frame_modal(turbo_frame, url)
|
||||
render json: { turbo_frame: turbo_frame, url: url }, status: :accepted
|
||||
end
|
||||
end
|
||||
@@ -1,4 +1,8 @@
|
||||
class ConversationsController < ApplicationController
|
||||
def create
|
||||
Current.user.start_or_continue_conversation
|
||||
end
|
||||
|
||||
def show
|
||||
@conversation = Current.user.conversation
|
||||
end
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
import { Controller } from "@hotwired/stimulus"
|
||||
import { post } from "@rails/request.js"
|
||||
|
||||
export default class extends Controller {
|
||||
static targets = [ "turboFrame", "searchInput", "askInput", "buttonsContainer" ]
|
||||
static outlets = [ "dialog" ]
|
||||
static values = {
|
||||
searchUrl: String,
|
||||
askUrl: String,
|
||||
}
|
||||
|
||||
dialogOutletConnected(outlet, element) {
|
||||
outlet.close()
|
||||
this.#clearTurboFrame()
|
||||
}
|
||||
|
||||
reset() {
|
||||
this.dialogOutlet.close()
|
||||
this.#clearTurboFrame()
|
||||
|
||||
this.#showItem(this.buttonsContainerTarget)
|
||||
this.#hideItem(this.askInputTarget)
|
||||
this.#hideItem(this.searchInputTarget)
|
||||
}
|
||||
|
||||
showModalAndSubmit(event) {
|
||||
event.preventDefault()
|
||||
|
||||
const form = event.target.closest("form")
|
||||
|
||||
this.showModal()
|
||||
form.requestSubmit()
|
||||
}
|
||||
|
||||
showModal() {
|
||||
this.dialogOutlet.open()
|
||||
}
|
||||
|
||||
search(event) {
|
||||
this.#openInTurboFrame(this.searchUrlValue)
|
||||
|
||||
this.#showItem(this.searchInputTarget)
|
||||
this.#hideItem(this.askInputTarget)
|
||||
this.#hideItem(this.buttonsContainerTarget)
|
||||
}
|
||||
|
||||
ask(event) {
|
||||
this.#initializeConversation()
|
||||
this.#openInTurboFrame(this.askUrlValue)
|
||||
|
||||
this.#showItem(this.askInputTarget)
|
||||
this.#hideItem(this.searchInputTarget)
|
||||
this.#hideItem(this.buttonsContainerTarget)
|
||||
}
|
||||
|
||||
#clearTurboFrame() {
|
||||
this.turboFrameTarget.removeAttribute("src")
|
||||
this.turboFrameTarget.innerHtml = ""
|
||||
}
|
||||
|
||||
#openInTurboFrame(url) {
|
||||
this.turboFrameTarget.src = url
|
||||
}
|
||||
|
||||
#initializeConversation() {
|
||||
post(this.askUrlValue)
|
||||
}
|
||||
|
||||
#showItem(element) {
|
||||
element.removeAttribute("hidden")
|
||||
|
||||
const autofocusElement = element.querySelector("[autofocus]")
|
||||
autofocusElement?.focus()
|
||||
}
|
||||
|
||||
#hideItem(element) {
|
||||
element.setAttribute("hidden", "hidden")
|
||||
}
|
||||
}
|
||||
@@ -21,7 +21,7 @@ export default class extends Controller {
|
||||
}
|
||||
}
|
||||
|
||||
submitEnd(even) {
|
||||
submitEnd(event) {
|
||||
this.#submittingMessage = false
|
||||
|
||||
if (event.detail.success) {
|
||||
|
||||
@@ -34,7 +34,7 @@ export default class extends Controller {
|
||||
}
|
||||
|
||||
scrollToBottom() {
|
||||
this.messagesTarget.scrollTop = this.messagesTarget.scrollHeight
|
||||
this.element.scrollTop = this.element.scrollHeight
|
||||
}
|
||||
|
||||
beforeStreamRender(event) {
|
||||
|
||||
@@ -1,91 +0,0 @@
|
||||
import { Controller } from "@hotwired/stimulus"
|
||||
import { HttpStatus } from "helpers/http_helpers"
|
||||
import { isMultiLineString } from "helpers/text_helpers";
|
||||
import { marked } from "marked"
|
||||
import { nextFrame } from "helpers/timing_helpers";
|
||||
|
||||
export default class extends Controller {
|
||||
static targets = [ "input", "form", "modalTurboFrame" ]
|
||||
static classes = [ "error", "busy" ]
|
||||
static outlets = [ "dialog" ]
|
||||
|
||||
// Actions
|
||||
|
||||
async focus() {
|
||||
await nextFrame()
|
||||
|
||||
this.inputTarget.focus()
|
||||
this.inputTarget.selection.placeCursorAtTheEnd()
|
||||
}
|
||||
|
||||
executeCommand(event) {
|
||||
if (!this.inputTarget.value.trim()) {
|
||||
event.preventDefault()
|
||||
}
|
||||
}
|
||||
|
||||
submitCommand({ target }) {
|
||||
this.#submitCommand()
|
||||
}
|
||||
|
||||
handleCommandResponse(event) {
|
||||
const response = event.detail.fetchResponse?.response
|
||||
|
||||
if (event.detail.success) {
|
||||
this.#handleSuccessResponse(response)
|
||||
} else if (response) {
|
||||
this.#handleErrorResponse(response)
|
||||
}
|
||||
}
|
||||
|
||||
hideError() {
|
||||
this.element.classList.remove(this.errorClass)
|
||||
}
|
||||
|
||||
commandSubmitted() {
|
||||
this.element.classList.add(this.busyClass)
|
||||
}
|
||||
|
||||
#reset(inputValue = "") {
|
||||
this.inputTarget.value = inputValue
|
||||
|
||||
this.element.classList.remove(this.errorClass)
|
||||
this.element.classList.remove(this.busyClass)
|
||||
}
|
||||
|
||||
#handleSuccessResponse(response) {
|
||||
if (response.headers.get("Content-Type")?.includes("application/json")) {
|
||||
response.json().then((responseJson) => {
|
||||
this.#handleJsonResponse(responseJson)
|
||||
})
|
||||
}
|
||||
this.#reset()
|
||||
}
|
||||
|
||||
async #handleErrorResponse(response) {
|
||||
this.element.classList.add(this.errorClass)
|
||||
}
|
||||
|
||||
#handleJsonResponse(responseJson) {
|
||||
const { redirect_to, turbo_frame, url } = responseJson
|
||||
|
||||
if (redirect_to) {
|
||||
Turbo.visit(redirect_to)
|
||||
}
|
||||
|
||||
if (turbo_frame && url) {
|
||||
this.#showTurboFrameModal(turbo_frame, url)
|
||||
}
|
||||
}
|
||||
|
||||
#submitCommand() {
|
||||
this.formTarget.requestSubmit()
|
||||
}
|
||||
|
||||
#showTurboFrameModal(name, url) {
|
||||
this.inputTarget.blur()
|
||||
this.modalTurboFrameTarget.id = name
|
||||
this.modalTurboFrameTarget.src = url
|
||||
this.dialogOutlet.open()
|
||||
}
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
class Command
|
||||
include Rails.application.routes.url_helpers
|
||||
|
||||
def execute
|
||||
raise NotImplementedError
|
||||
end
|
||||
|
||||
private
|
||||
def redirect_to(...)
|
||||
Command::Result::Redirection.new(...)
|
||||
end
|
||||
end
|
||||
@@ -1,14 +0,0 @@
|
||||
class Command::Ask < Command
|
||||
attr_reader :question
|
||||
|
||||
def initialize(question)
|
||||
@question = question
|
||||
end
|
||||
|
||||
def execute
|
||||
conversation = Conversation.create_or_find_by(user: Current.user)
|
||||
conversation.ask(question) if question.present?
|
||||
|
||||
Command::Result::ShowModal.new(turbo_frame: "conversation", url: conversation_path)
|
||||
end
|
||||
end
|
||||
@@ -1,11 +0,0 @@
|
||||
class Command::GoToCard < Command
|
||||
attr_reader :card
|
||||
|
||||
def initialize(card)
|
||||
@card = card
|
||||
end
|
||||
|
||||
def execute
|
||||
redirect_to card
|
||||
end
|
||||
end
|
||||
@@ -1,51 +0,0 @@
|
||||
class Command::Parser
|
||||
attr_reader :user, :script_name
|
||||
|
||||
def initialize(user: user, script_name:)
|
||||
@user = user
|
||||
@script_name = script_name
|
||||
end
|
||||
|
||||
def parse(string)
|
||||
parse_command(string).tap do |command|
|
||||
command.default_url_options[:script_name] = script_name
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
def parse_command(string)
|
||||
parse_rich_text_command as_plain_text_with_attachable_references(string)
|
||||
end
|
||||
|
||||
def as_plain_text_with_attachable_references(string)
|
||||
ActionText::Content.new(string).render_attachments(&:to_gid).fragment.to_plain_text
|
||||
end
|
||||
|
||||
def parse_rich_text_command(string)
|
||||
command_name, *command_arguments = string.strip.split(" ")
|
||||
combined_arguments = command_arguments.join(" ")
|
||||
|
||||
case command_name
|
||||
when "/ask"
|
||||
Command::Ask.new(combined_arguments)
|
||||
else
|
||||
parse_free_string(string)
|
||||
end
|
||||
end
|
||||
|
||||
def parse_free_string(string)
|
||||
if card = single_card_from(string)
|
||||
Command::GoToCard.new(card)
|
||||
else
|
||||
parse_with_fallback(string)
|
||||
end
|
||||
end
|
||||
|
||||
def single_card_from(string)
|
||||
user.accessible_cards.find_by_id(string)
|
||||
end
|
||||
|
||||
def parse_with_fallback(string)
|
||||
Command::Search.new(string)
|
||||
end
|
||||
end
|
||||
@@ -1,11 +0,0 @@
|
||||
class Command::Result::Redirection
|
||||
attr_reader :url
|
||||
|
||||
def initialize(url)
|
||||
@url = url
|
||||
end
|
||||
|
||||
def as_json
|
||||
{ redirect_to: url }
|
||||
end
|
||||
end
|
||||
@@ -1,12 +0,0 @@
|
||||
class Command::Result::ShowModal
|
||||
attr_reader :url, :turbo_frame
|
||||
|
||||
def initialize(url:, turbo_frame:)
|
||||
@url = url
|
||||
@turbo_frame = turbo_frame
|
||||
end
|
||||
|
||||
def as_json
|
||||
{ turbo_frame: turbo_frame, url: url }
|
||||
end
|
||||
end
|
||||
@@ -1,12 +0,0 @@
|
||||
class Command::Search < Command
|
||||
attr_reader :terms
|
||||
|
||||
def initialize(terms)
|
||||
@terms = terms
|
||||
end
|
||||
|
||||
|
||||
def execute
|
||||
redirect_to search_path(q: terms)
|
||||
end
|
||||
end
|
||||
@@ -3,7 +3,7 @@ module Conversation::Broadcastable
|
||||
|
||||
def broadcast_state_change
|
||||
broadcast_replace_to user, :conversation,
|
||||
target: [ self, :thinking_indicator ],
|
||||
partial: "conversations/show/thinking_indicator"
|
||||
target: "conversation-thinking-indicator",
|
||||
partial: "conversations/composer/thinking_indicator"
|
||||
end
|
||||
end
|
||||
|
||||
+1
-3
@@ -1,6 +1,6 @@
|
||||
class User < ApplicationRecord
|
||||
include Accessor, Attachable, Assignee, Mentionable, Named, Role, Searcher,
|
||||
SignalUser, Staff, Transferable
|
||||
SignalUser, Staff, Transferable, Conversational
|
||||
include Timelined # Depends on Accessor
|
||||
|
||||
has_one_attached :avatar
|
||||
@@ -20,8 +20,6 @@ class User < ApplicationRecord
|
||||
has_many :push_subscriptions, class_name: "Push::Subscription", dependent: :delete_all
|
||||
has_many :period_activity_summaries, dependent: :destroy
|
||||
|
||||
has_one :conversation, dependent: :destroy
|
||||
|
||||
normalizes :email_address, with: ->(value) { value.strip.downcase }
|
||||
|
||||
def deactivate
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
module User::Conversational
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
has_one :conversation, dependent: :destroy
|
||||
end
|
||||
|
||||
def start_or_continue_conversation
|
||||
conversation || create_conversation!
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,48 @@
|
||||
<%= tag.div \
|
||||
class: "bar full-width",
|
||||
data: {
|
||||
controller: "bar",
|
||||
bar_dialog_outlet: "#bar-modal",
|
||||
bar_search_url_value: search_path,
|
||||
bar_ask_url_value: conversation_path
|
||||
} do %>
|
||||
<div class="flex justify-center gap bar__placeholder" data-bar-target="buttonsContainer">
|
||||
<%= tag.button \
|
||||
class: "btn btn--plain",
|
||||
data: {
|
||||
controller: "hotkey",
|
||||
action: "
|
||||
bar#search
|
||||
keydown.meta+k@document->hotkey#click keydown.ctrl+k@document->hotkey#click" } do %>
|
||||
<span>Search [<%= hotkey_label(["ctrl", "K"]) %>]</span>
|
||||
<% end %>
|
||||
<span>or</span>
|
||||
<%= tag.button \
|
||||
class: "btn btn--plain",
|
||||
data: {
|
||||
controller: "hotkey",
|
||||
action: "
|
||||
bar#ask
|
||||
keydown.meta+a@document->hotkey#click keydown.ctrl+a@document->hotkey#click" } do %>
|
||||
<span>Ask [<%= hotkey_label(["ctrl", "A"]) %>]</span>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<div class="bar__input" data-bar-target="searchInput" hidden>
|
||||
<%= render "searches/form", query_terms: "" %>
|
||||
</div>
|
||||
|
||||
<div class="bar__input" data-bar-target="askInput" hidden>
|
||||
<%= render "conversations/composer" %>
|
||||
</div>
|
||||
|
||||
<%= tag.dialog \
|
||||
id: "bar-modal",
|
||||
class: "bar__modal",
|
||||
data: {
|
||||
controller: "dialog",
|
||||
dialog_target: "dialog",
|
||||
action: "keydown.esc->bar#reset:stop" } do %>
|
||||
<%= turbo_frame_tag "bar-content", data: { bar_target: "turboFrame" } %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
@@ -1,30 +0,0 @@
|
||||
<%= form_tag commands_path,
|
||||
id: "commands_form",
|
||||
class: [ "flex align-center gap-half" ],
|
||||
data: {
|
||||
controller: "form",
|
||||
terminal_target: "form",
|
||||
action: "
|
||||
keydown.enter->terminal#submitCommand
|
||||
turbo:submit-start->terminal#commandSubmitted
|
||||
turbo:submit-end->terminal#focus
|
||||
keydown.meta+k@document->terminal#focus:prevent
|
||||
keydown.ctrl+k@document->terminal#focus:prevent
|
||||
turbo:submit-end->terminal#handleCommandResponse
|
||||
"
|
||||
} do %>
|
||||
<%= rich_textarea_tag "command", nil,
|
||||
toolbar: false,
|
||||
class: "terminal__input input hide-focus-ring fill-transparent unpad",
|
||||
"single-line": true,
|
||||
id: "command",
|
||||
attachments: false,
|
||||
data: {
|
||||
terminal_target: "input"
|
||||
},
|
||||
placeholder: platform.desktop? ? "Press #{ hotkey_label(["ctrl", "K"]) } to search or type /commands…" : "Search or type /commands…",
|
||||
spellcheck: "false" do %>
|
||||
<%= global_mentions_prompt %>
|
||||
<%= tags_prompt %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
@@ -1,37 +0,0 @@
|
||||
<section class="terminal__help shadow" aria-labelledby="help-menu-title">
|
||||
<h2 id="help-menu-title"><strong>Fizzy do > Help</strong></h2>
|
||||
<dl>
|
||||
<h3>Search</h3>
|
||||
<dt>Type keywords to search cards and comments</dt>
|
||||
<dd>"CSS bug"</dd>
|
||||
|
||||
<h3>Navigate</h3>
|
||||
<dt>Type a card ID to go to it</dt>
|
||||
<dd>"137"</dd>
|
||||
<dt>Type a name to see someone’s profile</dt>
|
||||
<dd>"@jason"</dd>
|
||||
|
||||
<h3>Filter</h3>
|
||||
<dt>Type card IDs to isolate them</dt>
|
||||
<dd>"131, 137, 39"</dd>
|
||||
<dt>Type a tag to see just tagged cards</dt>
|
||||
<dd>"#design"</dd>
|
||||
|
||||
<h3>Commands</h3>
|
||||
<dt>Type / to issue commands</dt>
|
||||
<dd>"/move to Doing"</dd>
|
||||
|
||||
<dt>Type @ to match people</dt>
|
||||
<dd>"/assign to @jason"</dd>
|
||||
|
||||
<dt>Type # to match tags</dt>
|
||||
<dd>"/tag this #design"</dd>
|
||||
|
||||
<h3>Tips</h3>
|
||||
<dt>Combine for powerful actions</dt>
|
||||
<dd>"/move to Doing and /assign to @jason"</dd>
|
||||
|
||||
<dt>Or use natural language</dt>
|
||||
<dd>"Show cards added by Jason this month"</dd>
|
||||
</dl>
|
||||
</section>
|
||||
@@ -1,27 +0,0 @@
|
||||
<%= tag.div \
|
||||
id: "command-terminal",
|
||||
class: "terminal full-width flex flex-column justify-end",
|
||||
data: {
|
||||
controller: "terminal",
|
||||
terminal_dialog_outlet: "#terminal-modal",
|
||||
terminal_error_class: "terminal--error",
|
||||
terminal_confirmation_class: "terminal--confirmation",
|
||||
terminal_help_class: "terminal--showing-help",
|
||||
terminal_output_class: "terminal--showing-output",
|
||||
terminal_busy_class: "terminal--busy",
|
||||
toggle_class_toggle_class: "terminal--open"
|
||||
} do %>
|
||||
<%= tag.dialog \
|
||||
id: "terminal-modal",
|
||||
class: "terminal__modal panel shadow",
|
||||
data: {
|
||||
controller: "dialog",
|
||||
dialog_target: "dialog",
|
||||
dialog_modal_value: "true",
|
||||
action: "keydown.esc->dialog#close click@document->dialog#closeOnClickOutside" } do %>
|
||||
<%= turbo_frame_tag "terminal-turbo-frame", refresh: :morph, data: { terminal_target: "modalTurboFrame" }, target: "_top" %>
|
||||
<% end %>
|
||||
|
||||
<%= render "commands/form" %>
|
||||
<% end %>
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
<%= form_with model: Conversation::Message.new, local: true,
|
||||
class: "conversation__composer flex align-center justify-center gap-half",
|
||||
data: {
|
||||
turbo_frame: "conversation",
|
||||
action: "turbo:submit-end->conversation--composer#submitEnd",
|
||||
controller: "conversation--composer",
|
||||
conversation__composer_conversation__messages_outlet: "#conversation-messages" } do |form| %>
|
||||
<%= render "conversations/composer/thinking_indicator", conversation: nil %>
|
||||
|
||||
<%= form.hidden_field :client_message_id, data: { conversation__composer_target: "clientMessageIdInput" } %>
|
||||
<%= form.label :content, "Ask Fizzy", class: "font-weight-black txt-nowrap" %>
|
||||
<%= form.text_field :content,
|
||||
autofocus: true,
|
||||
autocomplete: "off",
|
||||
placeholder: "Ask a question…",
|
||||
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 %>
|
||||
@@ -0,0 +1,4 @@
|
||||
<%= tag.div id: "conversation-thinking-indicator",
|
||||
class: "conversation__thinking-indicator conversation__thinking-indicator--#{conversation&.state || :unknown}" do %>
|
||||
<div class="spinner"></div>
|
||||
<% 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",
|
||||
|
||||
@@ -1,18 +1,15 @@
|
||||
<%= turbo_frame_tag "conversation", target: "_top" do %>
|
||||
<%= turbo_frame_tag "bar-content" do %>
|
||||
<%= turbo_stream_from Current.user, :conversation %>
|
||||
|
||||
<div class="conversation">
|
||||
<div class="conversation__header">
|
||||
<span class="txt-medium font-weight-black margin-inline-start-half">Fizzy /Ask</span>
|
||||
<form method="dialog">
|
||||
<button class="btn txt-x-small">
|
||||
<%= icon_tag "remove-med" %>
|
||||
<span class="for-screen-reader">Close</span>
|
||||
</button>
|
||||
</form>
|
||||
<button class="btn txt-x-small" data-action="bar#reset">
|
||||
<%= icon_tag "remove-med" %>
|
||||
<span class="for-screen-reader">Close</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<%= turbo_frame_tag dom_id(@conversation, :messages_container), src: conversation_messages_path, target: "_top" %>
|
||||
<%= render partial: "conversations/show/thinking_indicator", locals: { conversation: @conversation } %>
|
||||
<%= render partial: "conversations/show/composer", locals: { conversation: @conversation } %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
<%= form_with model: Conversation::Message.new, local: true,
|
||||
id: dom_id(conversation, :composer),
|
||||
class: "conversation__composer",
|
||||
data: {
|
||||
action: "turbo:submit-end->conversation--composer#submitEnd",
|
||||
controller: "conversation--composer",
|
||||
conversation__composer_conversation__messages_outlet: "##{dom_id(conversation, :messages)}" } do |form| %>
|
||||
|
||||
<%= form.hidden_field :client_message_id, data: { conversation__composer_target: "clientMessageIdInput" } %>
|
||||
|
||||
<div class="conversation__input">
|
||||
<%= form.text_area :content, autofocus: "on", placeholder: "Ask something…", rows: 1,
|
||||
class: "input input--textarea", data: { action: "keydown.enter->conversation--composer#submit keydown.ctrl+enter->conversation--composer#submit", conversation__composer_target: "textInput" } %>
|
||||
</div>
|
||||
|
||||
<div class="conversation__submit">
|
||||
<%= form.button type: "submit", class: "btn btn--link txt-small",
|
||||
data: { conversation__composer_target: "submitButton", action: "click->conversation--composer#submit"} do %>
|
||||
<%= icon_tag "arrow-up" %>
|
||||
<span class="for-screen-reader">Send message</span>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
@@ -1,2 +0,0 @@
|
||||
<%= tag.div id: dom_id(conversation, :thinking_indicator),
|
||||
class: "conversation__thinking-indicator conversation__thinking-indicator--#{conversation.state}" %>
|
||||
@@ -19,8 +19,8 @@
|
||||
|
||||
<% if Current.user && !@hide_footer_frames %>
|
||||
<div id="footer_frames" data-turbo-permanent="true">
|
||||
<%= render "bar/bar" %>
|
||||
<%= render "my/pins/tray" %>
|
||||
<%= render "commands/terminal" %>
|
||||
<%= render "notifications/tray" %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
@@ -1,4 +1,11 @@
|
||||
<%= form_with url: search_path, method: :get, class: "search__form" do |form| %>
|
||||
<%= text_field_tag :q, query_terms, class: "search__form-input input", type: "search", placeholder: "What are you looking for?" %>
|
||||
<%= icon_tag "search", class: "search__form-icon" %>
|
||||
<%= form_with url: search_path, method: :get, class: "search__form flex align-center justify-center gap-half", data: { turbo_frame: "bar-content" } 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…",
|
||||
autocomplete: "off",
|
||||
autofocus: true,
|
||||
data: {
|
||||
action: "keydown.enter->bar#showModalAndSubmit keydown.esc->bar#reset" } %>
|
||||
<% end %>
|
||||
|
||||
@@ -1,18 +1,8 @@
|
||||
<li>
|
||||
<%= link_to result.source, class: "card card--search" do %>
|
||||
<header class="card__header">
|
||||
<label class="card__collection flex align-center">
|
||||
<span class="card__id"><%= result.card_id %></span>
|
||||
<span class="card__collection-name">
|
||||
<span class="overflow-ellipsis"><%= result.collection_name %></span>
|
||||
</span>
|
||||
</label>
|
||||
</header>
|
||||
|
||||
<div class="card__body justify-space-between">
|
||||
<div class="card__content">
|
||||
<h3 class="card__title">
|
||||
<%= result.card_title&.html_safe %>
|
||||
<%= link_to result.source, class: "search__result", data: { turbo_frame: "_top" } do %>
|
||||
<div>
|
||||
<h3 class="search__title txt--medium margin-none">
|
||||
# <%= result.card_id %> <%= result.card_title&.html_safe %>
|
||||
</h3>
|
||||
|
||||
<% if result.card_description.present? %>
|
||||
@@ -30,6 +20,6 @@
|
||||
<% end %>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
<div class="overflow-ellipsis translucent txt-ink"><%= result.collection_name %></div>
|
||||
<% end %>
|
||||
</li>
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
<div class="card card--search">
|
||||
<header class="card__header">
|
||||
<label class="card__collection flex align-center">
|
||||
<span class="card__id">864</span>
|
||||
<span class="card__collection-name">
|
||||
<span class="overflow-ellipsis"><%= collection %></span>
|
||||
</span>
|
||||
</label>
|
||||
</header>
|
||||
|
||||
<div class="card__body justify-space-between">
|
||||
<div class="card__content">
|
||||
<h3 class="card__title">
|
||||
<%= title.html_safe %>
|
||||
</h3>
|
||||
<%= yield %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,3 +0,0 @@
|
||||
<li>
|
||||
<%= link_to search_query.terms, search_path(q: search_query.terms), class: "btn txt-small" %>
|
||||
</li>
|
||||
@@ -1,38 +1,22 @@
|
||||
<% @page_title = "Search" %>
|
||||
|
||||
<% content_for :header do %>
|
||||
<nav class="header">
|
||||
<div class="header__actions header__actions--start">
|
||||
<%= link_to_back fallback_path: root_path %>
|
||||
</div>
|
||||
|
||||
<h1 class="header__title"><%= @page_title %></h1>
|
||||
|
||||
<div class="header__actions header__actions--end"></div>
|
||||
</nav>
|
||||
<% end %>
|
||||
|
||||
<%= auto_submit_form_with url: searches_queries_path(q: @query_terms), method: :post %>
|
||||
|
||||
<section class="search">
|
||||
<%= render "searches/form", query_terms: @query_terms %>
|
||||
|
||||
<div class="search__results">
|
||||
<% if @query_terms.present? && @search_results.none? %>
|
||||
<div class="search__empty">
|
||||
No search results
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<ul class="search__list">
|
||||
<%= render partial: "searches/result", collection: @search_results %>
|
||||
</ul>
|
||||
<%= turbo_frame_tag "bar-content" do %>
|
||||
<div class="search__header">
|
||||
<button class="btn txt-x-small" data-action="bar#reset">
|
||||
<%= icon_tag "remove-med" %>
|
||||
<span class="for-screen-reader">Close</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<aside class="search__history">
|
||||
<h3>Your recent searches…</h3>
|
||||
<ul class="search__list">
|
||||
<%= render partial: "searches/search_query", collection: @recent_search_queries.first(10) %>
|
||||
</ul>
|
||||
</aside>
|
||||
</section>
|
||||
<section class="search">
|
||||
<div class="search__results">
|
||||
<% if @search_results.none? %>
|
||||
<div class="search__empty">
|
||||
Not matches
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<ul class="search__list">
|
||||
<%= render partial: "searches/result", collection: @search_results %>
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
<% end %>
|
||||
|
||||
+1
-1
@@ -111,7 +111,7 @@ Rails.application.routes.draw do
|
||||
|
||||
resources :commands
|
||||
|
||||
resource :conversation, only: %i[ show ] do
|
||||
resource :conversation, only: %i[ show create ] do
|
||||
scope module: :conversations do
|
||||
resources :messages, only: %i[ index create ]
|
||||
end
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
require "test_helper"
|
||||
|
||||
class CommandsControllerTest < ActionDispatch::IntegrationTest
|
||||
setup do
|
||||
sign_in_as :kevin
|
||||
end
|
||||
|
||||
test "command that results in a redirect" do
|
||||
post commands_path, params: { command: "#{cards(:logo).id}" }
|
||||
|
||||
assert_redirected_to cards(:logo)
|
||||
end
|
||||
|
||||
test "command that triggers a redirect back" do
|
||||
post commands_path, params: { command: "design" }
|
||||
|
||||
assert_redirected_to search_path(q: "design")
|
||||
end
|
||||
end
|
||||
@@ -1,22 +0,0 @@
|
||||
require "test_helper"
|
||||
|
||||
class Command::GoToCardTest < ActionDispatch::IntegrationTest
|
||||
include CommandTestHelper
|
||||
|
||||
setup do
|
||||
@card = cards(:logo)
|
||||
end
|
||||
|
||||
test "redirect to the card perma" do
|
||||
result = execute_command "#{@card.id}"
|
||||
|
||||
assert_equal @card, result.url
|
||||
end
|
||||
|
||||
test "result in a regular search if the card does not exist" do
|
||||
command = parse_command "123"
|
||||
|
||||
result = command.execute
|
||||
assert_equal search_path(q: "123"), result.url
|
||||
end
|
||||
end
|
||||
@@ -49,4 +49,15 @@ class UserTest < ActiveSupport::TestCase
|
||||
|
||||
assert_not_includes User.active, system_user
|
||||
end
|
||||
|
||||
test "start or continue conversation" do
|
||||
user = users(:jz)
|
||||
|
||||
assert_nil user.conversation
|
||||
|
||||
conversation = user.start_or_continue_conversation
|
||||
|
||||
assert user.conversation
|
||||
assert_equal user.conversation, conversation
|
||||
end
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user