@supersuit/suggest-a-change 0.1.7 → 0.1.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +4 -0
- package/README.md +25 -1
- package/dist/client/entry.d.ts +24 -1
- package/dist/client/entry.js +42 -12
- package/dist/client/seen.js +2 -1
- package/package.json +1 -1
- package/src/client/entry.tsx +47 -14
- package/src/client/seen.ts +2 -1
- package/src/client/styles.css +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.1.8 (2026-10-17)
|
|
4
|
+
|
|
5
|
+
- **The news reaches a person whose entry sits in a menu.** In the Cofounders and Us apps the entry lives in a three-dot menu that stays closed and remounts it on every open, so the 0.1.7 dot was never seen and the toast was drawn inside the closed menu. New in `./client`: `<SuggestNews adapters>`, mounted once at the root, shows the toast with no entry on screen (and keeps the outbox moving); `useSuggestNews(adapters)` returns `{ moved }` for the menu's trigger; `<SuggestDot adapters moved>` draws the entry's dot there (nothing at 0), and the class `sac-dot-host` gives the trigger the `position: relative` it needs. The toast is once per page load and account however many times the entry mounts, whichever of these asks first. `<SuggestEntry>` keeps its own dot and, alone, behaves as in 0.1.7.
|
|
6
|
+
|
|
3
7
|
## 0.1.7 (2026-10-17)
|
|
4
8
|
|
|
5
9
|
- **The person who suggested a change is told when it is being made and when it ends.** `createReportHandlers` takes a required `tellSubmitter(e)`, called once per move to `being-made` or an end (`TOLD`, now in the contract) with `{ member, reportId, status, words, meaning, ended, note? }`. The words are the module's own (`wordsFor`/`meaningFor`), never the report's text; `note` is the one-line reason, on `not-now` only. The planning steps, a repeat, a refused move and a maintainer's own report starting tell nobody, and a failure never fails the move. **Breaking:** without `tellSubmitter` the handlers throw at startup; an app with nobody to tell passes `{ none: "<reason>" }`. The `status` tell event also carries `member` and `status`, and a `not-now` row in the member's list carries its `note`.
|
package/README.md
CHANGED
|
@@ -73,7 +73,31 @@ The four diagnostics are the app's version, the time, the screen size and the ph
|
|
|
73
73
|
|
|
74
74
|
**Identity.** Every screen asks `adapters.identity()` when it mounts, whenever it is handed a different `adapters` object, and whenever the host calls `refreshIdentity()` (exported from `"./client"`; call it after a sign-in, sign-out or account switch). When the account differs, the old account's rows leave the screen and its waiting reports stay on the phone, to be sent only while that account is signed in; every send re-confirms the account first. Keep `adapters` stable where you can; a parent that rebuilds it every render is safe, only slower (one `identity()` call per render).
|
|
75
75
|
|
|
76
|
-
**Telling the person in the app, with no setup.** The entry asks `mine()` once when it mounts and compares each report's status with the one this account last opened (kept on the phone beside the cached views, per app and account, and cleared on sign-out). A report that has since been started or ended (`TOLD`) shows a dot on the entry, says New on its row in My suggestions, and the newest such move gets one toast per
|
|
76
|
+
**Telling the person in the app, with no setup.** The entry asks `mine()` once when it mounts and compares each report's status with the one this account last opened (kept on the phone beside the cached views, per app and account, and cleared on sign-out). A report that has since been started or ended (`TOLD`) shows a dot on the entry, says New on its row in My suggestions, and the newest such move gets one toast per page load in the module's words (`words.text.movedToast`, given the status's own word and, on `not-now`, the reason: "Your suggestion: Done", "Your suggestion: Not now. too big for now"). Opening the report clears it. A report this phone sees for the first time is recorded silently, so an app taking this release does not announce every old report. The list's `not-now` rows carry their one-line `note` for this. Out of the app, the server's `tellSubmitter` does the telling (below).
|
|
77
|
+
|
|
78
|
+
**When the entry sits in a menu.** A host that shows the entry inside a menu that stays closed (a three-dot menu, often remounting its rows on every open) would never show the dot, and the toast would be drawn inside the closed menu. Mount `<SuggestNews>` once at the app's root, always mounted: it asks `mine()` when it mounts, shows the page load's one toast with no entry on screen, and keeps the outbox moving. Put the dot on the menu's own trigger with `useSuggestNews(adapters)`, which returns `{ moved }`, and `<SuggestDot>`, which draws the entry's dot (nothing when `moved` is 0). The trigger needs `position: relative`; the class `sac-dot-host` does it. The toast is once per page load and account however many times the entry mounts, whichever of these asks first. The entry in the menu keeps its own dot.
|
|
79
|
+
|
|
80
|
+
```tsx
|
|
81
|
+
import { SuggestDot, SuggestEntry, SuggestNews, useSuggestNews } from "@supersuit/suggest-a-change/client";
|
|
82
|
+
|
|
83
|
+
// once, at the root
|
|
84
|
+
<SuggestNews adapters={adapters} />
|
|
85
|
+
|
|
86
|
+
// the menu's trigger
|
|
87
|
+
function MoreButton({ onClick }: { onClick(): void }) {
|
|
88
|
+
const { moved } = useSuggestNews(adapters);
|
|
89
|
+
return (
|
|
90
|
+
<button type="button" className="sac-dot-host" aria-label="More" onClick={onClick}>
|
|
91
|
+
...<SuggestDot adapters={adapters} moved={moved} />
|
|
92
|
+
</button>
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// inside the menu, as before
|
|
97
|
+
<SuggestEntry adapters={adapters} role="menuitem" onOpen={() => setOpen(true)} />
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
`<SuggestNews>` also takes `children`, a function given `{ moved }`, for a root that draws the trigger itself. A host using only the hook renders `<Toasts />` somewhere always mounted so the toast has a place to show.
|
|
77
101
|
|
|
78
102
|
**Follow-ups.** Words and voice notes added to a report afterwards are queued in the outbox like the report (a `followUp` write, then one part per voice note), keep the app's refusal with a Try again, and delete their local copy only once they have landed. `StoreAdapter.append` must therefore dedupe by report id and `more.followId`.
|
|
79
103
|
|
package/dist/client/entry.d.ts
CHANGED
|
@@ -1,10 +1,33 @@
|
|
|
1
|
-
import { type AriaRole } from 'react';
|
|
1
|
+
import { type AriaRole, type ReactNode } from 'react';
|
|
2
2
|
import type { Adapters } from './adapters.ts';
|
|
3
3
|
/** Keeps the phone's outbox moving (on reconnect and every 15 seconds) for as long as it is mounted, with nothing drawn.
|
|
4
4
|
* <SuggestEntry> already does this; a host with no always-mounted entry mounts this once, high in the app. */
|
|
5
5
|
export declare function KeepSending({ adapters }: {
|
|
6
6
|
adapters: Adapters;
|
|
7
7
|
}): null;
|
|
8
|
+
/** For a host that shows the entry where it is not always visible (inside a menu): call this where the menu's trigger is
|
|
9
|
+
* drawn. It asks the app once per mount and returns how many of this person's suggestions moved since they last opened
|
|
10
|
+
* them (draw `<SuggestDot>` with it). The page load's one toast is shown by whichever of this, `<SuggestNews>` or the
|
|
11
|
+
* entry asks first; a host using only this hook must also render `<Toasts />` somewhere always mounted. */
|
|
12
|
+
export declare function useSuggestNews(adapters: Adapters): {
|
|
13
|
+
moved: number;
|
|
14
|
+
};
|
|
15
|
+
/** Mount once at the app's root, always mounted: shows the page load's one toast when a suggestion moved, with no entry
|
|
16
|
+
* on screen, and keeps the outbox moving (as `<KeepSending>` does). `children`, if given, receives `{ moved }`. */
|
|
17
|
+
export declare function SuggestNews({ adapters, children }: {
|
|
18
|
+
adapters: Adapters;
|
|
19
|
+
children?: (news: {
|
|
20
|
+
moved: number;
|
|
21
|
+
}) => ReactNode;
|
|
22
|
+
}): import("react").JSX.Element;
|
|
23
|
+
/** The entry's dot, for a host's own trigger (the three-dot button of a menu the entry sits in). Draws nothing when
|
|
24
|
+
* `moved` is 0. It is positioned like the entry's dot, so the trigger needs `position: relative` (the class
|
|
25
|
+
* `sac-dot-host` in the module's styles does it). */
|
|
26
|
+
export declare function SuggestDot({ adapters, moved: count, className }: {
|
|
27
|
+
adapters: Adapters;
|
|
28
|
+
moved: number;
|
|
29
|
+
className?: string;
|
|
30
|
+
}): import("react").JSX.Element | null;
|
|
8
31
|
export declare function SuggestEntry({ adapters, onOpen, className, role }: {
|
|
9
32
|
adapters: Adapters;
|
|
10
33
|
onOpen(): void;
|
package/dist/client/entry.js
CHANGED
|
@@ -16,18 +16,14 @@ export function KeepSending({ adapters }) {
|
|
|
16
16
|
useRuntime(adapters);
|
|
17
17
|
return null;
|
|
18
18
|
}
|
|
19
|
-
|
|
19
|
+
// THE NEWS: once per mount, asks the app for this person's reports and compares each with what they last opened. A move
|
|
20
|
+
// since then is counted (the dot), and the newest one gets the page load's one toast, whichever of the entry or a
|
|
21
|
+
// root-mounted SuggestNews asks first (the guard is per page load and account, in seen.ts). Opening a report anywhere
|
|
22
|
+
// recounts. `toasted` says this mount showed the toast, so a standalone entry knows to draw a toast host for it.
|
|
23
|
+
function useNews(adapters, rt) {
|
|
20
24
|
const S = stringsFor(adapters.words);
|
|
21
|
-
const [why, setWhy] = useState('');
|
|
22
|
-
const [busy, setBusy] = useState(false);
|
|
23
|
-
// The runtime keeps the outbox moving while the entry is mounted (what <KeepSending> does).
|
|
24
|
-
const { rt, who } = useRuntime(adapters);
|
|
25
25
|
const [news, setNews] = useState(0);
|
|
26
|
-
const [
|
|
27
|
-
useEffect(() => { setWhy(''); }, [adapters]);
|
|
28
|
-
useClearOnSignOut(rt, who, adapters.words.app);
|
|
29
|
-
// Once per mount, asks the app for this person's reports and compares each with what they last opened: a move since
|
|
30
|
-
// then shows the dot, and the newest one gets the open's one toast. Opening a report (anywhere) clears it.
|
|
26
|
+
const [toasted, setToasted] = useState(false);
|
|
31
27
|
useEffect(() => {
|
|
32
28
|
setNews(0);
|
|
33
29
|
if (!rt)
|
|
@@ -65,13 +61,47 @@ export function SuggestEntry({ adapters, onOpen, className, role }) {
|
|
|
65
61
|
const at = (x) => rows.find((r) => r.id === x)?.at ?? 0;
|
|
66
62
|
const top = [...m].sort((a, b) => at(b.id) - at(a.id))[0];
|
|
67
63
|
const note = top.status === 'not-now' ? rows.find((r) => r.id === top.id)?.note : undefined;
|
|
68
|
-
|
|
64
|
+
setToasted(true);
|
|
69
65
|
toast({ id: `sac-moved-${top.id}`, text: S.movedToast(statusWords(top.status, adapters.words.owner), typeof note === 'string' && note ? note : undefined), ms: 6000 });
|
|
70
66
|
})();
|
|
71
67
|
const off = onSeen(() => { void count(); });
|
|
72
68
|
return () => { live = false; off(); };
|
|
73
69
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
74
70
|
}, [rt]);
|
|
71
|
+
return { moved: news, toasted };
|
|
72
|
+
}
|
|
73
|
+
/** For a host that shows the entry where it is not always visible (inside a menu): call this where the menu's trigger is
|
|
74
|
+
* drawn. It asks the app once per mount and returns how many of this person's suggestions moved since they last opened
|
|
75
|
+
* them (draw `<SuggestDot>` with it). The page load's one toast is shown by whichever of this, `<SuggestNews>` or the
|
|
76
|
+
* entry asks first; a host using only this hook must also render `<Toasts />` somewhere always mounted. */
|
|
77
|
+
export function useSuggestNews(adapters) {
|
|
78
|
+
const { rt } = useRuntime(adapters);
|
|
79
|
+
return { moved: useNews(adapters, rt).moved };
|
|
80
|
+
}
|
|
81
|
+
/** Mount once at the app's root, always mounted: shows the page load's one toast when a suggestion moved, with no entry
|
|
82
|
+
* on screen, and keeps the outbox moving (as `<KeepSending>` does). `children`, if given, receives `{ moved }`. */
|
|
83
|
+
export function SuggestNews({ adapters, children }) {
|
|
84
|
+
const news = useSuggestNews(adapters);
|
|
85
|
+
return _jsxs(_Fragment, { children: [_jsx(Toasts, {}), children ? children(news) : null] });
|
|
86
|
+
}
|
|
87
|
+
/** The entry's dot, for a host's own trigger (the three-dot button of a menu the entry sits in). Draws nothing when
|
|
88
|
+
* `moved` is 0. It is positioned like the entry's dot, so the trigger needs `position: relative` (the class
|
|
89
|
+
* `sac-dot-host` in the module's styles does it). */
|
|
90
|
+
export function SuggestDot({ adapters, moved: count, className }) {
|
|
91
|
+
const S = stringsFor(adapters.words);
|
|
92
|
+
if (!count)
|
|
93
|
+
return null;
|
|
94
|
+
return _jsxs(_Fragment, { children: [_jsx("span", { className: `sac-entry-dot${className ? ` ${className}` : ''}`, "data-sac-moved": "", "aria-hidden": "true" }), _jsx("span", { className: "sac-sr", children: S.entryMoved })] });
|
|
95
|
+
}
|
|
96
|
+
export function SuggestEntry({ adapters, onOpen, className, role }) {
|
|
97
|
+
const S = stringsFor(adapters.words);
|
|
98
|
+
const [why, setWhy] = useState('');
|
|
99
|
+
const [busy, setBusy] = useState(false);
|
|
100
|
+
// The runtime keeps the outbox moving while the entry is mounted (what <KeepSending> does).
|
|
101
|
+
const { rt, who } = useRuntime(adapters);
|
|
102
|
+
const { moved: news, toasted: toasting } = useNews(adapters, rt);
|
|
103
|
+
useEffect(() => { setWhy(''); }, [adapters]);
|
|
104
|
+
useClearOnSignOut(rt, who, adapters.words.app);
|
|
75
105
|
async function press() {
|
|
76
106
|
setBusy(true);
|
|
77
107
|
let who = null;
|
|
@@ -89,7 +119,7 @@ export function SuggestEntry({ adapters, onOpen, className, role }) {
|
|
|
89
119
|
setWhy('');
|
|
90
120
|
onOpen();
|
|
91
121
|
}
|
|
92
|
-
return (_jsxs("span", { className: "sac-entry-wrap", children: [_jsxs("button", { type: "button", role: role, "data-suggest-entry": "", className: `sac-entry${className ? ` ${className}` : ''}`, "aria-busy": busy || undefined, onClick: () => void press(), children: [S.entry,
|
|
122
|
+
return (_jsxs("span", { className: "sac-entry-wrap", children: [_jsxs("button", { type: "button", role: role, "data-suggest-entry": "", className: `sac-entry${className ? ` ${className}` : ''}`, "aria-busy": busy || undefined, onClick: () => void press(), children: [S.entry, _jsx(SuggestDot, { adapters: adapters, moved: news })] }), why ? _jsx("span", { role: "status", className: "sac-entry-why", children: why }) : null, toasting ? _jsx(Toasts, {}) : null] }));
|
|
93
123
|
}
|
|
94
124
|
/** For an error screen: opens the sheet as "something is broken", with the screen and what went wrong filled in. */
|
|
95
125
|
export function ReportProblem({ adapters, screen, error }) {
|
package/dist/client/seen.js
CHANGED
|
@@ -60,7 +60,8 @@ export function moved(store, id, rows) {
|
|
|
60
60
|
return out;
|
|
61
61
|
});
|
|
62
62
|
}
|
|
63
|
-
/** One toast per
|
|
63
|
+
/** One toast per page load, per account, whoever asks: a remounted entry (a menu that remounts it on every open) or a
|
|
64
|
+
* root-mounted SuggestNews beside it never says it again until the page is loaded again. */
|
|
64
65
|
const toasted = new Set();
|
|
65
66
|
export function firstToastThisOpen(id) {
|
|
66
67
|
const k = seenKey(id);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@supersuit/suggest-a-change",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.8",
|
|
4
4
|
"description": "The report format and flow behind a Suggest a change button: what a report is, who acts at each step, the screens, and the check every app runs. The contract has zero dependencies.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
package/src/client/entry.tsx
CHANGED
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
// THE ENTRY: one control, `data-suggest-entry`, which is the contract the screen checker looks for.
|
|
3
3
|
// Signed out, it opens nothing and says why. Signed in, it hands over to the host (`onOpen`), which
|
|
4
4
|
// shows the sheet. `<ReportProblem>` is the same control for an error screen, already filled in.
|
|
5
|
-
import { useEffect, useState, type AriaRole } from 'react'
|
|
5
|
+
import { useEffect, useState, type AriaRole, type ReactNode } from 'react'
|
|
6
6
|
import type { Adapters } from './adapters.ts'
|
|
7
|
-
import { useRuntime } from './runtime.ts'
|
|
7
|
+
import { useRuntime, type Runtime } from './runtime.ts'
|
|
8
8
|
import { firstToastThisOpen, moved, onSeen } from './seen.ts'
|
|
9
9
|
import { SuggestSheet } from './sheet.tsx'
|
|
10
10
|
import { toast, Toasts } from './toast.tsx'
|
|
@@ -18,18 +18,14 @@ export function KeepSending({ adapters }: { adapters: Adapters }) {
|
|
|
18
18
|
return null
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
|
|
21
|
+
// THE NEWS: once per mount, asks the app for this person's reports and compares each with what they last opened. A move
|
|
22
|
+
// since then is counted (the dot), and the newest one gets the page load's one toast, whichever of the entry or a
|
|
23
|
+
// root-mounted SuggestNews asks first (the guard is per page load and account, in seen.ts). Opening a report anywhere
|
|
24
|
+
// recounts. `toasted` says this mount showed the toast, so a standalone entry knows to draw a toast host for it.
|
|
25
|
+
function useNews(adapters: Adapters, rt: Runtime | null): { moved: number; toasted: boolean } {
|
|
22
26
|
const S = stringsFor(adapters.words)
|
|
23
|
-
const [why, setWhy] = useState('')
|
|
24
|
-
const [busy, setBusy] = useState(false)
|
|
25
|
-
// The runtime keeps the outbox moving while the entry is mounted (what <KeepSending> does).
|
|
26
|
-
const { rt, who } = useRuntime(adapters)
|
|
27
27
|
const [news, setNews] = useState(0)
|
|
28
|
-
const [
|
|
29
|
-
useEffect(() => { setWhy('') }, [adapters])
|
|
30
|
-
useClearOnSignOut(rt, who, adapters.words.app)
|
|
31
|
-
// Once per mount, asks the app for this person's reports and compares each with what they last opened: a move since
|
|
32
|
-
// then shows the dot, and the newest one gets the open's one toast. Opening a report (anywhere) clears it.
|
|
28
|
+
const [toasted, setToasted] = useState(false)
|
|
33
29
|
useEffect(() => {
|
|
34
30
|
setNews(0)
|
|
35
31
|
if (!rt) return
|
|
@@ -52,13 +48,50 @@ export function SuggestEntry({ adapters, onOpen, className, role }: { adapters:
|
|
|
52
48
|
const at = (x: string) => rows!.find((r) => r.id === x)?.at ?? 0
|
|
53
49
|
const top = [...m].sort((a, b) => at(b.id) - at(a.id))[0]
|
|
54
50
|
const note = top.status === 'not-now' ? rows.find((r) => r.id === top.id)?.note : undefined
|
|
55
|
-
|
|
51
|
+
setToasted(true)
|
|
56
52
|
toast({ id: `sac-moved-${top.id}`, text: S.movedToast(statusWords(top.status, adapters.words.owner), typeof note === 'string' && note ? note : undefined), ms: 6000 })
|
|
57
53
|
})()
|
|
58
54
|
const off = onSeen(() => { void count() })
|
|
59
55
|
return () => { live = false; off() }
|
|
60
56
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
61
57
|
}, [rt])
|
|
58
|
+
return { moved: news, toasted }
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** For a host that shows the entry where it is not always visible (inside a menu): call this where the menu's trigger is
|
|
62
|
+
* drawn. It asks the app once per mount and returns how many of this person's suggestions moved since they last opened
|
|
63
|
+
* them (draw `<SuggestDot>` with it). The page load's one toast is shown by whichever of this, `<SuggestNews>` or the
|
|
64
|
+
* entry asks first; a host using only this hook must also render `<Toasts />` somewhere always mounted. */
|
|
65
|
+
export function useSuggestNews(adapters: Adapters): { moved: number } {
|
|
66
|
+
const { rt } = useRuntime(adapters)
|
|
67
|
+
return { moved: useNews(adapters, rt).moved }
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Mount once at the app's root, always mounted: shows the page load's one toast when a suggestion moved, with no entry
|
|
71
|
+
* on screen, and keeps the outbox moving (as `<KeepSending>` does). `children`, if given, receives `{ moved }`. */
|
|
72
|
+
export function SuggestNews({ adapters, children }: { adapters: Adapters; children?: (news: { moved: number }) => ReactNode }) {
|
|
73
|
+
const news = useSuggestNews(adapters)
|
|
74
|
+
return <><Toasts />{children ? children(news) : null}</>
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** The entry's dot, for a host's own trigger (the three-dot button of a menu the entry sits in). Draws nothing when
|
|
78
|
+
* `moved` is 0. It is positioned like the entry's dot, so the trigger needs `position: relative` (the class
|
|
79
|
+
* `sac-dot-host` in the module's styles does it). */
|
|
80
|
+
export function SuggestDot({ adapters, moved: count, className }: { adapters: Adapters; moved: number; className?: string }) {
|
|
81
|
+
const S = stringsFor(adapters.words)
|
|
82
|
+
if (!count) return null
|
|
83
|
+
return <><span className={`sac-entry-dot${className ? ` ${className}` : ''}`} data-sac-moved="" aria-hidden="true" /><span className="sac-sr">{S.entryMoved}</span></>
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function SuggestEntry({ adapters, onOpen, className, role }: { adapters: Adapters; onOpen(): void; className?: string; /** An ARIA role for the button (for example `menuitem` when the host shows it as a menu row). Unset by default. */ role?: AriaRole }) {
|
|
87
|
+
const S = stringsFor(adapters.words)
|
|
88
|
+
const [why, setWhy] = useState('')
|
|
89
|
+
const [busy, setBusy] = useState(false)
|
|
90
|
+
// The runtime keeps the outbox moving while the entry is mounted (what <KeepSending> does).
|
|
91
|
+
const { rt, who } = useRuntime(adapters)
|
|
92
|
+
const { moved: news, toasted: toasting } = useNews(adapters, rt)
|
|
93
|
+
useEffect(() => { setWhy('') }, [adapters])
|
|
94
|
+
useClearOnSignOut(rt, who, adapters.words.app)
|
|
62
95
|
async function press() {
|
|
63
96
|
setBusy(true)
|
|
64
97
|
let who = null
|
|
@@ -72,7 +105,7 @@ export function SuggestEntry({ adapters, onOpen, className, role }: { adapters:
|
|
|
72
105
|
<span className="sac-entry-wrap">
|
|
73
106
|
<button type="button" role={role} data-suggest-entry="" className={`sac-entry${className ? ` ${className}` : ''}`} aria-busy={busy || undefined} onClick={() => void press()}>
|
|
74
107
|
{S.entry}
|
|
75
|
-
|
|
108
|
+
<SuggestDot adapters={adapters} moved={news} />
|
|
76
109
|
</button>
|
|
77
110
|
{why ? <span role="status" className="sac-entry-why">{why}</span> : null}
|
|
78
111
|
{toasting ? <Toasts /> : null}
|
package/src/client/seen.ts
CHANGED
|
@@ -61,7 +61,8 @@ export function moved(store: Store, id: Id, rows: { id: string; status: string }
|
|
|
61
61
|
})
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
-
/** One toast per
|
|
64
|
+
/** One toast per page load, per account, whoever asks: a remounted entry (a menu that remounts it on every open) or a
|
|
65
|
+
* root-mounted SuggestNews beside it never says it again until the page is loaded again. */
|
|
65
66
|
const toasted = new Set<string>()
|
|
66
67
|
export function firstToastThisOpen(id: Id): boolean {
|
|
67
68
|
const k = seenKey(id)
|
package/src/client/styles.css
CHANGED
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
.sac-item { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 8px; }
|
|
46
46
|
.sac-row-button { width: 100%; text-align: left; display: flex; flex-direction: column; gap: 2px; }
|
|
47
47
|
.sac-row-status { font-weight: 600; }
|
|
48
|
-
.sac-entry { position: relative; }
|
|
48
|
+
.sac-entry, .sac-dot-host { position: relative; }
|
|
49
49
|
.sac-entry-dot { position: absolute; top: 6px; right: 6px; width: 10px; height: 10px; border-radius: 50%; background: var(--sac-accent); box-shadow: 0 0 0 2px var(--sac-bg); }
|
|
50
50
|
.sac-new { align-self: flex-start; font-size: 0.8rem; font-weight: 600; padding: 1px 8px; border-radius: 999px; background: var(--sac-accent); color: var(--sac-accent-fg); }
|
|
51
51
|
.sac-muted { color: var(--sac-muted); font-size: 0.9rem; }
|