Skip to content

Latest commit

 

History

History
218 lines (171 loc) · 8.56 KB

File metadata and controls

218 lines (171 loc) · 8.56 KB

Dashboard block grid

Wave-Terminal-inspired movable+resizable blocks for the web/ PWA. Lives in web/dashboard.js (framework) and web/dashboard-init.js (v1 block registry).

Model

The dashboard is a 12-column CSS grid laid over <main class="dashboard">. Each block has an integer cell rect {col, row, w, h}, persisted to localStorage["familiar_dashboard_layout"] keyed by block id.

  • Columns: 12
  • Row height anchor: 48 px
  • Gap: 8 px
  • Min block size: 2 cols wide × 3 rows tall

Drag and resize happen in pointer-pixel space, then snap to cell coords on release. A ghost preview shows the snapped target.

Mobile (< 768px) collapses to a single-column flex stack; drag/resize are disabled, settings still work.

Registering a block type

import { dashboard } from "/dashboard.js";

dashboard.registerBlockType({
  id: "gpu",                // unique block-type id
  name: "gpu",              // shown in header bar + settings drawer title
  defaultRect: { col: 8, row: 0, w: 4, h: 4 },
  render(el) {
    // el is the block's `<div class="block-content">` — populate it.
    el.classList.add("block-content-gpu");
    el.append(renderGpuWidget());   // your DOM
  },
  renderSettings(el, state, save) {
    // Optional. el is the per-block section inside the settings drawer.
    // state is { rect, visible, settings } — write to state.settings.*
    // and call save() to persist + apply.
    const row = document.createElement("label");
    row.textContent = "show temperature";
    const input = document.createElement("input");
    input.type = "checkbox";
    input.checked = !!state.settings.showTemp;
    input.addEventListener("change", () => {
      state.settings.showTemp = input.checked;
      save();
    });
    row.appendChild(input);
    el.appendChild(row);
  },
});

Then mount once on the root:

const root = document.getElementById("dashboard");
dashboard.mount(root);

mount() instantiates one block per registered type, restoring rect+settings from localStorage when present.

Common settings (all blocks get these)

The framework's settings drawer always includes:

  • visible — toggle the block on/off (preserves rect+settings while hidden)
  • tintnone | warm | cool (subtle accent overlay)
  • font scale85% | 100% | 115% (sets --block-font-scale on the block element; content using font-size: 1em will scale)

Block-type opt-in goes in renderSettings(el, state, save).

Theme conventions

  • Use the existing CSS custom properties from style.css (--bg, --fg, --fg-muted, --accent, --user-bg, --assistant-bg, --border, --sidebar-bg, --color-warn, --color-error).
  • Fonts via --sans / --serif / --mono.
  • Match the parchment + sigil-gold aesthetic of the existing UI.
  • Respect prefers-reduced-motion: reduce — kill transitions, keep state changes instant.

Micro-animations + sound design (v29)

The dashboard has a small "vibe layer" of magical feedback:

Cue Trigger Where
Gold ripple on send submit any chat turn .send-ripple overlay on #send-btn
Block enter/exit add via "+" picker; hide via X .block-entering / .block-leaving
Slot-picker patch wash 200 from PATCH /api/familiar/admin/slots/:slot .slot-row--success gold sweep
Synthesized chime same trigger, when sound is enabled sound.chime()
Synthesized thunk 503 / 409 / network error on slot PATCH sound.thunk()
Synthesized tick send button submit, when sound is enabled sound.tick()

Sound is OFF by default. Enable it via the "sound fx" toggle in the sidebar voice section. Persisted to localStorage["familiar_sound"] ("on" / "off"). The toggle's first click also unlocks the AudioContext (browsers gate audio behind a user gesture).

Both prefers-reduced-motion: reduce and the familiar_sound opt-out disable all synthesized cues — independently. Animations gated only by reduced-motion still respect that media query via CSS.

The realm word (e.g. "Twilight Jewel") is mirrored into the connection status pill's tooltip so you can hover the pill from anywhere to see which build is loaded.

Public API surface

type BlockRect = { col: number; row: number; w: number; h: number };

type BlockType = {
  id: string;
  name: string;
  defaultRect?: BlockRect;
  render(el: HTMLElement): void;
  renderSettings?(
    el: HTMLElement,
    state: { rect: BlockRect; visible: boolean; settings: Record<string, unknown> },
    save: () => void,
  ): void;
};

interface Dashboard {
  registerBlockType(type: BlockType): void;
  mount(root: HTMLElement): void;
  resetLayout(): void;                       // wipes localStorage + reloads
  applyPreset(id: string): void;             // rebuilds from a named preset
  presets: string[];                          // available preset ids
  showBlock(id: string): void;                // un-hide a hidden block
  hideBlock(id: string): void;                // hide (preserves rect+settings)
  listTypes(): Array<{ id: string; name: string; visible: boolean; registered: true }>;
  listBlocks(): Array<{ id: string; typeId: string; visible: boolean }>;
}

Header controls (dashboard-init.js)

  • + (add block) — opens a popover listing every registered block type with its current visibility. Click a row to toggle show/hide. Keyboard nav (#65, commit 0551e5b): arrow Up/Down cycles rows; first row auto-focused on open; Esc returns focus to the + button.
  • layout — opens a popover with named presets (default, compact, data-dense) plus a destructive reset all row. Selecting a preset rebuilds the layout map from PRESETS[id] and reloads; reset wipes localStorage.

Keyboard shortcuts

Key Action
Ctrl+K (or Cmd+K) Open the command palette — fuzzy-search every action
/ Focus the chat input from anywhere
Ctrl+L Focus the chat input (shell muscle memory)
Ctrl+Shift+N New session
Ctrl+Shift+S Toggle sidebar
Ctrl+Home / Ctrl+End Scroll chat log to start / end
Esc Abort in-flight assistant stream (also closes the dashboard picker, returning focus to its trigger)
Tab (in settings drawer) Cycles within the drawer (focus trap)

Chat slash commands

Typed into the chat input — intercepted before the LLM sees them.

Command Action
/help or /? Toast with the full shortcut list
/clear Soft-clear the visible log (session intact; switching sessions restores)
/abort or /stop Cancel the in-flight assistant stream
/version Show the build's realm word, hash, branch, built timestamp

Unknown /-prefixed input falls through to the assistant normally — typing /path/to/file in a question still works.

Mobile / touch

Below 768px viewport, the grid collapses to a single-column stack. To reorder blocks on a touch device:

  1. Long-press the block header for 500ms → "edit mode" engages, with reorder chevrons visible
  2. Tap the up/down chevrons on each block to move it through the source order; layout persists per-browser

Hit areas auto-bump to 44×44px minimum on coarse-pointer devices.

Sound + welcome ritual

  • Sound effects — synthesized via Web Audio API (web/widgets/sound.js). OFF by default; enable via the "sound fx" toggle in the sidebar.
  • Welcome flourish — first visit per deploy (keyed on git hash): realm word fades in over the dashboard for ~1.5s, plays sound.flourish() at peak. Each new deploy gets its own flourish. Respects prefers-reduced-motion.

Preset definitions live in PRESETS in dashboard.js — add a key per block-type id (rect map), or false to hide a type in that preset.

Also exposed as window.familiarDashboard for debugging.

v1 blocks (registered by dashboard-init.js)

id type default rect (col,row,w,h) notes
chat chat 0,0,8,12 wraps the existing #log + #form markup
palace palace 0,12,12,14 wraps the existing #palace-view markup
model model 8,0,4,6 placeholder; real picker lands via slot-picker

Future block authors (Echo: /api/familiar/stats endpoint, Luna: stat widgets) just call dashboard.registerBlockType({...}) before dashboard.mount() is invoked. Add the registration to dashboard-init.js (or a new init file loaded ahead of app.js).