koan-server 0.57.0

GraphQL, Subsonic REST, and MCP server for koan music player.
Documentation
/*
 * The web UI's stylesheet, compiled by Tailwind into assets/ui.css (`just css`). The compiled file is committed:
 * the crate embeds it, and building koan must not need the Tailwind CLI.
 *
 * The pages carry their look as utilities in the Rust templates. What is here is what those cannot say: element
 * defaults, and rules for markup that ui.js builds or classes it toggles (`playing`, `busy`, `missing`).
 */
@import "tailwindcss" source(none);
@source "../src/ui";
@source "../assets/ui.js";
@import "../../../site/src/theme.css";

@theme {
  /* Wider than this, a sidebar and a transport bar; narrower, a tab bar. ui.js matches on the same width. */
  --breakpoint-wide: 721px;
  --animate-busy: busy 0.7s linear infinite;

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

@layer base {
  :root {
    --side-w: 200px;
    --bar-h: 76px;
    --tabs-h: 0px;

    @variant max-wide {
      --bar-h: 60px;
      --tabs-h: 56px;
    }
  }

  html,
  body {
    @apply overflow-x-hidden;
  }

  /* Touch screens get no hover, so a tap's highlight is the only sign it landed. Preflight removes it. */
  html {
    -webkit-tap-highlight-color: revert;
  }

  body {
    @apply bg-bg text-body text-ink;
    -webkit-text-size-adjust: 100%;
    padding-bottom: calc(var(--bar-h) + var(--tabs-h) + env(safe-area-inset-bottom));
  }

  a {
    @apply text-brand hover:underline;
  }

  p {
    @apply my-[1em];
  }

  h1 {
    @apply mt-1 mb-4 text-title font-extralight wrap-anywhere max-wide:text-title-sm;
  }

  h2 {
    @apply mt-6 mb-2.5 text-meta font-semibold tracking-[.08em] text-muted uppercase;
  }

  button {
    @apply cursor-pointer rounded-md border border-rule bg-rule px-3.5 py-2 text-ink hover:border-hover disabled:cursor-progress disabled:opacity-55;
  }

  input,
  select {
    @apply rounded-md border border-rule bg-bg px-2.5 py-2 text-ink;
  }

  input {
    @apply min-w-0;
  }

  input:focus {
    @apply outline-1 outline-brand;
  }

  input[type="range"] {
    @apply border-0 bg-transparent p-0 accent-brand;
  }

  /* Browser defaults that preflight removes, which the pages are drawn with. */
  input[type="checkbox"],
  input[type="range"] {
    margin: revert;
  }

  small {
    font-size: revert;
  }

  code {
    font: revert;
  }

  ::placeholder {
    color: revert;
  }
}

@layer components {
  /* Button variants, on every page and on the rows ui.js builds. */
  .primary {
    @apply border-brand bg-brand font-semibold text-bg;
  }

  .quiet {
    @apply border-transparent bg-transparent text-muted hover:text-ink;
  }

  /* The track menu; ui.js fills it. */
  #track-menu button {
    @apply rounded-md px-3 py-2 text-left text-ink hover:bg-rule;
  }

  #track-menu p {
    @apply m-0 truncate px-3 pt-1.5 pb-1 text-meta text-muted;
  }

  #track-menu hr {
    @apply my-1 border-rule;
  }

  /* A button whose request is running; Datastar sets the class. */
  .busy::after {
    content: "";
    @apply ms-[.45em] inline-block size-[.8em] animate-busy rounded-full border-2 border-current border-r-transparent align-[-.1em] motion-reduce:animate-none motion-reduce:border-r-current;
  }

  /* A cover that failed to load; ui.js marks it. */
  img.missing {
    @apply invisible;
  }

  /* Track lists: the pages render them, and ui.js builds the queue's. */
  .tracks {
    @apply grid gap-1.5;
  }

  .tracks li {
    @apply flex min-w-0 cursor-pointer items-center gap-3 max-wide:gap-2 max-wide:select-none [-webkit-touch-callout:none] rounded-lg border border-rule bg-surface py-1.5 pr-1.5 pl-3 hover:border-hover focus-visible:border-hover focus-visible:outline-none;
  }

  .tracks li.playing {
    @apply border-l-3 border-l-brand;
  }

  .tracks li.playing .t {
    @apply text-brand;
  }

  .tracks li.disc {
    @apply cursor-default border-0 bg-transparent px-0 pt-3 pb-0.5 text-meta tracking-[.08em] text-muted uppercase;
  }

  .tracks .n {
    @apply w-[1.6em] flex-none text-right text-muted tabular-nums;
  }

  .tracks .t {
    @apply min-w-0 flex-1 truncate;
  }

  .tracks .t small {
    @apply block truncate text-meta text-muted;
  }

  .tracks .d {
    @apply flex-none text-meta text-muted tabular-nums;
  }

  .tracks button {
    @apply flex-none px-2.5 py-1 text-input leading-none max-wide:px-2;
  }

  .tracks button svg {
    @apply inline size-[15px] fill-current align-[-2px];
  }

  /* A favourite's heart beside the ⋯ on a narrow screen; see Hearts::mark. */
  .tracks .fav-mark {
    @apply hidden flex-none px-1 text-brand;
  }

  .tracks .fav-mark svg {
    @apply size-[13px] fill-current;
  }

  .tracks li:has([data-fav][aria-pressed="true"]) .fav-mark {
    @apply max-wide:inline-flex;
  }
}