@tenyokjs/devdevian-cli 0.5.0-alpha.1 → 0.5.1-alpha.1
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/README.md +13 -0
- package/VERSIONS.md +9 -0
- package/bin/devdevian.mjs +1 -0
- package/lib/appearance.mjs +8 -0
- package/lib/devi.mjs +137 -0
- package/lib/tui.mjs +123 -49
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -31,12 +31,25 @@ Pages have a bounded 30-second in-memory cache. Refresh explicitly with Shift+R.
|
|
|
31
31
|
|
|
32
32
|
Ordinary reply deletion replaces your text with a placeholder and preserves nested replies. Its attachments become unavailable. Deleting an accepted answer reopens the question. D/d works on a selected reply or while reading it in full; confirmation is required.
|
|
33
33
|
|
|
34
|
+
## Meet Devi
|
|
35
|
+
|
|
36
|
+
Devi is the Devdevian mascot: a small ghost with a laptop and an orbit. The terminal uses colored ASCII art, so it needs no image protocol or image downloads. A compact drawing is used when space is limited; authentication codes, addresses and controls take priority over decoration.
|
|
37
|
+
|
|
38
|
+
- **“Devi missed you.”** appears while checking your saved session and loading the first page. It disappears as soon as the page is ready; there is no minimum splash duration or fake progress bar.
|
|
39
|
+
- **“Devi saved you a seat.”** welcomes you to browser-approved sign-in. Sign-in instructions stay visible in small terminals.
|
|
40
|
+
- Empty feed, Inbox, communities, local drafts and search views show a still mascot with a relevant next step. Errors remain error states; they do not become a happy empty view.
|
|
41
|
+
- Subtle mode animates a blink only during startup work or pending browser approval. **Reduced motion** keeps the drawing static. Disable it entirely in **Appearance → Devi greeting**, with `devdevian appearance --welcome off`, or just for one launch with `devdevian tui --welcome off`.
|
|
42
|
+
- A failed session check offers **R / Enter** to retry the existing session. **Ctrl+C** exits during loading. Script commands and `--json` output never print the mascot.
|
|
43
|
+
|
|
44
|
+
All mascot captions are in English. Welcome preferences are local and compatible with existing 0.5.0 configuration files.
|
|
45
|
+
|
|
34
46
|
## Themes and layout
|
|
35
47
|
|
|
36
48
|
Open **Appearance** in navigation, type `/appearance`, or press **,** from a reading/list screen. Use arrows to choose options and Tab to move between them. The entire interface previews each change, including code colors, the file picker and navigation. Wide terminals include a sample card next to the controls.
|
|
37
49
|
|
|
38
50
|
- **Midnight** preserves the original blue/purple design; **Nebula** uses a deeper violet surface; **Daylight** is light; **Contrast** emphasizes readable foregrounds.
|
|
39
51
|
- Choose a **blue, violet, cyan or rose accent**, comfortable/compact lists, automatic/hidden sidebar, automatic/stacked review panels, rounded/straight frames and subtle/reduced loading indicators.
|
|
52
|
+
- **Devi greeting** shows/hides the terminal mascot on startup, sign-in and supported empty sections. Reduced motion keeps Devi still.
|
|
40
53
|
- **Ctrl+S** saves locally. **Esc** cancels the preview. **0** restores the default preview; save to keep it, or cancel to retain existing settings.
|
|
41
54
|
- Hidden navigation stays available with **Tab**. Stacked review panes switch with **W**. Narrow terminals automatically avoid a permanent sidebar and split review layout.
|
|
42
55
|
|
package/VERSIONS.md
CHANGED
|
@@ -8,6 +8,15 @@ Every published npm artifact has a unique immutable version. We record each vers
|
|
|
8
8
|
|
|
9
9
|
**0.5.0 focuses on themes, interface customization and local appearance preferences.** Projects and Events parity remains a separate future milestone.
|
|
10
10
|
|
|
11
|
+
## 0.5.1-alpha.1 — Meet Devi
|
|
12
|
+
|
|
13
|
+
- Added a terminal-native ASCII mascot based on the ghost/laptop/orbit logo, with full and compact drawings and English greetings: “Devi missed you.” and “Devi saved you a seat.”
|
|
14
|
+
- Startup follows the actual session check and first page request, with no minimum duration or artificial progress. Connection errors stop the loading state and offer an existing-session retry.
|
|
15
|
+
- Supported empty sections show a still Devi and a contextual next step. Sign-in codes, addresses and controls take priority on small screens.
|
|
16
|
+
- Subtle motion blinks only during active startup or browser approval; Reduced motion is static. Appearance includes a persistent welcome on/off option, also available to offline commands and session-only launch flags.
|
|
17
|
+
- Script/JSON commands remain banner-free. No additional image dependency, API change, schema migration or production-data change. Existing preferences default the new setting to on.
|
|
18
|
+
- Checked delayed/failing startup, retry, animation/static/off modes, English captions, first-page handoff, empty Inbox, script output and 60×18 device approval in unit and pseudo-terminal regressions.
|
|
19
|
+
|
|
11
20
|
## 0.5.0-alpha.1 — Make the terminal yours
|
|
12
21
|
|
|
13
22
|
**Changes**
|
package/bin/devdevian.mjs
CHANGED
|
@@ -120,6 +120,7 @@ const help = `Devdevian CLI ${version} · alpha
|
|
|
120
120
|
--review-layout MODE auto | stacked
|
|
121
121
|
--frame STYLE round | single
|
|
122
122
|
--motion MODE subtle | reduced
|
|
123
|
+
--welcome MODE on | off (Devi greeting)
|
|
123
124
|
--no-live Disable background reads in the live workspace
|
|
124
125
|
--project PATH Root directory for the live file/diff picker (default: current directory)
|
|
125
126
|
--community ID Publish to an approved private community
|
package/lib/appearance.mjs
CHANGED
|
@@ -72,6 +72,14 @@ export const appearanceFields = [
|
|
|
72
72
|
["reduced", "Reduced · static indicators"],
|
|
73
73
|
],
|
|
74
74
|
},
|
|
75
|
+
{
|
|
76
|
+
key: "welcome",
|
|
77
|
+
label: "Devi greeting",
|
|
78
|
+
options: [
|
|
79
|
+
["on", "Show Devi"],
|
|
80
|
+
["off", "Hide Devi"],
|
|
81
|
+
],
|
|
82
|
+
},
|
|
75
83
|
];
|
|
76
84
|
export const defaultAppearance = Object.freeze(
|
|
77
85
|
Object.fromEntries(
|
package/lib/devi.mjs
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import React, { useEffect, useState } from "react";
|
|
2
|
+
import { Box, Text } from "ink";
|
|
3
|
+
import stringWidth from "string-width";
|
|
4
|
+
import { clip, wrap } from "./tui-model.mjs";
|
|
5
|
+
import { useTheme } from "./theme.mjs";
|
|
6
|
+
const h = React.createElement;
|
|
7
|
+
// Terminal-native mascot: ghost, laptop and orbit, drawn without image protocols.
|
|
8
|
+
const full = [
|
|
9
|
+
' .-""""-.',
|
|
10
|
+
" / \\",
|
|
11
|
+
" / .----. \\ +",
|
|
12
|
+
" | ( o o ) |",
|
|
13
|
+
" .---| '----' .-------.",
|
|
14
|
+
" .-' | / </> / '-.",
|
|
15
|
+
"( | _ /______/ )",
|
|
16
|
+
" '---.._ \\_/ \\_/ _..---'",
|
|
17
|
+
" '----------'",
|
|
18
|
+
];
|
|
19
|
+
const small = [
|
|
20
|
+
" .---. .-----.",
|
|
21
|
+
" ( o o ) / </> /",
|
|
22
|
+
" /_ _\\ /_____/",
|
|
23
|
+
" '---'",
|
|
24
|
+
];
|
|
25
|
+
export function deviArt(compact = false, frame = 0) {
|
|
26
|
+
const blink = frame % 6 === 4;
|
|
27
|
+
return (compact ? small : full).map((line) =>
|
|
28
|
+
blink ? line.replace("o o", "- -").replace("o o", "- -") : line,
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
// Required status and controls get space before decoration. No minimum display timer.
|
|
32
|
+
export function deviLayout({
|
|
33
|
+
width,
|
|
34
|
+
height,
|
|
35
|
+
message,
|
|
36
|
+
status = [],
|
|
37
|
+
footer = [],
|
|
38
|
+
show = true,
|
|
39
|
+
frame = 0,
|
|
40
|
+
}) {
|
|
41
|
+
width = Math.max(1, width);
|
|
42
|
+
height = Math.max(1, height);
|
|
43
|
+
const instructions = footer.flatMap((line) => wrap(line, width));
|
|
44
|
+
const details = status.flatMap((line) => wrap(line, width));
|
|
45
|
+
const footerRows = instructions.slice(0, height);
|
|
46
|
+
const budget = Math.max(0, height - footerRows.length);
|
|
47
|
+
const detailRows = details.slice(0, budget);
|
|
48
|
+
let remaining = height - footerRows.length - detailRows.length;
|
|
49
|
+
const title = message ? wrap(message, width) : [];
|
|
50
|
+
let art = [];
|
|
51
|
+
if (show) {
|
|
52
|
+
for (const compact of [false, true]) {
|
|
53
|
+
const candidate = deviArt(compact, frame);
|
|
54
|
+
if (
|
|
55
|
+
candidate.every((line) => stringWidth(line) <= width) &&
|
|
56
|
+
candidate.length + title.length + 2 <= remaining
|
|
57
|
+
) {
|
|
58
|
+
art = candidate;
|
|
59
|
+
break;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
const rows = [];
|
|
64
|
+
if (art.length) {
|
|
65
|
+
rows.push(
|
|
66
|
+
...art.map((text) => ({
|
|
67
|
+
text: text.padEnd(Math.max(...art.map((line) => stringWidth(line)))),
|
|
68
|
+
role: text.includes("</>") ? "blue" : "purple",
|
|
69
|
+
})),
|
|
70
|
+
{ text: "" },
|
|
71
|
+
);
|
|
72
|
+
remaining -= art.length + 1;
|
|
73
|
+
}
|
|
74
|
+
if (title.length && title.length <= remaining) {
|
|
75
|
+
rows.push(...title.map((text) => ({ text, role: "text", bold: true })));
|
|
76
|
+
remaining -= title.length;
|
|
77
|
+
}
|
|
78
|
+
if (remaining && rows.length) rows.push({ text: "" });
|
|
79
|
+
rows.push(
|
|
80
|
+
...detailRows.map((text) => ({ text, role: "text" })),
|
|
81
|
+
...footerRows.map((text) => ({ text, role: "muted" })),
|
|
82
|
+
);
|
|
83
|
+
return rows
|
|
84
|
+
.slice(0, height)
|
|
85
|
+
.map((row) => ({ ...row, text: clip(row.text, width) }));
|
|
86
|
+
}
|
|
87
|
+
export function DeviPanel({
|
|
88
|
+
width,
|
|
89
|
+
height,
|
|
90
|
+
message = "Devi missed you.",
|
|
91
|
+
status,
|
|
92
|
+
footer,
|
|
93
|
+
show = true,
|
|
94
|
+
animate = false,
|
|
95
|
+
motion = "subtle",
|
|
96
|
+
}) {
|
|
97
|
+
const c = useTheme(),
|
|
98
|
+
[frame, setFrame] = useState(0);
|
|
99
|
+
useEffect(() => {
|
|
100
|
+
if (!show || !animate || motion === "reduced") return;
|
|
101
|
+
const timer = setInterval(() => setFrame((value) => (value + 1) % 6), 400);
|
|
102
|
+
return () => clearInterval(timer);
|
|
103
|
+
}, [show, animate, motion]);
|
|
104
|
+
const rows = deviLayout({
|
|
105
|
+
width,
|
|
106
|
+
height,
|
|
107
|
+
message,
|
|
108
|
+
status,
|
|
109
|
+
footer,
|
|
110
|
+
show,
|
|
111
|
+
frame: motion === "reduced" ? 0 : frame,
|
|
112
|
+
});
|
|
113
|
+
return h(
|
|
114
|
+
Box,
|
|
115
|
+
{
|
|
116
|
+
width,
|
|
117
|
+
height,
|
|
118
|
+
flexDirection: "column",
|
|
119
|
+
justifyContent: "center",
|
|
120
|
+
overflow: "hidden",
|
|
121
|
+
},
|
|
122
|
+
...rows.map((row, index) =>
|
|
123
|
+
h(
|
|
124
|
+
Text,
|
|
125
|
+
{
|
|
126
|
+
key: index,
|
|
127
|
+
color: c[row.role] || c.text,
|
|
128
|
+
bold: row.bold,
|
|
129
|
+
wrap: "truncate",
|
|
130
|
+
},
|
|
131
|
+
" ".repeat(
|
|
132
|
+
Math.max(0, Math.floor((width - stringWidth(row.text)) / 2)),
|
|
133
|
+
) + row.text,
|
|
134
|
+
),
|
|
135
|
+
),
|
|
136
|
+
);
|
|
137
|
+
}
|
package/lib/tui.mjs
CHANGED
|
@@ -3,6 +3,7 @@ import { render, Box, Text, useApp, useInput, useWindowSize } from "ink";
|
|
|
3
3
|
import { randomBytes } from "node:crypto";
|
|
4
4
|
import { homedir, hostname } from "node:os";
|
|
5
5
|
import { workspaceRoot, reviewSource } from "./workspace.mjs";
|
|
6
|
+
import { DeviPanel } from "./devi.mjs";
|
|
6
7
|
import { WorkspacePicker } from "./picker.mjs";
|
|
7
8
|
import { DraftStore, draftIdentity } from "./drafts.mjs";
|
|
8
9
|
import { spawn } from "node:child_process";
|
|
@@ -523,7 +524,8 @@ function App({ initialConfig, flags, root, initialAppearance }) {
|
|
|
523
524
|
),
|
|
524
525
|
[user, setUser] = useState(null),
|
|
525
526
|
[device, setDevice] = useState(null),
|
|
526
|
-
[authVersion, setAuthVersion] = useState(0)
|
|
527
|
+
[authVersion, setAuthVersion] = useState(0),
|
|
528
|
+
[booting, setBooting] = useState(true);
|
|
527
529
|
const [route, setRoute] = useState({ view: "feed", page: 1 }),
|
|
528
530
|
[data, setData] = useState(null),
|
|
529
531
|
[loading, setLoading] = useState(false),
|
|
@@ -693,6 +695,7 @@ function App({ initialConfig, flags, root, initialAppearance }) {
|
|
|
693
695
|
setPreview(null);
|
|
694
696
|
setDevice(null);
|
|
695
697
|
setUser(null);
|
|
698
|
+
setBooting(true);
|
|
696
699
|
setReading(null);
|
|
697
700
|
setActionIndex(null);
|
|
698
701
|
setUnread(0);
|
|
@@ -716,6 +719,7 @@ function App({ initialConfig, flags, root, initialAppearance }) {
|
|
|
716
719
|
} else setError(problemMessage(e));
|
|
717
720
|
}
|
|
718
721
|
function navigate(next, { replace = false } = {}) {
|
|
722
|
+
setBooting(false);
|
|
719
723
|
if (!replace)
|
|
720
724
|
history.current.push({
|
|
721
725
|
route,
|
|
@@ -898,7 +902,7 @@ function App({ initialConfig, flags, root, initialAppearance }) {
|
|
|
898
902
|
if (!controller.signal.aborted) fail(e);
|
|
899
903
|
});
|
|
900
904
|
return () => controller.abort();
|
|
901
|
-
}, [phase, api]);
|
|
905
|
+
}, [phase, api, authVersion]);
|
|
902
906
|
useEffect(() => {
|
|
903
907
|
if (phase !== "ready" || !api || form || route.view === "help") return;
|
|
904
908
|
let active = true;
|
|
@@ -964,7 +968,10 @@ function App({ initialConfig, flags, root, initialAppearance }) {
|
|
|
964
968
|
if (active) fail(e);
|
|
965
969
|
})
|
|
966
970
|
.finally(() => {
|
|
967
|
-
if (active)
|
|
971
|
+
if (active) {
|
|
972
|
+
setLoading(false);
|
|
973
|
+
setBooting(false);
|
|
974
|
+
}
|
|
968
975
|
});
|
|
969
976
|
return () => {
|
|
970
977
|
active = false;
|
|
@@ -996,6 +1003,7 @@ function App({ initialConfig, flags, root, initialAppearance }) {
|
|
|
996
1003
|
if (controller.signal.aborted) return;
|
|
997
1004
|
resetAccount();
|
|
998
1005
|
setConfig(saved);
|
|
1006
|
+
setBooting(true);
|
|
999
1007
|
setPhase("checking");
|
|
1000
1008
|
setNotice("Terminal connected.");
|
|
1001
1009
|
return;
|
|
@@ -1062,6 +1070,7 @@ function App({ initialConfig, flags, root, initialAppearance }) {
|
|
|
1062
1070
|
);
|
|
1063
1071
|
}
|
|
1064
1072
|
function openForm(next) {
|
|
1073
|
+
setBooting(false);
|
|
1065
1074
|
setForm({
|
|
1066
1075
|
...next,
|
|
1067
1076
|
draftId: next.draftId || randomBytes(32).toString("hex"),
|
|
@@ -1772,6 +1781,14 @@ function App({ initialConfig, flags, root, initialAppearance }) {
|
|
|
1772
1781
|
setDevice(null);
|
|
1773
1782
|
return;
|
|
1774
1783
|
}
|
|
1784
|
+
if (phase === "checking") {
|
|
1785
|
+
if (input === "q") exit();
|
|
1786
|
+
else if (error && (keyEvent.return || input === "r" || input === "R")) {
|
|
1787
|
+
setError("");
|
|
1788
|
+
setAuthVersion((v) => v + 1);
|
|
1789
|
+
}
|
|
1790
|
+
return;
|
|
1791
|
+
}
|
|
1775
1792
|
if (input === "q") exit();
|
|
1776
1793
|
else if (keyEvent.return && !busy.current) {
|
|
1777
1794
|
if (phase === "pending" && device) openBrowser(device.verification_uri);
|
|
@@ -2257,44 +2274,82 @@ function App({ initialConfig, flags, root, initialAppearance }) {
|
|
|
2257
2274
|
},
|
|
2258
2275
|
});
|
|
2259
2276
|
else if (phase !== "ready") {
|
|
2260
|
-
const
|
|
2261
|
-
|
|
2262
|
-
"Your conversations, without leaving the terminal.",
|
|
2263
|
-
"",
|
|
2264
|
-
...(phase === "pending" && device
|
|
2277
|
+
const statusLines =
|
|
2278
|
+
phase === "pending" && device
|
|
2265
2279
|
? [
|
|
2266
2280
|
"Confirm this code: " + device.user_code,
|
|
2267
|
-
"Waiting for your approval " + spin,
|
|
2268
2281
|
device.verification_uri,
|
|
2269
|
-
"
|
|
2282
|
+
"Waiting for your approval...",
|
|
2270
2283
|
]
|
|
2271
2284
|
: phase === "checking"
|
|
2272
|
-
?
|
|
2285
|
+
? error
|
|
2286
|
+
? ["Connection could not be completed.", error]
|
|
2287
|
+
: ["Connecting to Devdevian..."]
|
|
2273
2288
|
: [
|
|
2274
2289
|
"Sign in with your existing Devdevian account.",
|
|
2275
|
-
"",
|
|
2276
|
-
"Enter connects this terminal through your browser.",
|
|
2277
2290
|
"Google, GitHub and email sign-in stay on the website.",
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
|
|
2281
|
-
|
|
2291
|
+
];
|
|
2292
|
+
const footer =
|
|
2293
|
+
phase === "pending"
|
|
2294
|
+
? ["Enter reopens the browser · Esc cancels"]
|
|
2295
|
+
: phase === "checking"
|
|
2296
|
+
? [error ? "R or Enter retries · Ctrl+C exits" : "Ctrl+C exits"]
|
|
2297
|
+
: [
|
|
2298
|
+
"Enter connects this terminal through your browser.",
|
|
2299
|
+
"Q or Ctrl+C exits.",
|
|
2300
|
+
];
|
|
2282
2301
|
center = h(
|
|
2283
2302
|
Box,
|
|
2284
2303
|
{
|
|
2285
2304
|
borderStyle: colors.frame,
|
|
2286
2305
|
borderColor: colors.purple,
|
|
2287
2306
|
flexDirection: "column",
|
|
2288
|
-
paddingX:
|
|
2289
|
-
|
|
2307
|
+
paddingX: 1,
|
|
2308
|
+
height: bodyHeight,
|
|
2309
|
+
overflow: "hidden",
|
|
2310
|
+
},
|
|
2311
|
+
h(DeviPanel, {
|
|
2312
|
+
width: Math.max(1, width - 4),
|
|
2313
|
+
height: Math.max(1, bodyHeight - 2),
|
|
2314
|
+
message:
|
|
2315
|
+
effectiveAppearance.welcome === "off"
|
|
2316
|
+
? ""
|
|
2317
|
+
: phase === "checking"
|
|
2318
|
+
? "Devi missed you."
|
|
2319
|
+
: "Devi saved you a seat.",
|
|
2320
|
+
status: statusLines,
|
|
2321
|
+
footer,
|
|
2322
|
+
show: effectiveAppearance.welcome === "on",
|
|
2323
|
+
animate: phase !== "auth" && !error,
|
|
2324
|
+
motion: effectiveAppearance.motion,
|
|
2325
|
+
}),
|
|
2326
|
+
);
|
|
2327
|
+
} else if (
|
|
2328
|
+
booting &&
|
|
2329
|
+
!current &&
|
|
2330
|
+
effectiveAppearance.welcome === "on" &&
|
|
2331
|
+
!error &&
|
|
2332
|
+
!dialog &&
|
|
2333
|
+
!form &&
|
|
2334
|
+
!picker
|
|
2335
|
+
) {
|
|
2336
|
+
center = h(
|
|
2337
|
+
Box,
|
|
2338
|
+
{
|
|
2339
|
+
borderStyle: colors.frame,
|
|
2340
|
+
borderColor: colors.purple,
|
|
2341
|
+
paddingX: 1,
|
|
2290
2342
|
height: bodyHeight,
|
|
2291
2343
|
overflow: "hidden",
|
|
2292
2344
|
},
|
|
2293
|
-
h(
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
|
|
2345
|
+
h(DeviPanel, {
|
|
2346
|
+
width: Math.max(1, width - 4),
|
|
2347
|
+
height: Math.max(1, bodyHeight - 2),
|
|
2348
|
+
message: "Devi missed you.",
|
|
2349
|
+
status: ["Loading your " + labelOf(route).toLowerCase() + "..."],
|
|
2350
|
+
footer: ["Ctrl+C exits"],
|
|
2351
|
+
motion: effectiveAppearance.motion,
|
|
2352
|
+
animate: true,
|
|
2298
2353
|
}),
|
|
2299
2354
|
);
|
|
2300
2355
|
} else if (dialog) {
|
|
@@ -2405,7 +2460,7 @@ function App({ initialConfig, flags, root, initialAppearance }) {
|
|
|
2405
2460
|
width: contentWidth,
|
|
2406
2461
|
height: contentHeight,
|
|
2407
2462
|
});
|
|
2408
|
-
else if (loading && !current)
|
|
2463
|
+
else if ((loading || booting) && !current)
|
|
2409
2464
|
body = h(
|
|
2410
2465
|
Text,
|
|
2411
2466
|
{ color: colors.muted },
|
|
@@ -2500,30 +2555,49 @@ function App({ initialConfig, flags, root, initialAppearance }) {
|
|
|
2500
2555
|
height: contentHeight,
|
|
2501
2556
|
offset: Math.min(scroll, Math.max(0, lines.length - contentHeight)),
|
|
2502
2557
|
});
|
|
2503
|
-
else if (!current?.items?.length)
|
|
2504
|
-
|
|
2505
|
-
|
|
2506
|
-
|
|
2507
|
-
|
|
2508
|
-
|
|
2509
|
-
|
|
2510
|
-
|
|
2511
|
-
|
|
2512
|
-
|
|
2513
|
-
|
|
2514
|
-
|
|
2515
|
-
|
|
2516
|
-
|
|
2517
|
-
|
|
2518
|
-
|
|
2519
|
-
|
|
2520
|
-
|
|
2521
|
-
|
|
2522
|
-
|
|
2523
|
-
|
|
2524
|
-
|
|
2525
|
-
|
|
2526
|
-
|
|
2558
|
+
else if (!current?.items?.length) {
|
|
2559
|
+
const messages = {
|
|
2560
|
+
inbox: [
|
|
2561
|
+
"You're all caught up.",
|
|
2562
|
+
"Replies, mentions and review updates will appear here.",
|
|
2563
|
+
],
|
|
2564
|
+
drafts: [
|
|
2565
|
+
"No local drafts yet.",
|
|
2566
|
+
"Start writing to save a private draft on this device.",
|
|
2567
|
+
],
|
|
2568
|
+
communities: [
|
|
2569
|
+
"Find your people.",
|
|
2570
|
+
"Use / to search or N to create a private community.",
|
|
2571
|
+
],
|
|
2572
|
+
feed: [
|
|
2573
|
+
"Nothing here yet.",
|
|
2574
|
+
"N starts a conversation. A asks a question.",
|
|
2575
|
+
],
|
|
2576
|
+
search: route.query
|
|
2577
|
+
? [
|
|
2578
|
+
"No conversations matched your search.",
|
|
2579
|
+
"Try fewer words or a different phrase.",
|
|
2580
|
+
]
|
|
2581
|
+
: [
|
|
2582
|
+
"Find your next useful conversation.",
|
|
2583
|
+
"Press / and type /search followed by your query.",
|
|
2584
|
+
],
|
|
2585
|
+
};
|
|
2586
|
+
const message = messages[route.view] || [
|
|
2587
|
+
"Nothing here yet.",
|
|
2588
|
+
"Use / to search or Tab to explore another section.",
|
|
2589
|
+
];
|
|
2590
|
+
body = h(DeviPanel, {
|
|
2591
|
+
width: contentWidth,
|
|
2592
|
+
height: contentHeight,
|
|
2593
|
+
message: message[0],
|
|
2594
|
+
status: [message[1]],
|
|
2595
|
+
show:
|
|
2596
|
+
effectiveAppearance.welcome === "on" &&
|
|
2597
|
+
Object.hasOwn(messages, route.view),
|
|
2598
|
+
motion: effectiveAppearance.motion,
|
|
2599
|
+
});
|
|
2600
|
+
} else {
|
|
2527
2601
|
const visible = Math.max(1, Math.floor(contentHeight / stride)),
|
|
2528
2602
|
first = Math.max(0, index - visible + 1),
|
|
2529
2603
|
rows = listRows(
|