book-free.openapps.xyz

Source

This app’s code and revision history are public. Download a copy or explore the files below.

Public
The app’s code and revision history are public.
Owned app · rev f8f1b3a80195
Run it yourself
Take the whole tree and run it on your own machine. The download needs no account. Cloning needs a sign-in.
Download .zip
bounded clone 7385a6c1c47153b1728160ff
A copy you download is not bounded. The guarantees this app runs under - declared egress, proven invariants, writes that fail closed - live in the runtime serving this page, not in the files. What you get is auditable source you can read and run, with no runtime guarantees attached.
← All files
src/ui.tsx
// Small shared pieces: dialog, toast, copy-to-clipboard, file download, icons.
import { useEffect, useRef, useState, type ReactNode } from "react";

type Slot = ReactNode | ((close: () => void) => ReactNode);

/** The kit's dialog: a centered card on wide screens, a bottom sheet on phones. `close` plays the exit, then calls onClose. */
export function Modal({ title, onClose, children, footer }: { title: string; onClose: () => void; children: Slot; footer?: Slot }) {
  const ref = useRef<HTMLDivElement>(null);
  const [leaving, setLeaving] = useState(false);
  const close = () => setLeaving(true);
  useEffect(() => {
    const prev = document.activeElement as HTMLElement | null;
    const onKey = (e: KeyboardEvent) => { if (e.key === "Escape") setLeaving(true); };
    document.addEventListener("keydown", onKey);
    document.body.style.overflow = "hidden";
    (ref.current?.querySelector<HTMLElement>("[autofocus], .dialog-body :is(input, select, textarea)") ?? ref.current)?.focus();
    return () => {
      document.removeEventListener("keydown", onKey);
      document.body.style.overflow = "";
      prev?.focus();
    };
  }, []);
  const fill = (s: Slot) => (typeof s === "function" ? s(close) : s);
  return (
    <div className={`dialog-backdrop${leaving ? " leaving" : ""}`} onMouseDown={(e) => { if (e.target === e.currentTarget) close(); }}>
      <div className="dialog" role="dialog" aria-modal="true" aria-label={title} ref={ref} tabIndex={-1}
        onAnimationEnd={(e) => { if (leaving && e.target === e.currentTarget) onClose(); }}>
        <div className="dialog-head">
          <h2 className="dialog-title">{title}</h2>
          <button className="btn btn-quiet btn-icon btn-sm" type="button" onClick={close} aria-label="Close"><CloseIcon /></button>
        </div>
        <div className="dialog-body">{fill(children)}</div>
        {footer && <div className="dialog-foot">{fill(footer)}</div>}
      </div>
    </div>
  );
}

let pushToast: ((text: string) => void) | null = null;
export function toast(text: string) { pushToast?.(text); }

export function Toaster() {
  const [msg, setMsg] = useState<{ text: string; n: number; leaving: boolean } | null>(null);
  useEffect(() => {
    let timer: number | undefined;
    let n = 0;
    pushToast = (text) => {
      setMsg({ text, n: ++n, leaving: false });
      window.clearTimeout(timer);
      timer = window.setTimeout(() => setMsg((m) => m && { ...m, leaving: true }), 2600);
    };
    return () => { pushToast = null; window.clearTimeout(timer); };
  }, []);
  if (!msg) return null;
  return (
    <div key={msg.n} className={`toast${msg.leaving ? " leaving" : ""}`} role="status"
      onAnimationEnd={() => { if (msg.leaving) setMsg(null); }}>{msg.text}</div>
  );
}

export async function copy(text: string, done = "Copied") {
  try {
    await navigator.clipboard.writeText(text);
    toast(done);
  } catch {
    window.prompt("Copy this:", text);
  }
}

/** The kit's dropdown on a <details>: closes on a pick, an outside click or Escape. */
export function Dropdown({ label, className = "btn btn-sm", end, children }: { label: ReactNode; className?: string; end?: boolean; children: ReactNode }) {
  const ref = useRef<HTMLDetailsElement>(null);
  const [open, setOpen] = useState(false);
  useEffect(() => {
    if (!open) return;
    const shut = (e: Event) => {
      if (e instanceof KeyboardEvent ? e.key === "Escape" : !ref.current?.contains(e.target as Node)) ref.current!.open = false;
    };
    document.addEventListener("pointerdown", shut);
    document.addEventListener("keydown", shut);
    return () => { document.removeEventListener("pointerdown", shut); document.removeEventListener("keydown", shut); };
  }, [open]);
  return (
    <details className="dropdown" ref={ref} onToggle={() => setOpen(ref.current!.open)}>
      <summary className={className}>{label} <ChevronDown /></summary>
      <div className={`dropdown-menu${end ? " dropdown-menu-end" : ""}`} onClick={() => { ref.current!.open = false; }}>{children}</div>
    </details>
  );
}

/** Saves text as a file, like a calendar invite. */
export function download(name: string, text: string, type: string) {
  const url = URL.createObjectURL(new Blob([text], { type }));
  const a = Object.assign(document.createElement("a"), { href: url, download: name });
  document.body.appendChild(a);
  a.click();
  a.remove();
  setTimeout(() => URL.revokeObjectURL(url), 1000);
}

/** Scrolls an element to the top of the screen on phones, where the next step would otherwise sit below the fold. */
export function bringIntoView(el: Element | null) {
  if (!el || !matchMedia("(max-width: 860px)").matches) return;
  const reduce = matchMedia("(prefers-reduced-motion: reduce)").matches;
  const top = el.getBoundingClientRect().top + window.scrollY - 76;
  window.scrollTo({ top, behavior: reduce ? "auto" : "smooth" });
}

const svg = { width: 16, height: 16, viewBox: "0 0 20 20", "aria-hidden": true } as const;
const line = { fill: "none", stroke: "currentColor", strokeWidth: 1.5, strokeLinecap: "round", strokeLinejoin: "round" } as const;
export const CloseIcon = () => <svg {...svg} width={18} height={18}><path d="M5 5l10 10M15 5 5 15" {...line} strokeWidth={1.7} /></svg>;
export const PlusIcon = () => <svg {...svg}><path d="M10 4.5v11M4.5 10h11" {...line} strokeWidth={1.7} /></svg>;
export const BackIcon = () => <svg {...svg} width={14} height={14}><path d="M12.5 4.5 7 10l5.5 5.5" {...line} strokeWidth={1.9} /></svg>;
export const ChevronLeft = () => <svg {...svg} width={18} height={18}><path d="M12.5 4.5 7 10l5.5 5.5" {...line} strokeWidth={1.7} /></svg>;
export const ChevronRight = () => <svg {...svg} width={18} height={18}><path d="M7.5 4.5 13 10l-5.5 5.5" {...line} strokeWidth={1.7} /></svg>;
export const ChevronDown = () => <svg {...svg} width={14} height={14}><path d="M5 7.5 10 12.5l5-5" {...line} strokeWidth={1.8} /></svg>;
export const ClockIcon = () => <svg {...svg}><circle cx="10" cy="10" r="7.25" {...line} /><path d="M10 6v4.2l2.8 1.8" {...line} /></svg>;
export const VideoIcon = () => <svg {...svg}><rect x="2.75" y="5.25" width="10.5" height="9.5" rx="2" {...line} /><path d="m13.25 8.6 4-2.35v7.5l-4-2.35" {...line} /></svg>;
export const CalendarIcon = () => <svg {...svg}><rect x="3.5" y="4.5" width="13" height="12" rx="2" {...line} /><path d="M3.5 8.5h13M7 3v3M13 3v3" {...line} /></svg>;
export const GlobeIcon = () => <svg {...svg}><circle cx="10" cy="10" r="7.25" {...line} /><path d="M2.75 10h14.5M10 2.75c2.2 2.3 2.2 12.2 0 14.5M10 2.75c-2.2 2.3-2.2 12.2 0 14.5" {...line} strokeWidth={1.3} /></svg>;
export const CheckIcon = () => <svg {...svg} width={22} height={22}><path d="M4.5 10.5 8 14l7.5-8" {...line} strokeWidth={2.2} /></svg>;

Change history

Add favicon.ico and link tag
f8f1b3a801 · 2 hours ago
Quality pass: guest reschedule, days off, calendar feed, API docs
c827e5d073 · 5 hours ago
Bounded source sync
68e67b4092 · 1 day ago
Bounded source sync
ab2e006f9b · 1 day ago
Bounded source sync
bb10824b54 · 1 day ago
Bounded source sync
d22f765fb7 · 1 day ago
Bounded source sync
30b4b06513 · 1 day ago
Bounded source sync
c3d297477a · 1 day ago
Bounded source sync
7b18317417 · 1 day ago