koan-server 0.61.12

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";
@import "./controls.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;
    /* The toggle's tick, in `bg`: a data URL cannot read a custom property, so it is stated per scheme. */
    --tick: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 14 14'%3E%3Cpath d='M3 7.5l2.5 2.5L11 4.5' fill='none' stroke='%231e1e1e' stroke-width='1.6' stroke-linecap='square'/%3E%3C/svg%3E");

    @media (prefers-color-scheme: light) {
      --tick: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 14 14'%3E%3Cpath d='M3 7.5l2.5 2.5L11 4.5' fill='none' stroke='%23ffffff' stroke-width='1.6' stroke-linecap='square'/%3E%3C/svg%3E");
    }

    @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];
  }

  /* The app's own words are lowercase, as on the site; what comes from the library or a person keeps its case, so a
   * heading that shows one says `normal-case`. Screen readers read the markup, which keeps its capitals. */
  h1 {
    @apply mt-1 mb-4 text-display font-extralight text-strong lowercase wrap-anywhere max-wide:text-title;
  }

  h2 {
    @apply mt-8 mb-3 text-title-sm font-light text-strong lowercase;
  }

  button,
  summary,
  label {
    @apply lowercase;
  }

  /* Bordered, the weight a button has unless the page says otherwise: a secondary action that bare text would not
   * read as. Square, as every control in the theme is; the outline is `muted` because `rule` is too faint to mark
   * where a control is. */
  button {
    @apply cursor-pointer border border-muted bg-transparent px-3 py-1.5 text-meta text-ink hover:bg-hover/30 active:bg-hover disabled:cursor-progress disabled:opacity-40;
  }

  input,
  select {
    @apply border border-muted bg-transparent px-2.5 py-2 text-ink normal-case;
  }

  input {
    @apply min-w-0;
  }

  /* The theme's toggle: a square box, outlined off, filled with a tick on. */
  input[type="checkbox"] {
    @apply size-3.5 flex-none cursor-pointer appearance-none p-0 align-[-2px] checked:border-brand checked:bg-brand;
    background-image: none;
  }

  input[type="checkbox"]:checked {
    background: var(--brand) var(--tick) center / 100% no-repeat;
  }

  /* 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 {
  /* The theme's button weights, beside the bordered default. Prominent is what the page is for, and a page has at
   * most one; standard is the label alone; quiet is a bar's text button, and the icon buttons in rows. */
  .primary {
    @apply border-brand px-5 py-3 text-body text-brand;
  }

  .standard {
    @apply border-transparent px-1 py-2 text-control hover:bg-transparent hover:text-strong;
  }

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

  /* Destructive: compact or bordered, in `bad`. */
  .bad {
    @apply border-bad text-bad;
  }

  .quiet.bad {
    @apply border-transparent hover:text-bad;
  }

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

  #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;
  }

  /* No rules between rows: they are told apart by rhythm and alignment. */
  .tracks li {
    @apply flex min-w-0 cursor-pointer items-center gap-3 px-2 py-1 max-wide:gap-2 max-wide:px-0 max-wide:select-none [-webkit-touch-callout:none] hover:bg-hover/30 focus-visible:bg-hover/30 focus-visible:outline-none;
  }

  .tracks li.playing :is(.n, .t) {
    @apply text-brand;
  }

  .tracks li.disc {
    @apply cursor-default px-0 pt-4 pb-1 text-meta text-muted lowercase hover:bg-transparent;
  }

  .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-2 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;
  }
}