pdf-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 0341fee61377
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 7716a56ff3432bc0c1e4837c
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: toast, dialog, segmented control, saving a file, sizes.
import { useEffect, useId, useRef, useState, type KeyboardEvent, type ReactNode } from "react";

type Toast = { text: string; action?: { label: string; run: () => void } };
let pushToast: ((t: Toast) => void) | null = null;
/** Shows a short message at the bottom of the screen, with an optional button such as Undo. */
export function toast(text: string, action?: Toast["action"]) { pushToast?.({ text, action }); }

export function Toaster() {
  const [shown, setShown] = useState<(Toast & { id: number; leaving: boolean }) | null>(null);
  useEffect(() => {
    let timer: number | undefined;
    let id = 0;
    pushToast = (t) => {
      const mine = ++id;
      setShown({ ...t, id: mine, leaving: false });
      window.clearTimeout(timer);
      timer = window.setTimeout(() => setShown((s) => (s?.id === mine ? { ...s, leaving: true } : s)), t.action ? 5000 : 2600);
    };
    return () => { pushToast = null; window.clearTimeout(timer); };
  }, []);
  if (!shown) return null;
  return (
    <div className={`toast${shown.leaving ? " leaving" : ""}`} role="status" key={shown.id}
      onAnimationEnd={() => shown.leaving && setShown((s) => (s?.id === shown.id ? null : s))}>
      <span>{shown.text}</span>
      {shown.action && (
        <button className="toast-action" type="button" onClick={() => { shown.action!.run(); setShown({ ...shown, leaving: true }); }}>
          {shown.action.label}
        </button>
      )}
    </div>
  );
}

/** The kit dialog: a centered card on wide screens, a bottom sheet on phones. Escape, the backdrop and Close all close it. */
export function Dialog({ title, onClose, children, foot }: {
  title: string;
  onClose: () => void;
  children: ReactNode;
  foot?: (close: () => void) => ReactNode;
}) {
  const [leaving, setLeaving] = useState(false);
  const box = useRef<HTMLDivElement>(null);
  const titleId = useId();
  const close = () => setLeaving(true);
  useEffect(() => {
    const before = document.activeElement as HTMLElement | null;
    box.current?.focus({ preventScroll: true }); // the sheet starts below the screen; focusing must not scroll the page
    const overflow = document.body.style.overflow;
    document.body.style.overflow = "hidden";
    return () => {
      document.body.style.overflow = overflow;
      before?.focus?.({ preventScroll: true });
    };
  }, []);
  const onKeyDown = (e: KeyboardEvent) => {
    if (e.key === "Escape") {
      e.preventDefault();
      close();
    }
    if (e.key !== "Tab" || !box.current) return;
    // Keep keyboard focus inside the dialog while it is open.
    const items = box.current.querySelectorAll<HTMLElement>("button:not(:disabled), input, select, textarea, a[href]");
    const first = items[0];
    const last = items[items.length - 1];
    if (e.shiftKey && (document.activeElement === first || document.activeElement === box.current)) {
      e.preventDefault();
      last?.focus();
    } else if (!e.shiftKey && document.activeElement === last) {
      e.preventDefault();
      first?.focus();
    }
  };
  return (
    <div className={`dialog-backdrop${leaving ? " leaving" : ""}`} onKeyDown={onKeyDown}
      onClick={(e) => e.target === e.currentTarget && close()}
      onAnimationEnd={(e) => leaving && e.target === e.currentTarget && onClose()}>
      <div className="dialog" role="dialog" aria-modal="true" aria-labelledby={titleId} tabIndex={-1} ref={box}>
        <div className="dialog-head">
          <h2 className="dialog-title" id={titleId}>{title}</h2>
          <button className="btn btn-quiet btn-icon btn-sm" type="button" aria-label="Close" onClick={close}>
            <svg width="18" height="18" viewBox="0 0 20 20" fill="none" stroke="currentColor" strokeWidth="1.7" strokeLinecap="round" aria-hidden="true"><path d="M5 5l10 10M15 5 5 15" /></svg>
          </button>
        </div>
        <div className="dialog-body">{children}</div>
        {foot && <div className="dialog-foot">{foot(close)}</div>}
      </div>
    </div>
  );
}

/** A row of choices where one is on. Arrow keys move between them, as in any radio group. */
export function Segmented<T extends string>({ label, value, options, onChange, className = "segmented" }: {
  label: string;
  className?: string;
  value: T;
  options: [T, string][];
  onChange: (v: T) => void;
}) {
  const onKeyDown = (e: KeyboardEvent<HTMLDivElement>) => {
    const step = e.key === "ArrowRight" || e.key === "ArrowDown" ? 1 : e.key === "ArrowLeft" || e.key === "ArrowUp" ? -1 : 0;
    if (!step) return;
    e.preventDefault();
    const i = (options.findIndex(([v]) => v === value) + step + options.length) % options.length;
    onChange(options[i][0]);
    e.currentTarget.querySelectorAll<HTMLButtonElement>("button")[i]?.focus();
  };
  return (
    <div className={className} role="radiogroup" aria-label={label} onKeyDown={onKeyDown} style={{ gridTemplateColumns: `repeat(${options.length}, minmax(0, 1fr))` }}>
      {options.map(([v, text]) => (
        <button key={v} type="button" role="radio" aria-checked={v === value} tabIndex={v === value ? 0 : -1} onClick={() => onChange(v)}>
          {text}
        </button>
      ))}
    </div>
  );
}

/** True while a CSS media query matches. */
export function useMedia(query: string): boolean {
  const [on, setOn] = useState(() => matchMedia(query).matches);
  useEffect(() => {
    const m = matchMedia(query);
    const change = () => setOn(m.matches);
    m.addEventListener("change", change);
    change();
    return () => m.removeEventListener("change", change);
  }, [query]);
  return on;
}

/** Hands a file made on this device to the browser's download. */
export function save(data: Uint8Array | Blob, name: string, note?: string) {
  const blob = data instanceof Blob ? data : new Blob([data], { type: "application/pdf" });
  const url = URL.createObjectURL(blob);
  const a = document.createElement("a");
  a.href = url;
  a.download = name;
  document.body.append(a);
  a.click();
  a.remove();
  window.setTimeout(() => URL.revokeObjectURL(url), 60_000);
  toast(note || `Saved ${name} (${size(blob.size)})`);
}

export function size(bytes: number): string {
  if (bytes < 1024) return `${bytes} B`;
  if (bytes < 1024 * 1024) return `${Math.round(bytes / 1024)} KB`;
  return `${(bytes / 1024 / 1024).toFixed(1)} MB`;
}

Change history

Quality pass: compress, PDF to JPG, phone layout, job links, llms.txt and index.md
0341fee613 · 6 hours ago
Bounded source sync
f00de5deb0 · 1 day ago
Bounded source sync
3c8124e576 · 1 day ago
Bounded source sync
5c14663e0f · 1 day ago
Bounded source sync
ee5c852f40 · 1 day ago