still-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 0fa0e84d537b
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 a8c677472ab5691741798a3d
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, copy, share, icons, choice chips and the mood check-in.
import { useEffect, useState, type ReactNode } from "react";
import { MOODS } from "./lib/progress";
import { onLink } from "./lib/router";

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

export function Toaster() {
  const [text, setText] = useState<string | null>(null);
  useEffect(() => {
    let timer: number | undefined;
    pushToast = (t) => {
      setText(t);
      window.clearTimeout(timer);
      timer = window.setTimeout(() => setText(null), 2600);
    };
    return () => { pushToast = null; window.clearTimeout(timer); };
  }, []);
  return text ? <div className="toast" role="status">{text}</div> : null;
}

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) {
  if (navigator.share && matchMedia("(pointer: coarse)").matches) {
    try { await navigator.share({ title, url }); return; } catch { /* cancelled */ }
  }
  await copy(url, "Link copied");
}

/** An in-app link: a real <a href> that changes the page without reloading. */
export function A({ href, className, children, ...rest }: { href: string; className?: string; children: ReactNode; "aria-label"?: string; "aria-current"?: "page" | undefined }) {
  return <a href={href} className={className} onClick={onLink} {...rest}>{children}</a>;
}

const PATHS: Record<string, ReactNode> = {
  today: <><circle cx="12" cy="12" r="4" /><path d="M12 2.5v2.2M12 19.3v2.2M2.5 12h2.2M19.3 12h2.2M5.3 5.3l1.6 1.6M17.1 17.1l1.6 1.6M5.3 18.7l1.6-1.6M17.1 6.9l1.6-1.6" /></>,
  breathe: <><circle cx="12" cy="12" r="8.5" /><circle cx="12" cy="12" r="4.5" /></>,
  meditate: <><circle cx="12" cy="6" r="2.4" /><path d="M5 19.5c2-.9 4.4-1.3 7-1.3s5 .4 7 1.3M8 15.5l2-4.5h4l2 4.5M7 15.5h10" /></>,
  sounds: <path d="M3 12h1M7 8v8M11 5v14M15 9v6M19 7v10" />,
  sleep: <path d="M19.5 14.5A8 8 0 0 1 9.5 4.5a8 8 0 1 0 10 10Z" />,
  progress: <path d="M4 20V10M10 20V4M16 20v-7M22 20H2" />,
  play: <path d="M8 5.5v13l11-6.5-11-6.5Z" fill="currentColor" stroke="none" />,
  pause: <path d="M8 5v14M16 5v14" strokeWidth="3" />,
  close: <path d="M6 6l12 12M18 6 6 18" />,
  back: <path d="M15 5l-7 7 7 7" />,
  prev: <path d="M17 6v12L9 12l8-6ZM7 6v12" />,
  next: <path d="M7 6v12l8-6-8-6ZM17 6v12" />,
  moon: <path d="M19.5 14.5A8 8 0 0 1 9.5 4.5a8 8 0 1 0 10 10Z" />,
  share: <path d="M12 3v12M7 8l5-5 5 5M5 13v6h14v-6" />,
  plus: <path d="M12 5v14M5 12h14" />,
  minus: <path d="M5 12h14" />,
};

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

/** A row of choices where one is picked, like lengths in minutes. */
export function Chips<T extends string | number>({ label, options, value, onChange, format }: { label: string; options: T[]; value: T; onChange: (v: T) => void; format?: (v: T) => string }) {
  return (
    <div className="chips" role="radiogroup" aria-label={label}>
      {options.map((o) => (
        <button key={String(o)} type="button" role="radio" aria-checked={o === value} className="chip-btn" onClick={() => onChange(o)}>
          {format ? format(o) : String(o)}
        </button>
      ))}
    </div>
  );
}

export function Toggle({ label, checked, onChange, hint }: { label: string; checked: boolean; onChange: (v: boolean) => void; hint?: string }) {
  return (
    <label className="toggle">
      <span className="toggle-text"><span>{label}</span>{hint && <small>{hint}</small>}</span>
      <input type="checkbox" role="switch" checked={checked} onChange={(e) => onChange(e.target.checked)} />
      <span className="toggle-track" aria-hidden="true" />
    </label>
  );
}

/** The quiet mood check-in: five steps from very tense to very calm. Optional; kept on this device. */
export function Mood({ label, value, onChange }: { label: string; value?: number; onChange: (v: number) => void }) {
  return (
    <fieldset className="mood">
      <legend>{label}</legend>
      <div className="mood-row">
        {MOODS.map((m, i) => (
          <button key={m} type="button" className="mood-dot" aria-pressed={value === i + 1} onClick={() => onChange(i + 1)}>
            <span className="mood-mark" style={{ ["--s" as string]: String(0.5 + i * 0.125) }} aria-hidden="true" />
            <span className="mood-word">{m}</span>
          </button>
        ))}
      </div>
    </fieldset>
  );
}

/** A length of time in whole minutes; `up` rounds up, for time left. */
export const fmtMinutes = (sec: number, up = false) => `${Math.max(up ? 0 : 1, up ? Math.ceil(sec / 60) : Math.round(sec / 60))} min`;

export const fmtClock = (sec: number) => {
  const s = Math.max(0, Math.ceil(sec));
  const h = Math.floor(s / 3600);
  const m = Math.floor((s % 3600) / 60);
  const r = String(s % 60).padStart(2, "0");
  return h ? `${h}:${String(m).padStart(2, "0")}:${r}` : `${m}:${r}`;
};

/** Whether the person asked for less motion. */
export function useReducedMotion(): boolean {
  const [reduced, setReduced] = useState(() => matchMedia("(prefers-reduced-motion: reduce)").matches);
  useEffect(() => {
    const q = matchMedia("(prefers-reduced-motion: reduce)");
    const on = () => setReduced(q.matches);
    q.addEventListener("change", on);
    return () => q.removeEventListener("change", on);
  }, []);
  return reduced;
}

Change history

Link to OpenApps pages and add a contribution strip
0fa0e84d53 · 19 hours ago
Bind the agent address in the policy
6f3baa1c72 · 1 day ago
Add favicon.ico so browsers that ask for it stop getting a 404
1a004fe0e3 · 1 day ago
Bounded source sync
afe642fe50 · 2 days ago
Bounded source sync
2c581f55ea · 2 days ago
Bounded source sync
375934c28b · 2 days ago
Bounded source sync
74ff37d51a · 2 days ago