@jant/core 0.6.2 → 0.6.4
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/dist/{app-Ct9c4zYF.js → app-B-wKZB8f.js} +267 -205
- package/dist/app-qwMcaTML.js +6 -0
- package/dist/client/.vite/manifest.json +3 -3
- package/dist/client/_assets/{client-Bp2IPjDe.js → client-B1XjvRqE.js} +1 -1
- package/dist/client/_assets/client-BMPMuwvV.css +2 -0
- package/dist/client/_assets/{client-auth-C4hQWqH1.js → client-auth-B9T2QFl2.js} +21 -21
- package/dist/{export-O2w3AsZX.js → export-CzuQyg5h.js} +28 -15
- package/dist/{github-sync-BUzIYouS.js → github-sync-CerNYCAn.js} +2 -2
- package/dist/{github-sync-D49RADci.js → github-sync-Dbrb1DS5.js} +5 -2
- package/dist/index.js +3 -3
- package/dist/node.js +4 -4
- package/package.json +1 -1
- package/src/__tests__/export-service.test.ts +127 -0
- package/src/app.tsx +7 -0
- package/src/client/components/__tests__/jant-collection-directory.test.ts +0 -42
- package/src/client/components/collection-manager-types.ts +0 -2
- package/src/client/components/jant-collection-directory.ts +0 -23
- package/src/i18n/locales/public/en.po +0 -12
- package/src/i18n/locales/public/zh-Hans.po +0 -12
- package/src/i18n/locales/public/zh-Hant.po +0 -12
- package/src/lib/github-sync-site-config.ts +4 -2
- package/src/middleware/__tests__/cache-control.test.ts +50 -0
- package/src/middleware/cache-control.ts +60 -0
- package/src/services/export-theme/styles/main.css +4 -3
- package/src/services/export.ts +47 -17
- package/src/services/github-sync.ts +8 -2
- package/src/styles/ui.css +23 -46
- package/src/ui/feed/ThreadPreview.tsx +17 -9
- package/src/ui/feed/__tests__/thread-preview.test.ts +131 -6
- package/src/ui/feed/thread-preview-state.ts +43 -0
- package/src/ui/pages/CollectionsPage.tsx +0 -22
- package/src/ui/shared/CollectionsManager.tsx +6 -41
- package/dist/app-CUZaVgsC.js +0 -6
- package/dist/client/_assets/client-YVrRjAid.css +0 -2
package/src/styles/ui.css
CHANGED
|
@@ -611,12 +611,12 @@
|
|
|
611
611
|
}
|
|
612
612
|
|
|
613
613
|
.site-logo-avatar {
|
|
614
|
-
|
|
615
|
-
|
|
614
|
+
box-sizing: border-box;
|
|
615
|
+
width: calc(var(--avatar-size) + 4px);
|
|
616
|
+
height: calc(var(--avatar-size) + 4px);
|
|
616
617
|
border-radius: var(--avatar-radius);
|
|
617
618
|
object-fit: cover;
|
|
618
|
-
|
|
619
|
-
color-mix(in srgb, var(--site-divider) 82%, transparent);
|
|
619
|
+
border: 1px solid color-mix(in srgb, var(--site-divider) 82%, transparent);
|
|
620
620
|
}
|
|
621
621
|
|
|
622
622
|
.jant-brand-mark {
|
|
@@ -1115,6 +1115,10 @@
|
|
|
1115
1115
|
flex: 1 1 18rem;
|
|
1116
1116
|
}
|
|
1117
1117
|
|
|
1118
|
+
.collections-page-heading .page-intro-title-row {
|
|
1119
|
+
align-items: center;
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1118
1122
|
.collections-page-actions {
|
|
1119
1123
|
display: flex;
|
|
1120
1124
|
align-items: center;
|
|
@@ -1129,7 +1133,7 @@
|
|
|
1129
1133
|
}
|
|
1130
1134
|
|
|
1131
1135
|
.collections-page-action-group[data-collections-toolbar] {
|
|
1132
|
-
gap: 0.
|
|
1136
|
+
gap: 0.25rem;
|
|
1133
1137
|
}
|
|
1134
1138
|
|
|
1135
1139
|
.collections-page-toolbar-button {
|
|
@@ -1137,13 +1141,13 @@
|
|
|
1137
1141
|
display: inline-flex;
|
|
1138
1142
|
align-items: center;
|
|
1139
1143
|
justify-content: center;
|
|
1140
|
-
width:
|
|
1141
|
-
height:
|
|
1144
|
+
width: 2.25rem;
|
|
1145
|
+
height: 2.25rem;
|
|
1142
1146
|
padding: 0;
|
|
1143
1147
|
border: none;
|
|
1144
1148
|
border-radius: 999px;
|
|
1145
1149
|
background: transparent;
|
|
1146
|
-
color: var(--
|
|
1150
|
+
color: var(--site-text-secondary);
|
|
1147
1151
|
cursor: pointer;
|
|
1148
1152
|
transition:
|
|
1149
1153
|
background-color 0.16s ease,
|
|
@@ -1152,41 +1156,15 @@
|
|
|
1152
1156
|
border-color 0.16s ease;
|
|
1153
1157
|
}
|
|
1154
1158
|
|
|
1155
|
-
.collections-page-toolbar-button:hover
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
.collections-page-toolbar-button:focus-visible {
|
|
1160
|
-
outline: none;
|
|
1161
|
-
background: color-mix(in srgb, var(--accent) 46%, transparent);
|
|
1162
|
-
box-shadow: 0 0 0 3px
|
|
1163
|
-
color-mix(in srgb, var(--site-accent) 12%, transparent);
|
|
1164
|
-
}
|
|
1165
|
-
|
|
1166
|
-
.collections-page-more-btn {
|
|
1167
|
-
min-width: 2.1rem;
|
|
1168
|
-
width: 2.1rem;
|
|
1169
|
-
height: 2.1rem;
|
|
1170
|
-
border: 1px solid color-mix(in srgb, var(--border) 76%, transparent);
|
|
1171
|
-
background: color-mix(
|
|
1172
|
-
in srgb,
|
|
1173
|
-
var(--background) 90%,
|
|
1174
|
-
var(--site-page-bg) 10%
|
|
1175
|
-
);
|
|
1176
|
-
color: var(--site-text-secondary);
|
|
1177
|
-
box-shadow: 0 12px 24px -24px rgba(15, 23, 42, 0.26);
|
|
1178
|
-
}
|
|
1179
|
-
|
|
1180
|
-
.collections-page-more-btn:hover,
|
|
1181
|
-
.collections-page-more-btn[aria-expanded="true"] {
|
|
1182
|
-
border-color: color-mix(in srgb, var(--site-accent) 18%, var(--border));
|
|
1183
|
-
background: color-mix(in srgb, var(--accent) 34%, var(--background));
|
|
1159
|
+
.collections-page-toolbar-button:hover,
|
|
1160
|
+
.collections-page-toolbar-button[aria-expanded="true"] {
|
|
1161
|
+
background: var(--accent);
|
|
1184
1162
|
color: var(--foreground);
|
|
1185
1163
|
}
|
|
1186
1164
|
|
|
1187
|
-
.collections-page-
|
|
1165
|
+
.collections-page-toolbar-button:focus-visible {
|
|
1188
1166
|
outline: none;
|
|
1189
|
-
|
|
1167
|
+
background: var(--accent);
|
|
1190
1168
|
box-shadow: 0 0 0 3px
|
|
1191
1169
|
color-mix(in srgb, var(--site-accent) 12%, transparent);
|
|
1192
1170
|
}
|
|
@@ -1377,7 +1355,6 @@
|
|
|
1377
1355
|
line-height: var(--collection-directory-title-line-height);
|
|
1378
1356
|
letter-spacing: -0.02em;
|
|
1379
1357
|
text-wrap: pretty;
|
|
1380
|
-
user-select: all;
|
|
1381
1358
|
}
|
|
1382
1359
|
|
|
1383
1360
|
.collection-directory-title-marker {
|
|
@@ -2210,7 +2187,6 @@
|
|
|
2210
2187
|
|
|
2211
2188
|
.feed-link-title-link {
|
|
2212
2189
|
text-decoration: none;
|
|
2213
|
-
user-select: all;
|
|
2214
2190
|
}
|
|
2215
2191
|
|
|
2216
2192
|
.feed-link-title-link:hover {
|
|
@@ -2228,10 +2204,6 @@
|
|
|
2228
2204
|
text-wrap: pretty;
|
|
2229
2205
|
}
|
|
2230
2206
|
|
|
2231
|
-
.feed-note-title a {
|
|
2232
|
-
user-select: all;
|
|
2233
|
-
}
|
|
2234
|
-
|
|
2235
2207
|
.feed-continue-link {
|
|
2236
2208
|
display: inline-block;
|
|
2237
2209
|
margin-top: 0.5rem;
|
|
@@ -7002,10 +6974,15 @@
|
|
|
7002
6974
|
opacity: 0.82;
|
|
7003
6975
|
}
|
|
7004
6976
|
|
|
7005
|
-
/* Single image: constrain to container width
|
|
6977
|
+
/* Single image: constrain to container width. min-width keeps the preview
|
|
6978
|
+
wide enough for the remove button; a very long image overflows it and is
|
|
6979
|
+
cropped from the top (object-fit: cover) instead of shrinking to a sliver. */
|
|
7006
6980
|
.compose-attachment:only-child .compose-attachment-img {
|
|
7007
6981
|
max-width: 100%;
|
|
7008
6982
|
max-height: min(200px, 22dvh);
|
|
6983
|
+
min-width: 48px;
|
|
6984
|
+
object-fit: cover;
|
|
6985
|
+
object-position: center top;
|
|
7009
6986
|
}
|
|
7010
6987
|
|
|
7011
6988
|
.compose-attachment:only-child .compose-attachment-preview-fallback {
|
|
@@ -11,7 +11,10 @@ import { useLingui } from "../../i18n/context.js";
|
|
|
11
11
|
import type { ThreadPreviewProps } from "../../types.js";
|
|
12
12
|
import { TimelineItem } from "./TimelineItem.js";
|
|
13
13
|
import { TimelineItemFromPost } from "./TimelineItem.js";
|
|
14
|
-
import {
|
|
14
|
+
import {
|
|
15
|
+
getThreadPreviewState,
|
|
16
|
+
threadContextAssumesOverflow,
|
|
17
|
+
} from "./thread-preview-state.js";
|
|
15
18
|
|
|
16
19
|
const ROOT_CONTEXT_DISPLAY = {
|
|
17
20
|
footer: {
|
|
@@ -42,6 +45,10 @@ export const ThreadPreview: FC<ThreadPreviewProps> = ({
|
|
|
42
45
|
latestReply,
|
|
43
46
|
totalReplyCount,
|
|
44
47
|
});
|
|
48
|
+
const assumeOverflow = threadContextAssumesOverflow({
|
|
49
|
+
rootPost,
|
|
50
|
+
totalReplyCount,
|
|
51
|
+
});
|
|
45
52
|
const hiddenPostsLabel = i18n._(
|
|
46
53
|
msg({
|
|
47
54
|
message: "{count, plural, one {# more post} other {# more posts}}",
|
|
@@ -75,14 +82,14 @@ export const ThreadPreview: FC<ThreadPreviewProps> = ({
|
|
|
75
82
|
: undefined;
|
|
76
83
|
const gapHref = renderedSecondReply?.permalink ?? latestReply.permalink;
|
|
77
84
|
|
|
78
|
-
// Always render the collapsible shell
|
|
79
|
-
//
|
|
80
|
-
//
|
|
81
|
-
//
|
|
82
|
-
//
|
|
83
|
-
//
|
|
84
|
-
//
|
|
85
|
-
//
|
|
85
|
+
// Always render the collapsible shell + toggle: the cap and fade are a
|
|
86
|
+
// constant "this is context" affordance. The toggle's *initial* visibility
|
|
87
|
+
// is a server-side guess (threadContextAssumesOverflow) — the real rendered
|
|
88
|
+
// height is unknown until the client measures. Guessing matters because a
|
|
89
|
+
// 2-post thread's shell holds only the root, and a short root genuinely
|
|
90
|
+
// fits the cap; rendering the toggle visible anyway makes it flash in then
|
|
91
|
+
// out on load. Client-side measurement (thread-context.ts) re-measures and
|
|
92
|
+
// corrects, so the guess only ever affects that first paint.
|
|
86
93
|
|
|
87
94
|
const rootItem = (
|
|
88
95
|
<div class="thread-item thread-item-context">
|
|
@@ -138,6 +145,7 @@ export const ThreadPreview: FC<ThreadPreviewProps> = ({
|
|
|
138
145
|
data-label-more={showMoreLabel}
|
|
139
146
|
data-label-less={showLessLabel}
|
|
140
147
|
aria-expanded="false"
|
|
148
|
+
hidden={!assumeOverflow}
|
|
141
149
|
>
|
|
142
150
|
<span class="thread-context-toggle-label">{showMoreLabel}</span>
|
|
143
151
|
<svg
|
|
@@ -7,7 +7,10 @@ import { createI18n } from "../../../i18n/i18n.js";
|
|
|
7
7
|
import type { PostView, TimelineItemView } from "../../../types.js";
|
|
8
8
|
import { CuratedThreadPreview } from "../CuratedThreadPreview.js";
|
|
9
9
|
import { ThreadPreview } from "../ThreadPreview.js";
|
|
10
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
getThreadPreviewState,
|
|
12
|
+
threadContextAssumesOverflow,
|
|
13
|
+
} from "../thread-preview-state.js";
|
|
11
14
|
|
|
12
15
|
function createPostView(overrides: Partial<PostView> = {}): PostView {
|
|
13
16
|
return {
|
|
@@ -48,6 +51,13 @@ function renderWithI18n(
|
|
|
48
51
|
return renderToString(render());
|
|
49
52
|
}
|
|
50
53
|
|
|
54
|
+
/** The opening tag of the show-more toggle button, for attribute assertions. */
|
|
55
|
+
function toggleTag(html: string): string {
|
|
56
|
+
return (
|
|
57
|
+
html.match(/<button[^>]*\bdata-thread-context-toggle\b[^>]*>/)?.[0] ?? ""
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
51
61
|
describe("getThreadPreviewState", () => {
|
|
52
62
|
it("has no hidden posts for a 2-post thread", () => {
|
|
53
63
|
const latestReply = createPostView({
|
|
@@ -236,13 +246,17 @@ describe("getThreadPreviewState", () => {
|
|
|
236
246
|
expect(html).toMatch(/data-label-less="[^"]+"/);
|
|
237
247
|
});
|
|
238
248
|
|
|
239
|
-
it("
|
|
240
|
-
//
|
|
241
|
-
//
|
|
242
|
-
//
|
|
249
|
+
it("hides the show-more toggle on first paint for a short lone root that fits the cap", () => {
|
|
250
|
+
// 2-post thread: the shell holds only the root. A short root genuinely
|
|
251
|
+
// fits the height cap, so the toggle is rendered hidden to avoid flashing
|
|
252
|
+
// it in then out. Client-side measurement (thread-context.ts) re-reveals
|
|
253
|
+
// it if the rendered height actually overflows.
|
|
243
254
|
const html = renderWithI18n(() =>
|
|
244
255
|
ThreadPreview({
|
|
245
|
-
rootPost: createPostView({
|
|
256
|
+
rootPost: createPostView({
|
|
257
|
+
bodyHtml: "<p>Root</p>",
|
|
258
|
+
summary: "Root",
|
|
259
|
+
}),
|
|
246
260
|
latestReply: createPostView({
|
|
247
261
|
id: "post-2",
|
|
248
262
|
permalink: "/post-2",
|
|
@@ -257,10 +271,35 @@ describe("getThreadPreviewState", () => {
|
|
|
257
271
|
expect(html).toContain("thread-context-shell");
|
|
258
272
|
expect(html).toContain("data-collapsed");
|
|
259
273
|
expect(html).toContain("data-thread-context-toggle");
|
|
274
|
+
expect(toggleTag(html)).toContain("hidden");
|
|
260
275
|
expect(html).toContain("<p>Root</p>");
|
|
261
276
|
expect(html).toContain("<p>Latest</p>");
|
|
262
277
|
});
|
|
263
278
|
|
|
279
|
+
it("shows the show-more toggle on first paint for a long lone root", () => {
|
|
280
|
+
// 2-post thread with a long root — the shell almost certainly overflows
|
|
281
|
+
// the cap, so the toggle is rendered visible immediately (no flash).
|
|
282
|
+
const html = renderWithI18n(() =>
|
|
283
|
+
ThreadPreview({
|
|
284
|
+
rootPost: createPostView({
|
|
285
|
+
bodyHtml: "<p>Root</p>",
|
|
286
|
+
summary: "word ".repeat(60),
|
|
287
|
+
}),
|
|
288
|
+
latestReply: createPostView({
|
|
289
|
+
id: "post-2",
|
|
290
|
+
permalink: "/post-2",
|
|
291
|
+
slug: "post-2",
|
|
292
|
+
bodyHtml: "<p>Latest</p>",
|
|
293
|
+
isLastInThread: true,
|
|
294
|
+
}),
|
|
295
|
+
totalReplyCount: 1,
|
|
296
|
+
}),
|
|
297
|
+
);
|
|
298
|
+
|
|
299
|
+
expect(html).toContain("data-thread-context-toggle");
|
|
300
|
+
expect(toggleTag(html)).not.toContain("hidden");
|
|
301
|
+
});
|
|
302
|
+
|
|
264
303
|
it("renders the collapsible shell even when just one extra item precedes the latest reply", () => {
|
|
265
304
|
// root + secondReply + hero — one extra context item. The server can't
|
|
266
305
|
// measure actual height, so it renders the shell assuming overflow (the
|
|
@@ -292,6 +331,9 @@ describe("getThreadPreviewState", () => {
|
|
|
292
331
|
expect(html).toContain("<p>Root</p>");
|
|
293
332
|
expect(html).toContain("<p>Second</p>");
|
|
294
333
|
expect(html).toContain("<p>Latest</p>");
|
|
334
|
+
// 3-post thread (totalReplyCount 2): the shell stacks 2 cards, so the
|
|
335
|
+
// toggle is rendered visible on first paint.
|
|
336
|
+
expect(toggleTag(html)).not.toContain("hidden");
|
|
295
337
|
});
|
|
296
338
|
|
|
297
339
|
it("points the hidden-posts gap link to the second reply so the detail page opens just above the hidden range", () => {
|
|
@@ -352,6 +394,22 @@ describe("getThreadPreviewState", () => {
|
|
|
352
394
|
);
|
|
353
395
|
});
|
|
354
396
|
|
|
397
|
+
it("renders curated thread previews without a collapsible context shell", () => {
|
|
398
|
+
// Curated previews render each segment in flow — no shell, no toggle —
|
|
399
|
+
// so the overflow heuristic never applies to them.
|
|
400
|
+
const post = createPostView({ summary: "Curated note" });
|
|
401
|
+
const html = renderWithI18n(() =>
|
|
402
|
+
CuratedThreadPreview({
|
|
403
|
+
curatedThread: {
|
|
404
|
+
rootPost: post,
|
|
405
|
+
segments: [{ post, hiddenBeforeCount: 0, highlighted: true }],
|
|
406
|
+
},
|
|
407
|
+
}),
|
|
408
|
+
);
|
|
409
|
+
|
|
410
|
+
expect(html).not.toContain("data-thread-context-toggle");
|
|
411
|
+
});
|
|
412
|
+
|
|
355
413
|
it("renders article summaries in curated thread previews", () => {
|
|
356
414
|
const articlePost = createPostView({
|
|
357
415
|
title: "Curated article",
|
|
@@ -381,3 +439,70 @@ describe("getThreadPreviewState", () => {
|
|
|
381
439
|
expect(html).not.toContain('id="continue"');
|
|
382
440
|
});
|
|
383
441
|
});
|
|
442
|
+
|
|
443
|
+
describe("threadContextAssumesOverflow", () => {
|
|
444
|
+
it("assumes overflow for 3+ post threads (the shell stacks 2+ cards)", () => {
|
|
445
|
+
expect(
|
|
446
|
+
threadContextAssumesOverflow({
|
|
447
|
+
rootPost: createPostView({ summary: "short" }),
|
|
448
|
+
totalReplyCount: 2,
|
|
449
|
+
}),
|
|
450
|
+
).toBe(true);
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
it("assumes a short lone root fits the cap (2-post thread)", () => {
|
|
454
|
+
expect(
|
|
455
|
+
threadContextAssumesOverflow({
|
|
456
|
+
rootPost: createPostView({
|
|
457
|
+
summary: "Took the long way home because the light was good.",
|
|
458
|
+
}),
|
|
459
|
+
totalReplyCount: 1,
|
|
460
|
+
}),
|
|
461
|
+
).toBe(false);
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
it("assumes overflow for a long lone root", () => {
|
|
465
|
+
expect(
|
|
466
|
+
threadContextAssumesOverflow({
|
|
467
|
+
rootPost: createPostView({ summary: "x".repeat(200) }),
|
|
468
|
+
totalReplyCount: 1,
|
|
469
|
+
}),
|
|
470
|
+
).toBe(true);
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
it("assumes overflow for a short lone root that carries media", () => {
|
|
474
|
+
expect(
|
|
475
|
+
threadContextAssumesOverflow({
|
|
476
|
+
rootPost: createPostView({
|
|
477
|
+
summary: "short",
|
|
478
|
+
media: [{} as PostView["media"][number]],
|
|
479
|
+
}),
|
|
480
|
+
totalReplyCount: 1,
|
|
481
|
+
}),
|
|
482
|
+
).toBe(true);
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
it("assumes overflow for a short lone root with a link preview image", () => {
|
|
486
|
+
expect(
|
|
487
|
+
threadContextAssumesOverflow({
|
|
488
|
+
rootPost: createPostView({
|
|
489
|
+
format: "link",
|
|
490
|
+
summary: "short",
|
|
491
|
+
previewImageUrl: "https://example.com/cover.jpg",
|
|
492
|
+
}),
|
|
493
|
+
totalReplyCount: 1,
|
|
494
|
+
}),
|
|
495
|
+
).toBe(true);
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
it("counts plain-text code points, not UTF-16 units, against the limit", () => {
|
|
499
|
+
// 130 CJK code points — comfortably over the 120 limit even though each
|
|
500
|
+
// is a single BMP character. Confirms the threshold reads real length.
|
|
501
|
+
expect(
|
|
502
|
+
threadContextAssumesOverflow({
|
|
503
|
+
rootPost: createPostView({ summary: "字".repeat(130) }),
|
|
504
|
+
totalReplyCount: 1,
|
|
505
|
+
}),
|
|
506
|
+
).toBe(true);
|
|
507
|
+
});
|
|
508
|
+
});
|
|
@@ -22,3 +22,46 @@ export function getThreadPreviewState({
|
|
|
22
22
|
hiddenCount,
|
|
23
23
|
};
|
|
24
24
|
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* A lone root post shorter than this (plain-text code points) comfortably
|
|
28
|
+
* fits the thread-context height cap. Pairs with the
|
|
29
|
+
* `--site-thread-context-max-height` token (160px) in tokens.css — revisit
|
|
30
|
+
* both together if that cap changes.
|
|
31
|
+
*/
|
|
32
|
+
const SHORT_ROOT_CHAR_LIMIT = 120;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* First-paint guess for whether the thread-context shell will overflow its
|
|
36
|
+
* height cap, used to pick the initial "Show more" toggle visibility.
|
|
37
|
+
*
|
|
38
|
+
* The client (thread-context.ts) always re-measures and corrects this, so a
|
|
39
|
+
* wrong guess costs at most a one-time flash on load — never a wrong final
|
|
40
|
+
* state. Kept deliberately coarse:
|
|
41
|
+
*
|
|
42
|
+
* - 3+ post threads stack 2+ cards in the shell — effectively always overflow.
|
|
43
|
+
* - A lone root carrying media is tall regardless of its text length.
|
|
44
|
+
* - Otherwise fall back to a plain-text length threshold. Only clearly short
|
|
45
|
+
* roots flip to "fits"; anything at or above the limit keeps the historical
|
|
46
|
+
* "assume overflow" default, so this can only remove flashes, never add them.
|
|
47
|
+
*
|
|
48
|
+
* @param rootPost - the thread's root post (the only post in a 2-post thread's
|
|
49
|
+
* shell)
|
|
50
|
+
* @param totalReplyCount - replies in the thread; `>= 2` means 3+ posts total
|
|
51
|
+
* @returns whether to render the toggle visible on first paint
|
|
52
|
+
*/
|
|
53
|
+
export function threadContextAssumesOverflow({
|
|
54
|
+
rootPost,
|
|
55
|
+
totalReplyCount,
|
|
56
|
+
}: {
|
|
57
|
+
rootPost: PostView;
|
|
58
|
+
totalReplyCount: number;
|
|
59
|
+
}): boolean {
|
|
60
|
+
if (totalReplyCount >= 2) return true;
|
|
61
|
+
if (rootPost.media.length > 0 || Boolean(rootPost.previewImageUrl)) {
|
|
62
|
+
return true;
|
|
63
|
+
}
|
|
64
|
+
// `summary` is format-normalized at the viewmodel layer: quote text for
|
|
65
|
+
// quotes, body text for notes, the URL for bare links.
|
|
66
|
+
return [...(rootPost.summary ?? "")].length >= SHORT_ROOT_CHAR_LIMIT;
|
|
67
|
+
}
|
|
@@ -11,37 +11,18 @@ import type { CollectionsPageProps } from "../../types.js";
|
|
|
11
11
|
import { CollectionDirectory } from "../shared/CollectionDirectory.js";
|
|
12
12
|
import { CollectionsManager } from "../shared/CollectionsManager.js";
|
|
13
13
|
|
|
14
|
-
const countCollections = (items: CollectionsPageProps["items"]) =>
|
|
15
|
-
items.filter((item) => item.type === "collection" && item.collection).length;
|
|
16
|
-
|
|
17
14
|
export const CollectionsPage: FC<CollectionsPageProps> = ({
|
|
18
15
|
items,
|
|
19
16
|
isAuthenticated,
|
|
20
17
|
sitePathPrefix = "",
|
|
21
18
|
}) => {
|
|
22
19
|
const { i18n } = useLingui();
|
|
23
|
-
const collectionCount = countCollections(items);
|
|
24
20
|
const emptyMessage = i18n._(
|
|
25
21
|
msg({
|
|
26
22
|
message: "No collections yet. Start one to organize posts by topic.",
|
|
27
23
|
comment: "@context: Empty state message on collections page",
|
|
28
24
|
}),
|
|
29
25
|
);
|
|
30
|
-
const collectionCountLabel = `${collectionCount} ${
|
|
31
|
-
collectionCount === 1
|
|
32
|
-
? i18n._(
|
|
33
|
-
msg({
|
|
34
|
-
message: "collection",
|
|
35
|
-
comment: "@context: Singular collection count label",
|
|
36
|
-
}),
|
|
37
|
-
)
|
|
38
|
-
: i18n._(
|
|
39
|
-
msg({
|
|
40
|
-
message: "collections",
|
|
41
|
-
comment: "@context: Plural collection count label",
|
|
42
|
-
}),
|
|
43
|
-
)
|
|
44
|
-
}`;
|
|
45
26
|
|
|
46
27
|
if (isAuthenticated) {
|
|
47
28
|
return (
|
|
@@ -66,9 +47,6 @@ export const CollectionsPage: FC<CollectionsPageProps> = ({
|
|
|
66
47
|
)}
|
|
67
48
|
</h1>
|
|
68
49
|
</div>
|
|
69
|
-
<div class="page-intro-meta-row">
|
|
70
|
-
<p class="page-intro-meta">{collectionCountLabel}</p>
|
|
71
|
-
</div>
|
|
72
50
|
</div>
|
|
73
51
|
</header>
|
|
74
52
|
|
|
@@ -13,9 +13,6 @@ import { getCollectionMutationLabels } from "./collection-management-labels.js";
|
|
|
13
13
|
const escapeJson = (data: unknown) =>
|
|
14
14
|
JSON.stringify(data).replace(/</g, "\\u003c");
|
|
15
15
|
|
|
16
|
-
const countCollections = (items: CollectionDirectoryItem[]) =>
|
|
17
|
-
items.filter((item) => item.type === "collection" && item.collection).length;
|
|
18
|
-
|
|
19
16
|
export interface CollectionsManagerProps {
|
|
20
17
|
items: CollectionDirectoryItem[];
|
|
21
18
|
sitePathPrefix?: string;
|
|
@@ -34,22 +31,6 @@ export const CollectionsManager: FC<CollectionsManagerProps> = ({
|
|
|
34
31
|
`${getNewCollectionPath()}?returnTo=${encodeURIComponent(collectionsHref)}`,
|
|
35
32
|
sitePathPrefix,
|
|
36
33
|
);
|
|
37
|
-
const collectionCount = countCollections(items);
|
|
38
|
-
const collectionCountLabel = `${collectionCount} ${
|
|
39
|
-
collectionCount === 1
|
|
40
|
-
? i18n._(
|
|
41
|
-
msg({
|
|
42
|
-
message: "collection",
|
|
43
|
-
comment: "@context: Singular collection count label",
|
|
44
|
-
}),
|
|
45
|
-
)
|
|
46
|
-
: i18n._(
|
|
47
|
-
msg({
|
|
48
|
-
message: "collections",
|
|
49
|
-
comment: "@context: Plural collection count label",
|
|
50
|
-
}),
|
|
51
|
-
)
|
|
52
|
-
}`;
|
|
53
34
|
const mutationLabels = getCollectionMutationLabels(i18n);
|
|
54
35
|
|
|
55
36
|
const labels = {
|
|
@@ -59,18 +40,6 @@ export const CollectionsManager: FC<CollectionsManagerProps> = ({
|
|
|
59
40
|
comment: "@context: Collections page heading",
|
|
60
41
|
}),
|
|
61
42
|
),
|
|
62
|
-
collectionSingular: i18n._(
|
|
63
|
-
msg({
|
|
64
|
-
message: "collection",
|
|
65
|
-
comment: "@context: Singular collection count label",
|
|
66
|
-
}),
|
|
67
|
-
),
|
|
68
|
-
collectionPlural: i18n._(
|
|
69
|
-
msg({
|
|
70
|
-
message: "collections",
|
|
71
|
-
comment: "@context: Plural collection count label",
|
|
72
|
-
}),
|
|
73
|
-
),
|
|
74
43
|
organize: i18n._(
|
|
75
44
|
msg({
|
|
76
45
|
message: "Organize",
|
|
@@ -164,11 +133,6 @@ export const CollectionsManager: FC<CollectionsManagerProps> = ({
|
|
|
164
133
|
<div class="collections-page-heading page-intro">
|
|
165
134
|
<div class="page-intro-title-row">
|
|
166
135
|
<h1 class="page-intro-title">{labels.collectionsTitle}</h1>
|
|
167
|
-
</div>
|
|
168
|
-
<div class="page-intro-meta-row">
|
|
169
|
-
<p class="page-intro-meta" data-collections-count>
|
|
170
|
-
{collectionCountLabel}
|
|
171
|
-
</p>
|
|
172
136
|
<div class="collections-page-actions">
|
|
173
137
|
<div
|
|
174
138
|
class="collections-page-action-group"
|
|
@@ -202,14 +166,15 @@ export const CollectionsManager: FC<CollectionsManagerProps> = ({
|
|
|
202
166
|
>
|
|
203
167
|
<svg
|
|
204
168
|
xmlns="http://www.w3.org/2000/svg"
|
|
205
|
-
width="
|
|
206
|
-
height="
|
|
169
|
+
width="18"
|
|
170
|
+
height="18"
|
|
207
171
|
viewBox="0 0 24 24"
|
|
208
172
|
fill="none"
|
|
209
173
|
stroke="currentColor"
|
|
210
174
|
stroke-width="2"
|
|
211
175
|
stroke-linecap="round"
|
|
212
176
|
stroke-linejoin="round"
|
|
177
|
+
aria-hidden="true"
|
|
213
178
|
>
|
|
214
179
|
<path d="M12 5v14" />
|
|
215
180
|
<path d="M5 12h14" />
|
|
@@ -218,7 +183,7 @@ export const CollectionsManager: FC<CollectionsManagerProps> = ({
|
|
|
218
183
|
<div class="relative">
|
|
219
184
|
<button
|
|
220
185
|
type="button"
|
|
221
|
-
class="collections-page-toolbar-button
|
|
186
|
+
class="collections-page-toolbar-button"
|
|
222
187
|
aria-label={labels.moreActions}
|
|
223
188
|
aria-expanded="false"
|
|
224
189
|
aria-haspopup="menu"
|
|
@@ -227,8 +192,8 @@ export const CollectionsManager: FC<CollectionsManagerProps> = ({
|
|
|
227
192
|
>
|
|
228
193
|
<svg
|
|
229
194
|
xmlns="http://www.w3.org/2000/svg"
|
|
230
|
-
width="
|
|
231
|
-
height="
|
|
195
|
+
width="18"
|
|
196
|
+
height="18"
|
|
232
197
|
viewBox="0 0 24 24"
|
|
233
198
|
fill="currentColor"
|
|
234
199
|
>
|