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:
@@ -98,7 +98,18 @@ Build, replace the copy in `/Applications`, and relaunch — one command:
|
|||||||
npm run ship
|
npm run ship
|
||||||
```
|
```
|
||||||
|
|
||||||
No disk image; nothing here is being distributed.
|
One build, two destinations, every time:
|
||||||
|
|
||||||
|
- `/Applications/Work.app` — the copy tested here
|
||||||
|
- `~/Desktop/Work-<version>.zip` — the copy to send someone else
|
||||||
|
|
||||||
|
Both come from the same build, so a tester runs byte for byte what was just verified
|
||||||
|
rather than a second build that drifted. The zip is made with `ditto`, not `zip`: a plain
|
||||||
|
zip mangles the bundle's symlinks and the app will not open on the other machine.
|
||||||
|
|
||||||
|
The app is ad-hoc signed, so the first launch on someone else's Mac needs
|
||||||
|
**right-click → Open** rather than a double-click. Gatekeeper refuses it silently
|
||||||
|
otherwise.
|
||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "work-app",
|
"name": "work-app",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.1.0",
|
"version": "0.0.1",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
|||||||
+18
-7
@@ -1,25 +1,30 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
#
|
#
|
||||||
# Build, install to /Applications, relaunch.
|
# Build once, then put the result in both places, every time:
|
||||||
#
|
#
|
||||||
# The standing workflow for seeing a change: this replaces the installed app
|
# /Applications/Work.app the copy being tested here
|
||||||
# rather than producing a disk image, because nothing here is being distributed.
|
# ~/Desktop/Work-<version>.zip the copy to send someone else
|
||||||
|
#
|
||||||
|
# One build, two destinations — so the thing a tester runs is byte for byte the
|
||||||
|
# thing that was just tested here, rather than a second build that drifted.
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
cd "$(dirname "$0")/.."
|
cd "$(dirname "$0")/.."
|
||||||
APP="Work.app"
|
APP="Work.app"
|
||||||
BUILT="src-tauri/target/release/bundle/macos/$APP"
|
BUILT="src-tauri/target/release/bundle/macos/$APP"
|
||||||
DEST="/Applications/$APP"
|
DEST="/Applications/$APP"
|
||||||
|
VERSION="$(node -p "require('./package.json').version")"
|
||||||
|
ZIP="$HOME/Desktop/Work-$VERSION.zip"
|
||||||
|
|
||||||
echo "==> building"
|
echo "==> building $VERSION"
|
||||||
npm run tauri build -- --bundles app
|
npm run tauri build -- --bundles app
|
||||||
|
|
||||||
echo "==> quitting the running copy"
|
echo "==> quitting the running copy"
|
||||||
osascript -e 'quit app "Work"' >/dev/null 2>&1 || true
|
osascript -e 'quit app "Work"' >/dev/null 2>&1 || true
|
||||||
# A hung window would otherwise keep the bundle busy while it is replaced.
|
# A hung window would otherwise keep the bundle busy while it is replaced.
|
||||||
pkill -x Work >/dev/null 2>&1 || true
|
pkill -f "Work.app/Contents/MacOS" >/dev/null 2>&1 || true
|
||||||
for _ in 1 2 3 4 5 6 7 8 9 10; do
|
for _ in 1 2 3 4 5 6 7 8 9 10; do
|
||||||
pgrep -x Work >/dev/null || break
|
pgrep -f "Work.app/Contents/MacOS" >/dev/null || break
|
||||||
sleep 0.3
|
sleep 0.3
|
||||||
done
|
done
|
||||||
|
|
||||||
@@ -27,6 +32,12 @@ echo "==> installing to $DEST"
|
|||||||
rm -rf "$DEST"
|
rm -rf "$DEST"
|
||||||
cp -R "$BUILT" "$DEST"
|
cp -R "$BUILT" "$DEST"
|
||||||
|
|
||||||
|
# ditto rather than zip: it keeps the bundle's symlinks and resource forks,
|
||||||
|
# which a plain zip mangles into an app that will not open on the other end.
|
||||||
|
echo "==> packaging to $ZIP"
|
||||||
|
rm -f "$ZIP"
|
||||||
|
ditto -c -k --sequesterRsrc --keepParent "$BUILT" "$ZIP"
|
||||||
|
|
||||||
echo "==> launching"
|
echo "==> launching"
|
||||||
open "$DEST"
|
open "$DEST"
|
||||||
echo "==> done"
|
echo "==> done — $DEST and $ZIP"
|
||||||
|
|||||||
Generated
+1
-1
@@ -4911,7 +4911,7 @@ checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "work-app"
|
name = "work-app"
|
||||||
version = "0.1.0"
|
version = "0.0.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"block2",
|
"block2",
|
||||||
"mac-notification-sys",
|
"mac-notification-sys",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "work-app"
|
name = "work-app"
|
||||||
version = "0.1.0"
|
version = "0.0.1"
|
||||||
description = "A browser for the tools you work in"
|
description = "A browser for the tools you work in"
|
||||||
authors = ["Vincent"]
|
authors = ["Vincent"]
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|||||||
@@ -767,6 +767,26 @@ pub fn app_reports(state: State<'_, AppState>) -> Vec<(String, String)> {
|
|||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Puts the app list, groups, hidden elements and zoom back to defaults.
|
||||||
|
///
|
||||||
|
/// Settings only. Sessions are left alone — they live in WebKit's own store,
|
||||||
|
/// not in `apps.json`, and throwing them away would mean signing back into
|
||||||
|
/// every tool to undo a change to the nav.
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn reset_config(app: AppHandle, state: State<'_, AppState>) -> Result<Config, String> {
|
||||||
|
let old = state.cfg();
|
||||||
|
for a in &old.apps {
|
||||||
|
webviews::destroy(&app, &a.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
*state.config.lock().unwrap() = config::seed();
|
||||||
|
*state.active.lock().unwrap() = None;
|
||||||
|
*state.booted.lock().unwrap() = false;
|
||||||
|
state.unread.lock().unwrap().clear();
|
||||||
|
state.persist()?;
|
||||||
|
Ok(state.cfg())
|
||||||
|
}
|
||||||
|
|
||||||
// ------------------------------------------------------------ passwords
|
// ------------------------------------------------------------ passwords
|
||||||
|
|
||||||
/// Writes the offered login to the macOS Keychain.
|
/// Writes the offered login to the macOS Keychain.
|
||||||
|
|||||||
@@ -114,6 +114,7 @@ pub fn run() {
|
|||||||
commands::delete_group,
|
commands::delete_group,
|
||||||
commands::set_nav_collapsed,
|
commands::set_nav_collapsed,
|
||||||
commands::set_theme,
|
commands::set_theme,
|
||||||
|
commands::reset_config,
|
||||||
commands::unread_counts,
|
commands::unread_counts,
|
||||||
commands::focus_window,
|
commands::focus_window,
|
||||||
commands::save_password,
|
commands::save_password,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://schema.tauri.app/config/2",
|
"$schema": "https://schema.tauri.app/config/2",
|
||||||
"productName": "Work",
|
"productName": "Work",
|
||||||
"version": "0.1.0",
|
"version": "0.0.1",
|
||||||
"identifier": "com.vincent.workapp",
|
"identifier": "com.vincent.workapp",
|
||||||
"build": {
|
"build": {
|
||||||
"beforeDevCommand": "npm run dev",
|
"beforeDevCommand": "npm run dev",
|
||||||
@@ -22,11 +22,15 @@
|
|||||||
"hiddenTitle": true
|
"hiddenTitle": true
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"security": { "csp": null }
|
"security": {
|
||||||
|
"csp": null
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"bundle": {
|
"bundle": {
|
||||||
"active": true,
|
"active": true,
|
||||||
"targets": ["app"],
|
"targets": [
|
||||||
|
"app"
|
||||||
|
],
|
||||||
"icon": [
|
"icon": [
|
||||||
"icons/32x32.png",
|
"icons/32x32.png",
|
||||||
"icons/128x128.png",
|
"icons/128x128.png",
|
||||||
|
|||||||
+12
@@ -271,6 +271,18 @@ export default function App() {
|
|||||||
onConfig={setConfig}
|
onConfig={setConfig}
|
||||||
onTheme={onTheme}
|
onTheme={onTheme}
|
||||||
onClose={() => { setSettingsOpen(false); setFocusHidden(null); }}
|
onClose={() => { setSettingsOpen(false); setFocusHidden(null); }}
|
||||||
|
onReset={async () => {
|
||||||
|
// Rust closed every view and forgot which app was showing, so the
|
||||||
|
// shell has to build the defaults again from nothing.
|
||||||
|
const fresh = await api.getConfig();
|
||||||
|
setConfig(fresh);
|
||||||
|
setUnread({});
|
||||||
|
booted.current = false;
|
||||||
|
const first = [...fresh.apps].sort((a, b) => a.order - b.order)[0];
|
||||||
|
setActiveId(first?.id ?? null);
|
||||||
|
report();
|
||||||
|
await api.bootstrap();
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -60,3 +60,4 @@ export const unreadCounts = () => invoke<[string, number][]>("unread_counts");
|
|||||||
export const stageSnapshot = () => invoke<string | null>("stage_snapshot");
|
export const stageSnapshot = () => invoke<string | null>("stage_snapshot");
|
||||||
export const savePassword = () => invoke<string>("save_password");
|
export const savePassword = () => invoke<string>("save_password");
|
||||||
export const discardPassword = () => invoke<void>("discard_password");
|
export const discardPassword = () => invoke<void>("discard_password");
|
||||||
|
export const resetConfig = () => invoke<Config>("reset_config");
|
||||||
|
|||||||
+136
-87
@@ -6,6 +6,7 @@ import type { Config, Group, WorkApp } from "../types";
|
|||||||
import { Trash } from "./icons";
|
import { Trash } from "./icons";
|
||||||
import {
|
import {
|
||||||
BTN,
|
BTN,
|
||||||
|
BTN_QUIET,
|
||||||
BTN_PRIMARY,
|
BTN_PRIMARY,
|
||||||
Badge,
|
Badge,
|
||||||
Dialog,
|
Dialog,
|
||||||
@@ -29,10 +30,12 @@ interface Props {
|
|||||||
onConfig: (c: Config) => void;
|
onConfig: (c: Config) => void;
|
||||||
onTheme: (t: Theme) => void;
|
onTheme: (t: Theme) => void;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
|
/** Settings were reset; the shell has to rebuild every app's view. */
|
||||||
|
onReset: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Settings({
|
export default function Settings({
|
||||||
config, theme, activeId, focusHiddenFor, onConfig, onTheme, onClose,
|
config, theme, activeId, focusHiddenFor, onConfig, onTheme, onClose, onReset,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
const [name, setName] = useState("");
|
const [name, setName] = useState("");
|
||||||
const [url, setUrl] = useState("");
|
const [url, setUrl] = useState("");
|
||||||
@@ -40,6 +43,8 @@ export default function Settings({
|
|||||||
const [groupName, setGroupName] = useState("");
|
const [groupName, setGroupName] = useState("");
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [confirmDelete, setConfirmDelete] = useState<WorkApp | 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 [notifyStatus, setNotifyStatus] = useState<string | null>(null);
|
||||||
const [reports, setReports] = useState<[string, string][]>([]);
|
const [reports, setReports] = useState<[string, string][]>([]);
|
||||||
@@ -56,7 +61,7 @@ export default function Settings({
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!focusHiddenFor) return;
|
if (!focusHiddenFor) return;
|
||||||
document.getElementById("hidden-elements")?.scrollIntoView({ behavior: "smooth" });
|
setHiddenOpen(true);
|
||||||
}, [focusHiddenFor]);
|
}, [focusHiddenFor]);
|
||||||
|
|
||||||
const run = async (fn: () => Promise<Config>) => {
|
const run = async (fn: () => Promise<Config>) => {
|
||||||
@@ -86,6 +91,7 @@ export default function Settings({
|
|||||||
|
|
||||||
|
|
||||||
const withHidden = orderedApps.filter((a) => a.hidden.length > 0);
|
const withHidden = orderedApps.filter((a) => a.hidden.length > 0);
|
||||||
|
const totalHidden = withHidden.reduce((n, a) => n + a.hidden.length, 0);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -170,58 +176,59 @@ export default function Settings({
|
|||||||
</p>
|
</p>
|
||||||
</section>
|
</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 */}
|
{/* ------------------------------------------- hidden elements */}
|
||||||
<section id="hidden-elements" className="space-y-2">
|
<section className="space-y-2">
|
||||||
<SectionHeading>Hidden elements</SectionHeading>
|
<div className="flex items-center justify-between">
|
||||||
{withHidden.length === 0 ? (
|
<SectionHeading>Hidden elements</SectionHeading>
|
||||||
<p className={HELP}>
|
<button onClick={() => setHiddenOpen(true)} className={BTN}>
|
||||||
Nothing hidden. Right-click anything in an app and choose <em>Hide this
|
{totalHidden === 0 ? "None hidden" : `Manage ${totalHidden} hidden`}
|
||||||
element</em> — or use the eye button in the nav to pick one.
|
</button>
|
||||||
</p>
|
</div>
|
||||||
) : (
|
|
||||||
<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}>
|
<p className={HELP}>
|
||||||
Changes apply immediately, without a reload. A selector that stops matching
|
Right-click anything in an app and choose <em>Hide this element</em>. The
|
||||||
after the site changes simply hides nothing.
|
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>
|
</p>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
|
||||||
{/* ------------------------------------------------------ zoom */}
|
{/* ------------------------------------------------------ zoom */}
|
||||||
<section className="space-y-2">
|
<section className="space-y-2">
|
||||||
<SectionHeading>Zoom</SectionHeading>
|
<SectionHeading>Zoom</SectionHeading>
|
||||||
@@ -305,43 +312,6 @@ export default function Settings({
|
|||||||
</p>
|
</p>
|
||||||
</section>
|
</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 */}
|
{/* ----------------------------------------------- appearance */}
|
||||||
<section className="space-y-2">
|
<section className="space-y-2">
|
||||||
<SectionHeading>Appearance</SectionHeading>
|
<SectionHeading>Appearance</SectionHeading>
|
||||||
@@ -356,12 +326,91 @@ export default function Settings({
|
|||||||
/>
|
/>
|
||||||
</section>
|
</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>
|
<button onClick={onClose} className={BTN}>Done</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Dialog>
|
</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 && (
|
{confirmDelete && (
|
||||||
<Dialog
|
<Dialog
|
||||||
title={`Remove ${confirmDelete.name}?`}
|
title={`Remove ${confirmDelete.name}?`}
|
||||||
|
|||||||
Reference in New Issue
Block a user