Newer
Older
navi-1 / webclient / src / styles / partials / _composer.scss
@Eugene Sukhodolskiy Eugene Sukhodolskiy 1 day ago 11 KB webclient: split the app styles into partials
@use 'kit-deps' as *;
@use 'tokens' as *;

// ─── Input bar ─────────────────────────────────────────────────────────────
.input-bar {
  flex-shrink: 0;
  padding: 8px 15px;
  background: $surface-page;
  border-top: $border-width-base $border-style-base $border-color-muted;

  @media (max-width: 767px) {
    padding: 0; /* empty bar is flush; the attachment strip below owns the gaps */
  }
}

.input-dropzone {
  position: relative;

  &.is-dragging::after {
    content: 'Drop files here';
    position: absolute;
    inset: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    background: rgba($color-secondary, 0.12);
    border: 2px dashed $color-secondary;
    font-size: 14px;
    color: $color-secondary;
    pointer-events: none;
    z-index: 10;
  }
}

.file-preview-strip {
  display: flex;
  flex-wrap: wrap;
  gap: $space-xs;
  margin-bottom: $space-xs;
  max-width: 920px;
  margin-left: auto;
  margin-right: auto;

  @media (max-width: 767px) {
    /* The strip, not the bar, provides the top gap on full-bleed mobile */
    padding-top: $space-sm;
  }
}

.file-preview-image {
  position: relative;
  width: 60px;
  height: 60px;

  img {
    width: 100%;
    height: 100%;
    object-fit: cover;
    border: $border-width-base $border-style-base $border-color-muted;
  }
}

.file-preview-pill.file-preview-pill {
  /* GnChip root: filenames must not inherit the kit chip's uppercase */
  text-transform: none;
  max-width: 180px;

  /* Inside the kit's inline-flex chip the name span won't shrink below its
     content unless min-width is lifted (default `auto`), so the ellipsis
     only kicks in with an explicit min-width: 0 */
  .file-preview-name {
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }
}

/* Thumb stays app-owned (no kit image-preview component) — kit surface look */
.file-preview-image {
  border: $border-width-base $border-style-base $border-color-muted;
  border-radius: $border-radius-sm;
  overflow: hidden;
  background: $surface-panel-muted;
}

.file-preview-remove {
  background: none;
  border: none;
  cursor: pointer;
  color: $color-text-dark;
  padding: 0;
  display: flex;
  align-items: center;
  font-size: $icon-size-xs;
  transition: color $motion-fast $motion-ease;
  flex-shrink: 0;

  &:hover { color: $color-error; }
}

.upload-progress {
  height: 2px;
  background: $surface-panel-strong;
  margin-bottom: $space-xs;
  overflow: hidden;
}

.upload-progress-fill {
  height: 100%;
  background: $color-secondary;
  transition: width $motion-base $motion-ease;
}

// Indeterminate variant: fetch() gives no progress fraction, so the fill
// slides across instead of sitting at a fake fixed percentage.
.upload-progress.is-indeterminate {
  .upload-progress-fill {
    width: 30%;
    transition: none;
    animation: upload-slide 1.1s ease-in-out infinite;
  }
}

@keyframes upload-slide {
  0%   { transform: translateX(-110%); }
  100% { transform: translateX(440%); }
}

.input-row {
  display: flex;
  align-items: center;
  gap: $space-sm;
  background: $surface-panel-muted;
  border: $border-width-base $border-style-base $color-text-light;
  border-radius: $border-radius-sm;
  padding: $space-sm;
  max-width: 920px;
  margin: 0 auto;

  &:hover {
    border-bottom-color: $color-text-dark;
  }

  &:focus-within {
    border-color: $color-electric-blue;
    background: transparent;
  }

  @media (max-width: 767px) {
    border: none;
    border-radius: 0;
    max-width: none;
    padding: $space-sm $space-md;
  }
}

.input-textarea {
  flex: 1;
  background: none;
  border: none;
  outline: none;
  resize: none;
  color: $color-text-light;
  font-size: 14px;
  line-height: 1.5;
  font-family: inherit;
  min-height: 24px;
  max-height: 144px;
  overflow-y: auto;
  padding: 2px 0;

  &::placeholder { color: $color-text-dark; }
}

// ─── Welcome screen ────────────────────────────────────────────────────────
.welcome-screen {
  flex: 1;
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  // `safe center` is not cosmetic: with a plain `center`, content taller than
  // the pane overflows past the flex start edge and cannot be scrolled back to.
  // The profile grid grows with the profile list, so this pane must be able to
  // scroll — on a phone it is taller than the viewport with more than a few.
  justify-content: safe center;
  overflow-y: auto;
  padding: $space-xl;
  gap: $space-xl;
  text-align: center;

  @media (max-width: 768px) {
    padding: 15px;
    gap: $space-md;
    justify-content: flex-start;
    padding-top: 52px;
  }
}

.welcome-logo {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: $space-sm;

  img { width: 64px; height: 64px; }

  h1 {
    font-size: 30px; // kit h1 (typography tokens are not exported by kit-deps)
    font-weight: 600;
    color: $color-text-light;
    margin: 0;
  }

  p {
    font-size: var(--gn-font-size-base);
    color: $color-text-medium;
    margin: 0;
  }

  @media (max-width: 768px) {
    flex-direction: row;
    flex-wrap: wrap;
    justify-content: center;
    gap: $space-sm;

    img { width: 36px; height: 36px; }

    h1 { font-size: 22px; }

    p {
      font-size: 13px;
      flex-basis: 100%;
    }
  }
}

.welcome-profiles {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  // `1fr` rows all take the height of the tallest one, so a card whose name
  // wraps or whose description runs long does not stand out. A fixed pixel
  // height would not survive that: "Server Administrator" wraps on a phone.
  grid-auto-rows: 1fr;
  // The pane scrolls, so the grid must not be squeezed to fit it.
  flex-shrink: 0;
  gap: $space-md;
  width: min(100%, 880px);

  @media (max-width: 768px) {
    grid-template-columns: repeat(2, 1fr);
    gap: 10px;
    width: 100%;
    max-width: 100%;
  }
}

.welcome-profile-card {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: $space-sm;
  padding: $space-md;
  background: $surface-panel-muted;
  border: $border-width-base $border-style-base $border-color-muted;
  border-radius: $border-radius-md;
  cursor: pointer;
  transition: border-color $motion-fast $motion-ease, background $motion-fast $motion-ease;
  width: 100%;
  text-align: center;

  &:hover {
    border-color: $color-secondary;
    background: $surface-panel-strong;
  }

  &.is-creating {
    opacity: 0.6;
    pointer-events: none;
  }

  .profile-name {
    font-size: 14px;
    font-weight: 600;
    color: $color-text-light;
  }

  .profile-desc {
    font-size: 12px;
    color: $color-text-dark;
    line-height: 1.4;
    // No line clamp: a description the user cannot finish reading is worse
    // than a taller card, and `grid-auto-rows: 1fr` keeps the row even anyway.
  }

  @media (max-width: 768px) {
    width: auto;
    padding: 10px;
    gap: 6px;

    .profile-name { font-size: 13px; }
    .profile-desc { font-size: 11px; }
  }
}

// ─── Empty state ───────────────────────────────────────────────────────────
// Centers the GnEmptyState card inside the chat area.
.empty-chat {
  flex: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
}

// ─── Spinner ───────────────────────────────────────────────────────────────
.spinner {
  display: inline-block;
  width: 14px;
  height: 14px;
  border: 2px solid $border-color-muted;
  border-top-color: $color-secondary;
  border-radius: 50%;
  animation: spin 0.6s linear infinite;
  flex-shrink: 0;
}

@keyframes spin {
  to { transform: rotate(360deg); }
}

// ─── Animations ────────────────────────────────────────────────────────────
.fade-enter-active,
.fade-leave-active {
  transition: opacity $motion-fast $motion-ease;
}
.fade-enter-from,
.fade-leave-to {
  opacity: 0;
}

.msg-enter {
  animation: msgEnter $motion-base $motion-ease both;
}

@keyframes msgEnter {
  from { opacity: 0; transform: translateY(8px); }
  to   { opacity: 1; transform: translateY(0); }
}

.slide-left-enter-active,
.slide-left-leave-active {
  transition: transform $motion-slow $motion-ease, opacity $motion-slow $motion-ease;
}
.slide-left-enter-from {
  transform: translateX(-100%);
  opacity: 0;
}
.slide-left-leave-to {
  transform: translateX(-100%);
  opacity: 0;
}

// ─── Confirm dialog transition ─────────────────────────────────────────────
.modal-enter-active,
.modal-leave-active {
  transition: opacity $motion-slow $motion-ease;

  .modal-panel {
    transition: opacity $motion-slow $motion-ease, margin-top $motion-slow $motion-ease;
  }
}

.modal-enter-from,
.modal-leave-to {
  opacity: 0;

  .modal-panel {
    opacity: 0;
    margin-top: 200px;
  }
}

// ─── Disclosure chevrons ───────────────────────────────────────────────────

// Kit 1.0 dropped the hover icon rotation entirely; app-owned
// tool/thinking cards keep the open-state chevron flip.
.tool-card[open] > summary .tool-chevron,
.thinking-card[open] > summary .tool-chevron {
  transform: rotate(180deg) !important;
}

// ─── Syntax highlighting ───────────────────────────────────────────────────

// highlight.js emits `.hljs-*` classes but ships no theme of its own — without
// these rules every highlighted block renders in one flat text colour, which
// is what an un-styled `hljs` looks like. One palette, declared once, so the
// message cards, the artifact panel and the content lightbox all agree.
// Colours are kit palette tokens, picked for the dark default theme.
.hljs {
  background: transparent;
  color: inherit;
}

.hljs-comment,
.hljs-quote {
  color: $color-text-dark;
  font-style: italic;
}

.hljs-keyword,
.hljs-selector-tag,
.hljs-literal,
.hljs-doctag,
.hljs-formula {
  color: $color-purple;
}

.hljs-string,
.hljs-regexp,
.hljs-addition,
.hljs-char.escape_ {
  color: $color-neon-green;
}

.hljs-number,
.hljs-symbol,
.hljs-bullet,
.hljs-link {
  color: $color-orange;
}

.hljs-title,
.hljs-title.class_,
.hljs-title.function_,
.hljs-section,
.hljs-name,
.hljs-selector-id,
.hljs-selector-class {
  color: $color-electric-blue;
}

.hljs-attr,
.hljs-attribute,
.hljs-variable,
.hljs-template-variable,
.hljs-property {
  color: $color-cyan;
}

.hljs-type,
.hljs-built_in,
.hljs-class .hljs-title,
.hljs-params,
.hljs-operator,
.hljs-punctuation {
  color: $color-neon-yellow;
}

.hljs-meta,
.hljs-meta .hljs-keyword {
  color: $color-teal;
}

.hljs-deletion {
  color: $color-error;
}

.hljs-emphasis {
  font-style: italic;
}

.hljs-strong {
  font-weight: 600;
}

// ─── Search result message flash ────────────────────────────────────────────
.msg-flash {
  animation: msgFlash 2.5s ease-out;
}

@keyframes msgFlash {
  0%   { filter: brightness(1); }
  15%  { filter: brightness(1.4); background-color: rgba($color-secondary, 0.12); }
  40%  { filter: brightness(1.4); background-color: rgba($color-secondary, 0.12); }
  100% { filter: brightness(1); }
}