Ship to Desktop and /Applications; restructure Settings; add reset
Version pinned to 0.0.1 and staying on 0.x while this is in beta. `npm run ship` now exports twice from one build: /Applications/Work.app to test here, and ~/Desktop/Work-<version>.zip to send someone. One build, so a tester runs byte for byte what was verified rather than a second build that drifted. Packed with ditto rather than zip - a plain zip mangles the bundle's symlinks and it will not open on the far end. Settings: groups move up under apps, and the hidden-element list moves into its own window behind a one-line summary. It had grown to 21 selectors of framework class names and was most of the page, in front of the things people actually open Settings for. Reset puts apps, groups, hidden elements and zoom back to defaults, and leaves sessions alone - those live in WebKit's own store, and throwing them away would mean signing back into every tool to undo a change to the nav.
This commit is contained in:
+136
-87
@@ -6,6 +6,7 @@ import type { Config, Group, WorkApp } from "../types";
|
||||
import { Trash } from "./icons";
|
||||
import {
|
||||
BTN,
|
||||
BTN_QUIET,
|
||||
BTN_PRIMARY,
|
||||
Badge,
|
||||
Dialog,
|
||||
@@ -29,10 +30,12 @@ interface Props {
|
||||
onConfig: (c: Config) => void;
|
||||
onTheme: (t: Theme) => void;
|
||||
onClose: () => void;
|
||||
/** Settings were reset; the shell has to rebuild every app's view. */
|
||||
onReset: () => void;
|
||||
}
|
||||
|
||||
export default function Settings({
|
||||
config, theme, activeId, focusHiddenFor, onConfig, onTheme, onClose,
|
||||
config, theme, activeId, focusHiddenFor, onConfig, onTheme, onClose, onReset,
|
||||
}: Props) {
|
||||
const [name, setName] = useState("");
|
||||
const [url, setUrl] = useState("");
|
||||
@@ -40,6 +43,8 @@ export default function Settings({
|
||||
const [groupName, setGroupName] = useState("");
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [confirmDelete, setConfirmDelete] = useState<WorkApp | null>(null);
|
||||
const [hiddenOpen, setHiddenOpen] = useState(false);
|
||||
const [confirmReset, setConfirmReset] = useState(false);
|
||||
|
||||
const [notifyStatus, setNotifyStatus] = useState<string | null>(null);
|
||||
const [reports, setReports] = useState<[string, string][]>([]);
|
||||
@@ -56,7 +61,7 @@ export default function Settings({
|
||||
|
||||
useEffect(() => {
|
||||
if (!focusHiddenFor) return;
|
||||
document.getElementById("hidden-elements")?.scrollIntoView({ behavior: "smooth" });
|
||||
setHiddenOpen(true);
|
||||
}, [focusHiddenFor]);
|
||||
|
||||
const run = async (fn: () => Promise<Config>) => {
|
||||
@@ -86,6 +91,7 @@ export default function Settings({
|
||||
|
||||
|
||||
const withHidden = orderedApps.filter((a) => a.hidden.length > 0);
|
||||
const totalHidden = withHidden.reduce((n, a) => n + a.hidden.length, 0);
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -170,58 +176,59 @@ export default function Settings({
|
||||
</p>
|
||||
</section>
|
||||
|
||||
{/* ---------------------------------------------------- groups */}
|
||||
<section className="space-y-2">
|
||||
<SectionHeading>Groups</SectionHeading>
|
||||
<div className="space-y-1.5">
|
||||
{groups.length === 0 && <p className={HELP}>No groups yet.</p>}
|
||||
{groups.map((g: Group) => (
|
||||
<div key={g.id}
|
||||
className="flex items-center gap-2 rounded-lg border border-slate-200 px-2 py-1.5 dark:border-slate-800">
|
||||
<input
|
||||
value={g.name}
|
||||
onChange={(e) => run(() => api.updateGroup({ ...g, name: e.target.value }))}
|
||||
className={`${INPUT} h-[26px] w-full flex-1`}
|
||||
/>
|
||||
<span className="shrink-0 font-mono text-[10px] text-slate-400">
|
||||
{config.apps.filter((a) => a.groupId === g.id).length} apps
|
||||
</span>
|
||||
<button
|
||||
onClick={() => run(() => api.deleteGroup(g.id))}
|
||||
title={`Delete ${g.name} — its apps stay, ungrouped`}
|
||||
className={`${ICON_CHROME} hover:text-red-500!`}
|
||||
>
|
||||
<Trash />
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className={`${SUBPANEL} flex items-end gap-2`}>
|
||||
<label className="flex-1 space-y-1">
|
||||
<span className={LABEL}>New group</span>
|
||||
<input value={groupName} onChange={(e) => setGroupName(e.target.value)}
|
||||
onKeyDown={(e) => e.key === "Enter" && addGroup()}
|
||||
placeholder="Finance" className={`${INPUT} w-full`} />
|
||||
</label>
|
||||
<button onClick={addGroup} className={BTN}>Add group</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* ------------------------------------------- hidden elements */}
|
||||
<section id="hidden-elements" className="space-y-2">
|
||||
<SectionHeading>Hidden elements</SectionHeading>
|
||||
{withHidden.length === 0 ? (
|
||||
<p className={HELP}>
|
||||
Nothing hidden. Right-click anything in an app and choose <em>Hide this
|
||||
element</em> — or use the eye button in the nav to pick one.
|
||||
</p>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
{withHidden.map((app) => (
|
||||
<div key={app.id} className={SUBPANEL}>
|
||||
<div className="mb-2 flex items-center gap-2">
|
||||
<Favicon icon={app.icon} name={app.name} />
|
||||
<span className="text-[12px] font-medium">{app.name}</span>
|
||||
<Badge tone="accent">{app.hidden.length}</Badge>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
{app.hidden.map((sel, i) => (
|
||||
<div key={i} className="flex items-center gap-2">
|
||||
<input
|
||||
value={sel}
|
||||
onChange={(e) => {
|
||||
const next = [...app.hidden];
|
||||
next[i] = e.target.value;
|
||||
run(() => api.setHidden(app.id, next));
|
||||
}}
|
||||
title="Edit the selector to widen or narrow what it hides"
|
||||
className={`${INPUT} h-[26px] w-full flex-1 font-mono text-[11px]`}
|
||||
/>
|
||||
<button
|
||||
onClick={() =>
|
||||
run(() => api.setHidden(app.id, app.hidden.filter((_, j) => j !== i)))
|
||||
}
|
||||
title="Show this again"
|
||||
className={`${ICON_CHROME} hover:text-red-500!`}
|
||||
>
|
||||
<Trash />
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<section className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<SectionHeading>Hidden elements</SectionHeading>
|
||||
<button onClick={() => setHiddenOpen(true)} className={BTN}>
|
||||
{totalHidden === 0 ? "None hidden" : `Manage ${totalHidden} hidden`}
|
||||
</button>
|
||||
</div>
|
||||
<p className={HELP}>
|
||||
Changes apply immediately, without a reload. A selector that stops matching
|
||||
after the site changes simply hides nothing.
|
||||
Right-click anything in an app and choose <em>Hide this element</em>. The
|
||||
full list opens in its own window — there is usually a lot of it, and it is
|
||||
not what you came to Settings for.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
|
||||
{/* ------------------------------------------------------ zoom */}
|
||||
<section className="space-y-2">
|
||||
<SectionHeading>Zoom</SectionHeading>
|
||||
@@ -305,43 +312,6 @@ export default function Settings({
|
||||
</p>
|
||||
</section>
|
||||
|
||||
{/* ---------------------------------------------------- groups */}
|
||||
<section className="space-y-2">
|
||||
<SectionHeading>Groups</SectionHeading>
|
||||
<div className="space-y-1.5">
|
||||
{groups.length === 0 && <p className={HELP}>No groups yet.</p>}
|
||||
{groups.map((g: Group) => (
|
||||
<div key={g.id}
|
||||
className="flex items-center gap-2 rounded-lg border border-slate-200 px-2 py-1.5 dark:border-slate-800">
|
||||
<input
|
||||
value={g.name}
|
||||
onChange={(e) => run(() => api.updateGroup({ ...g, name: e.target.value }))}
|
||||
className={`${INPUT} h-[26px] w-full flex-1`}
|
||||
/>
|
||||
<span className="shrink-0 font-mono text-[10px] text-slate-400">
|
||||
{config.apps.filter((a) => a.groupId === g.id).length} apps
|
||||
</span>
|
||||
<button
|
||||
onClick={() => run(() => api.deleteGroup(g.id))}
|
||||
title={`Delete ${g.name} — its apps stay, ungrouped`}
|
||||
className={`${ICON_CHROME} hover:text-red-500!`}
|
||||
>
|
||||
<Trash />
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className={`${SUBPANEL} flex items-end gap-2`}>
|
||||
<label className="flex-1 space-y-1">
|
||||
<span className={LABEL}>New group</span>
|
||||
<input value={groupName} onChange={(e) => setGroupName(e.target.value)}
|
||||
onKeyDown={(e) => e.key === "Enter" && addGroup()}
|
||||
placeholder="Finance" className={`${INPUT} w-full`} />
|
||||
</label>
|
||||
<button onClick={addGroup} className={BTN}>Add group</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* ----------------------------------------------- appearance */}
|
||||
<section className="space-y-2">
|
||||
<SectionHeading>Appearance</SectionHeading>
|
||||
@@ -356,12 +326,91 @@ export default function Settings({
|
||||
/>
|
||||
</section>
|
||||
|
||||
<div className="flex justify-end border-t border-slate-200 pt-4 dark:border-slate-800">
|
||||
<div className="flex items-center justify-between border-t border-slate-200 pt-4 dark:border-slate-800">
|
||||
<button
|
||||
onClick={() => setConfirmReset(true)}
|
||||
title="Put apps, groups, hidden elements and zoom back to defaults"
|
||||
className={BTN_QUIET}
|
||||
>
|
||||
Reset to defaults
|
||||
</button>
|
||||
<button onClick={onClose} className={BTN}>Done</button>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
|
||||
{hiddenOpen && (
|
||||
<Dialog title="Hidden elements" onCancel={() => setHiddenOpen(false)} wide>
|
||||
<div className="space-y-3">
|
||||
{withHidden.length === 0 ? (
|
||||
<p className={HELP}>
|
||||
Nothing hidden. Right-click anything in an app and choose <em>Hide this
|
||||
element</em> — or use the eye button in the nav to pick one.
|
||||
</p>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
{withHidden.map((app) => (
|
||||
<div key={app.id} className={SUBPANEL}>
|
||||
<div className="mb-2 flex items-center gap-2">
|
||||
<Favicon icon={app.icon} name={app.name} />
|
||||
<span className="text-[12px] font-medium">{app.name}</span>
|
||||
<Badge tone="accent">{app.hidden.length}</Badge>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
{app.hidden.map((sel, i) => (
|
||||
<div key={i} className="flex items-center gap-2">
|
||||
<input
|
||||
value={sel}
|
||||
onChange={(e) => {
|
||||
const next = [...app.hidden];
|
||||
next[i] = e.target.value;
|
||||
run(() => api.setHidden(app.id, next));
|
||||
}}
|
||||
title="Edit the selector to widen or narrow what it hides"
|
||||
className={`${INPUT} h-[26px] w-full flex-1 font-mono text-[11px]`}
|
||||
/>
|
||||
<button
|
||||
onClick={() =>
|
||||
run(() => api.setHidden(app.id, app.hidden.filter((_, j) => j !== i)))
|
||||
}
|
||||
title="Show this again"
|
||||
className={`${ICON_CHROME} hover:text-red-500!`}
|
||||
>
|
||||
<Trash />
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<p className={HELP}>
|
||||
Changes apply immediately, without a reload. A selector that stops matching
|
||||
after the site changes simply hides nothing.
|
||||
</p>
|
||||
</div>
|
||||
</Dialog>
|
||||
)}
|
||||
|
||||
{confirmReset && (
|
||||
<Dialog
|
||||
title="Reset to defaults?"
|
||||
onCancel={() => setConfirmReset(false)}
|
||||
onConfirm={async () => {
|
||||
await run(() => api.resetConfig());
|
||||
setConfirmReset(false);
|
||||
onReset();
|
||||
}}
|
||||
confirmLabel="Reset"
|
||||
destructive
|
||||
>
|
||||
Your apps, groups, hidden elements and zoom go back to the defaults. You stay
|
||||
signed in to everything — sessions live in the browser engine's own store, not
|
||||
in this app's settings, and are not touched.
|
||||
</Dialog>
|
||||
)}
|
||||
|
||||
{confirmDelete && (
|
||||
<Dialog
|
||||
title={`Remove ${confirmDelete.name}?`}
|
||||
|
||||
Reference in New Issue
Block a user