time-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 4857f8a940b5
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 703ed60f2cc5ad3d98a5bf4a
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: modal sheet, toast (with an optional action like Undo), copy, share, a
// popover, and the icons.
import { useEffect, useRef, useState, type ReactNode } from "react";

export function Modal({ title, onClose, children, footer, wide = false }: { title: string; onClose: () => void; children: ReactNode; footer?: ReactNode; wide?: boolean }) {
  const ref = useRef<HTMLDivElement>(null);
  const close = useRef(onClose);
  close.current = onClose;
  useEffect(() => {
    const prev = document.activeElement as HTMLElement | null;
    const onKey = (e: KeyboardEvent) => { if (e.key === "Escape" && !document.querySelector(".pop")) close.current(); };
    document.addEventListener("keydown", onKey);
    document.body.style.overflow = "hidden";
    ref.current?.querySelector<HTMLElement>("[autofocus], input, select, textarea")?.focus();
    return () => {
      document.removeEventListener("keydown", onKey);
      document.body.style.overflow = "";
      prev?.focus?.();
    };
  }, []);
  return (
    <div className="modal-backdrop" onMouseDown={(e) => { if (e.target === e.currentTarget) onClose(); }}>
      <div className={`modal${wide ? " modal-wide" : ""}`} 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"><X /></button>
        </div>
        <div className="modal-body">{children}</div>
        {footer && <div className="modal-foot">{footer}</div>}
      </div>
    </div>
  );
}

/** A small floating panel under a button. Closes on outside click or Escape. */
export function Popover({ open, onClose, children, align = "left", label }: { open: boolean; onClose: () => void; children: ReactNode; align?: "left" | "right"; label: string }) {
  const ref = useRef<HTMLDivElement>(null);
  useEffect(() => {
    if (!open) return;
    const onDown = (e: MouseEvent | TouchEvent) => {
      const el = ref.current;
      if (el && !el.contains(e.target as Node) && !(el.parentElement?.contains(e.target as Node))) onClose();
    };
    const onKey = (e: KeyboardEvent) => { if (e.key === "Escape") { e.stopPropagation(); onClose(); } };
    document.addEventListener("mousedown", onDown);
    document.addEventListener("touchstart", onDown);
    document.addEventListener("keydown", onKey, true);
    return () => {
      document.removeEventListener("mousedown", onDown);
      document.removeEventListener("touchstart", onDown);
      document.removeEventListener("keydown", onKey, true);
    };
  }, [open, onClose]);
  if (!open) return null;
  return <div className={`pop pop-${align}`} ref={ref} role="dialog" aria-label={label}>{children}</div>;
}

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

export function Toaster() {
  const [t, setT] = useState<{ text: string; action?: { label: string; run: () => void } } | null>(null);
  useEffect(() => {
    let timer: number | undefined;
    pushToast = (text, action) => {
      setT({ text, action });
      window.clearTimeout(timer);
      timer = window.setTimeout(() => setT(null), action ? 6000 : 3200);
    };
    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" className="toast-action" 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, done = "Link copied") {
  if (navigator.share && matchMedia("(pointer: coarse)").matches) {
    try { await navigator.share({ title, url }); return; } catch { /* cancelled */ }
  }
  await copy(url, done);
}

export function download(name: string, text: string, type = "text/csv;charset=utf-8") {
  const url = URL.createObjectURL(new Blob(["" + text], { type }));
  const a = document.createElement("a");
  a.href = url;
  a.download = name;
  document.body.appendChild(a);
  a.click();
  a.remove();
  window.setTimeout(() => URL.revokeObjectURL(url), 2000);
}

// ---- Icons (stroke icons, 20px grid) ----

type P = { size?: number };
const svg = (size: number, children: ReactNode, fill = false) => (
  <svg width={size} height={size} viewBox="0 0 20 20" aria-hidden="true" fill={fill ? "currentColor" : "none"} stroke={fill ? "none" : "currentColor"} strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round">{children}</svg>
);
export const X = ({ size = 18 }: P) => svg(size, <path d="M5 5l10 10M15 5 5 15" />);
export const Play = ({ size = 18 }: P) => svg(size, <path d="M6.5 4.5v11l9-5.5z" />, true);
export const Stop = ({ size = 18 }: P) => svg(size, <rect x="5" y="5" width="10" height="10" rx="2" />, true);
export const Plus = ({ size = 18 }: P) => svg(size, <path d="M10 4v12M4 10h12" />);
export const Clock = ({ size = 20 }: P) => svg(size, <><circle cx="10" cy="10" r="7" /><path d="M10 6v4l2.5 2" /></>);
export const Grid = ({ size = 20 }: P) => svg(size, <><rect x="3" y="4" width="14" height="12" rx="2" /><path d="M3 8h14M8 8v8M12.5 8v8" /></>);
export const Chart = ({ size = 20 }: P) => svg(size, <path d="M4 16V9M8.5 16V4M13 16v-5M17 16H3" />);
export const Folder = ({ size = 20 }: P) => svg(size, <path d="M3 6.5A1.5 1.5 0 0 1 4.5 5h3l1.5 2h6.5A1.5 1.5 0 0 1 17 8.5v6a1.5 1.5 0 0 1-1.5 1.5h-11A1.5 1.5 0 0 1 3 14.5z" />);
export const Gear = ({ size = 20 }: P) => svg(size, <><circle cx="10" cy="10" r="2.6" /><path d="M10 2.8v2M10 15.2v2M17.2 10h-2M4.8 10h-2M15.1 4.9l-1.4 1.4M6.3 13.7l-1.4 1.4M15.1 15.1l-1.4-1.4M6.3 6.3 4.9 4.9" /></>);
export const Tag = ({ size = 18 }: P) => svg(size, <><path d="M3.5 10.5V4.5a1 1 0 0 1 1-1h6l6 6-7 7z" /><circle cx="7.5" cy="7.5" r="1.2" /></>);
export const Left = ({ size = 18 }: P) => svg(size, <path d="M12 4.5 6.5 10l5.5 5.5" />);
export const Right = ({ size = 18 }: P) => svg(size, <path d="M8 4.5 13.5 10 8 15.5" />);
export const Users = ({ size = 18 }: P) => svg(size, <><circle cx="7.5" cy="7" r="2.6" /><path d="M2.8 16c.5-2.6 2.4-4 4.7-4s4.2 1.4 4.7 4" /><path d="M13 4.6a2.5 2.5 0 0 1 0 4.8M14.5 12.2c1.5.5 2.5 1.8 2.8 3.8" /></>);
export const Check = ({ size = 16 }: P) => svg(size, <path d="M4.5 10.5 8 14l7.5-8" />);
export const Pencil = ({ size = 16 }: P) => svg(size, <path d="M12.5 4.5l3 3L7 16H4v-3z" />);

Change history

Point OpenApps links to openapps.free
4857f8a940 · 19 hours ago
Bind agent address constants in policy
62b1e6acb2 · 20 hours ago
Use functions.openapps.xyz alias and add OpenApp contribution strip
61d972193a · 20 hours ago
Bounded source sync
fc644b4327 · 1 day ago
Bounded source sync
22f511f3db · 1 day ago
Bounded source sync
351a8d754c · 1 day ago
Bounded source sync
0da238a77e · 1 day ago