lists-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 ebccaf0b13fe
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 ee59501e91bae6d2c23a8370
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: sheet, toast (with an optional undo), copy and share, icons.
import { useEffect, useRef, useState, type ReactNode } from "react";

export function Modal({ title, onClose, children, footer }: { title: string; onClose: () => void; children: ReactNode; footer?: ReactNode }) {
  const ref = useRef<HTMLDivElement>(null);
  useEffect(() => {
    const prev = document.activeElement as HTMLElement | null;
    const onKey = (e: KeyboardEvent) => { if (e.key === "Escape") onClose(); };
    document.addEventListener("keydown", onKey);
    document.body.style.overflow = "hidden";
    ref.current?.querySelector<HTMLElement>("[autofocus]")?.focus();
    return () => {
      document.removeEventListener("keydown", onKey);
      document.body.style.overflow = "";
      prev?.focus();
    };
  }, [onClose]);
  return (
    <div className="modal-backdrop" onMouseDown={(e) => { if (e.target === e.currentTarget) onClose(); }}>
      <div className="modal" role="dialog" aria-modal="true" aria-label={title} ref={ref}>
        <div className="modal-head">
          <h2 className="modal-title">{title}</h2>
          <button className="icon-btn" type="button" onClick={onClose} aria-label="Close"><Icon name="close" /></button>
        </div>
        <div className="modal-body">{children}</div>
        {footer && <div className="modal-foot">{footer}</div>}
      </div>
    </div>
  );
}

type ToastAction = { label: string; run: () => void };
let pushToast: ((text: string, action?: ToastAction) => void) | null = null;
export function toast(text: string, action?: ToastAction) { pushToast?.(text, action); }

export function Toaster() {
  const [t, setT] = useState<{ text: string; action?: ToastAction } | null>(null);
  useEffect(() => {
    let timer: number | undefined;
    pushToast = (text, action) => {
      setT({ text, action });
      window.clearTimeout(timer);
      timer = window.setTimeout(() => setT(null), action ? 6000 : 2800);
    };
    return () => { pushToast = null; window.clearTimeout(timer); };
  }, []);
  if (!t) return null;
  return (
    <div className="toast" role="status">
      <span>{t.text}</span>
      {t.action && <button type="button" onClick={() => { t.action!.run(); setT(null); }}>{t.action.label}</button>}
    </div>
  );
}

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

export async function share(url: string, title: string, text: string) {
  if (navigator.share && matchMedia("(pointer: coarse)").matches) {
    try { await navigator.share({ title, text, url }); return; } catch { /* cancelled */ }
  }
  await copy(url, "Link copied. Send it to everyone in the house.");
}

const PATHS: Record<string, ReactNode> = {
  close: <path d="M5 5l10 10M15 5 5 15" />,
  back: <path d="M12.5 4.5 7 10l5.5 5.5" />,
  plus: <path d="M10 4v12M4 10h12" />,
  more: <><circle cx="4.5" cy="10" r="1.3" fill="currentColor" stroke="none" /><circle cx="10" cy="10" r="1.3" fill="currentColor" stroke="none" /><circle cx="15.5" cy="10" r="1.3" fill="currentColor" stroke="none" /></>,
  share: <path d="M8 12l4-4M7 9.5 5.2 11.3a2.8 2.8 0 1 0 4 4L11 13.5M13 10.5l1.8-1.8a2.8 2.8 0 1 0-4-4L9 6.5" />,
  edit: <path d="M4 16h3l8.5-8.5a2.1 2.1 0 0 0-3-3L4 13v3Z" />,
  check: <path d="M5 10.5 8.5 14 15 6.5" />,
  up: <path d="M5 12.5 10 7.5l5 5" />,
  down: <path d="M5 7.5l5 5 5-5" />,
  cloud: <path d="M6 15h8.5a3.2 3.2 0 0 0 .4-6.4A4.8 4.8 0 0 0 5.7 9 3 3 0 0 0 6 15Z" />,
  offline: <><path d="M6 15h8.5a3.2 3.2 0 0 0 .4-6.4A4.8 4.8 0 0 0 5.7 9 3 3 0 0 0 6 15Z" /><path d="M3.5 3.5l13 13" /></>,
  list: <path d="M7 6h9M7 10h9M7 14h9M4 6h.01M4 10h.01M4 14h.01" />,
  trash: <path d="M4.5 6h11M8 6V4.5h4V6M6 6l.7 9.5h6.6L14 6" />,
};

export function Icon({ name, size = 18 }: { name: keyof typeof PATHS | string; size?: number }) {
  return (
    <svg width={size} height={size} viewBox="0 0 20 20" fill="none" stroke="currentColor" strokeWidth="1.7" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
      {PATHS[name]}
    </svg>
  );
}

Change history

Point OpenApps links to openapps.free
ebccaf0b13 · 17 hours ago
Bind agent address in policy constants
519855687c · 19 hours ago
Use OpenApps service URLs and add contribution strip
2c353edfdc · 19 hours ago
Bounded source sync
58ac7e08ec · 1 day ago
Bounded source sync
150962f3d9 · 1 day ago
Bounded source sync
e52e4676b9 · 1 day ago