@firedrill-tools/linkedin 0.1.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/LICENSE +201 -0
- package/README.md +189 -0
- package/firedrill/agent.target.json +16 -0
- package/firedrill/baseline.scenario.json +1834 -0
- package/firedrill/comment-throttled.scenario.json +11 -0
- package/firedrill/conformance.suite.json +27 -0
- package/firedrill/linkedin-comment-throttled.drill.json +51 -0
- package/firedrill/linkedin-comments-connection.drill.json +43 -0
- package/firedrill/linkedin-comments.drill.json +196 -0
- package/firedrill/linkedin-denied.drill.json +383 -0
- package/firedrill/linkedin-fresh-install.drill.json +43 -0
- package/firedrill/linkedin-identity.drill.json +73 -0
- package/firedrill/linkedin-invalid-token.drill.json +393 -0
- package/firedrill/linkedin-limited-scopes.drill.json +88 -0
- package/firedrill/linkedin-openid-only.drill.json +383 -0
- package/firedrill/linkedin-orgs-connection.drill.json +28 -0
- package/firedrill/linkedin-orgs-network.drill.json +193 -0
- package/firedrill/linkedin-post-lost-response.drill.json +61 -0
- package/firedrill/linkedin-posts-lifecycle.drill.json +228 -0
- package/firedrill/linkedin-reactions-metadata.drill.json +196 -0
- package/firedrill/linkedin-reactions-outage.drill.json +66 -0
- package/firedrill/linkedin-tight-limits.drill.json +228 -0
- package/firedrill/linkedin-ugc-legacy.drill.json +43 -0
- package/firedrill/linkedin-visibility-connection.drill.json +73 -0
- package/firedrill/linkedin-visibility-stranger.drill.json +88 -0
- package/firedrill/linkedin-wire-errors.drill.json +353 -0
- package/firedrill/post-lost-response.scenario.json +11 -0
- package/firedrill/reactions-outage.scenario.json +11 -0
- package/firedrill/tight-limits.scenario.json +16 -0
- package/firedrill/tools/linkedin/app/assets/ATTRIBUTION.md +28 -0
- package/firedrill/tools/linkedin/app/assets/fonts/OFL.txt +93 -0
- package/firedrill/tools/linkedin/app/assets/linkedin-in.svg +1 -0
- package/firedrill/tools/linkedin/app/assets/linkedin-wordmark.svg +1 -0
- package/firedrill/tools/linkedin/app/assets/linkedin.svg +1 -0
- package/firedrill/tools/linkedin/app/site/app.js +80 -0
- package/firedrill/tools/linkedin/app/site/assets/fonts/fira-sans-latin-400-normal.woff2 +0 -0
- package/firedrill/tools/linkedin/app/site/assets/fonts/fira-sans-latin-600-normal.woff2 +0 -0
- package/firedrill/tools/linkedin/app/site/assets/fonts/fira-sans-latin-700-normal.woff2 +0 -0
- package/firedrill/tools/linkedin/app/site/assets/fonts/fira-sans-latin-ext-400-normal.woff2 +0 -0
- package/firedrill/tools/linkedin/app/site/assets/fonts/fira-sans-latin-ext-600-normal.woff2 +0 -0
- package/firedrill/tools/linkedin/app/site/assets/fonts/fira-sans-latin-ext-700-normal.woff2 +0 -0
- package/firedrill/tools/linkedin/app/site/assets/linkedin-wordmark.svg +1 -0
- package/firedrill/tools/linkedin/app/site/assets/linkedin.svg +1 -0
- package/firedrill/tools/linkedin/app/site/base.css +87 -0
- package/firedrill/tools/linkedin/app/site/comment-box.js +119 -0
- package/firedrill/tools/linkedin/app/site/comments.css +59 -0
- package/firedrill/tools/linkedin/app/site/comments.js +188 -0
- package/firedrill/tools/linkedin/app/site/company.js +115 -0
- package/firedrill/tools/linkedin/app/site/composer.js +112 -0
- package/firedrill/tools/linkedin/app/site/feed.css +105 -0
- package/firedrill/tools/linkedin/app/site/feed.js +134 -0
- package/firedrill/tools/linkedin/app/site/icons.js +70 -0
- package/firedrill/tools/linkedin/app/site/index.html +58 -0
- package/firedrill/tools/linkedin/app/site/nav.css +51 -0
- package/firedrill/tools/linkedin/app/site/network.js +77 -0
- package/firedrill/tools/linkedin/app/site/overlay.css +62 -0
- package/firedrill/tools/linkedin/app/site/overlay.js +108 -0
- package/firedrill/tools/linkedin/app/site/pages.css +78 -0
- package/firedrill/tools/linkedin/app/site/post-actions.js +83 -0
- package/firedrill/tools/linkedin/app/site/post-menu.js +72 -0
- package/firedrill/tools/linkedin/app/site/post.css +82 -0
- package/firedrill/tools/linkedin/app/site/post.js +131 -0
- package/firedrill/tools/linkedin/app/site/profile.js +139 -0
- package/firedrill/tools/linkedin/app/site/reactions.js +46 -0
- package/firedrill/tools/linkedin/app/site/reactors.js +71 -0
- package/firedrill/tools/linkedin/app/site/shell.js +107 -0
- package/firedrill/tools/linkedin/app/site/store.js +62 -0
- package/firedrill/tools/linkedin/app/site/text.js +80 -0
- package/firedrill/tools/linkedin/app/site/ui.js +142 -0
- package/firedrill/tools/linkedin/behavior.mjs +66 -0
- package/firedrill/tools/linkedin/lib/access.mjs +69 -0
- package/firedrill/tools/linkedin/lib/auth.mjs +115 -0
- package/firedrill/tools/linkedin/lib/cascade.mjs +28 -0
- package/firedrill/tools/linkedin/lib/errors.mjs +50 -0
- package/firedrill/tools/linkedin/lib/message.mjs +43 -0
- package/firedrill/tools/linkedin/lib/paging.mjs +54 -0
- package/firedrill/tools/linkedin/lib/postinput.mjs +47 -0
- package/firedrill/tools/linkedin/lib/render.mjs +95 -0
- package/firedrill/tools/linkedin/lib/routes.mjs +219 -0
- package/firedrill/tools/linkedin/lib/social.mjs +25 -0
- package/firedrill/tools/linkedin/lib/store.mjs +54 -0
- package/firedrill/tools/linkedin/lib/text.mjs +55 -0
- package/firedrill/tools/linkedin/lib/urn.mjs +93 -0
- package/firedrill/tools/linkedin/lib/wire.mjs +125 -0
- package/firedrill/tools/linkedin/linkedin.tool.json +8935 -0
- package/firedrill/tools/linkedin/ops/comments-read.mjs +57 -0
- package/firedrill/tools/linkedin/ops/comments-write.mjs +104 -0
- package/firedrill/tools/linkedin/ops/feed.mjs +74 -0
- package/firedrill/tools/linkedin/ops/network.mjs +46 -0
- package/firedrill/tools/linkedin/ops/orgs.mjs +93 -0
- package/firedrill/tools/linkedin/ops/posts-edit.mjs +71 -0
- package/firedrill/tools/linkedin/ops/posts-read.mjs +54 -0
- package/firedrill/tools/linkedin/ops/posts-write.mjs +74 -0
- package/firedrill/tools/linkedin/ops/profile.mjs +38 -0
- package/firedrill/tools/linkedin/ops/reactions.mjs +78 -0
- package/firedrill/tools/linkedin/ops/social.mjs +34 -0
- package/firedrill/world.json +2585 -0
- package/firedrill.json +5 -0
- package/package.json +52 -0
- package/starter.json +1833 -0
- package/test/client.mjs +71 -0
- package/test/conformance.mjs +20 -0
- package/test/fixtures.mjs +182 -0
- package/test/flows/comments.mjs +116 -0
- package/test/flows/every.mjs +38 -0
- package/test/flows/faults.mjs +56 -0
- package/test/flows/identity.mjs +70 -0
- package/test/flows/orgs.mjs +107 -0
- package/test/flows/posts.mjs +196 -0
- package/test/flows/reactions.mjs +101 -0
- package/test/flows/wire.mjs +62 -0
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
// My Network > Connections: count header, search, "Recently added" list with paging.
|
|
2
|
+
import { el, call, describe, avatar, longDate, busy } from "./ui.js";
|
|
3
|
+
import { icon } from "./icons.js";
|
|
4
|
+
import { footer } from "./feed.js";
|
|
5
|
+
|
|
6
|
+
const PAGE = 10;
|
|
7
|
+
|
|
8
|
+
export function renderNetwork(main) {
|
|
9
|
+
const header = el("h1", { class: "network__count", text: "Connections" });
|
|
10
|
+
const input = el("input", { class: "network__search-input", attrs: { id: "network-search", type: "search", placeholder: "Search by name", autocomplete: "off" } });
|
|
11
|
+
const list = el("ul", { class: "network__list", attrs: { "aria-live": "polite" } });
|
|
12
|
+
const more = el("div", { class: "network__more" });
|
|
13
|
+
let query = "";
|
|
14
|
+
let loaded = 0;
|
|
15
|
+
let token = 0;
|
|
16
|
+
|
|
17
|
+
const load = async (reset) => {
|
|
18
|
+
const mine = ++token;
|
|
19
|
+
if (reset) { loaded = 0; list.replaceChildren(el("li", { class: "network__loading" }, el("span", { class: "spinner", attrs: { "aria-label": "Loading connections" } }))); }
|
|
20
|
+
more.replaceChildren();
|
|
21
|
+
try {
|
|
22
|
+
const page = await call("connections.list", { query, start: loaded, count: PAGE });
|
|
23
|
+
if (mine !== token) return;
|
|
24
|
+
if (reset) list.replaceChildren();
|
|
25
|
+
const total = page.paging.total ?? 0;
|
|
26
|
+
if (!query) header.textContent = `${total} Connection${total === 1 ? "" : "s"}`;
|
|
27
|
+
for (const { person, connectedAt } of page.elements) {
|
|
28
|
+
const name = `${person.localizedFirstName} ${person.localizedLastName}`.trim();
|
|
29
|
+
const href = `#/in/${encodeURIComponent(person.id)}`;
|
|
30
|
+
list.append(el("li", { class: "network__row" }, [
|
|
31
|
+
el("a", { attrs: { href, tabindex: "-1", "aria-hidden": "true" } }, avatar(name, `urn:li:person:${person.id}`, 72)),
|
|
32
|
+
el("div", { class: "network__meta" }, [
|
|
33
|
+
el("a", { class: "network__name", text: name, attrs: { href } }),
|
|
34
|
+
person.localizedHeadline ? el("span", { class: "network__headline", text: person.localizedHeadline }) : null,
|
|
35
|
+
el("span", { class: "network__date", text: `Connected on ${longDate(connectedAt)}` }),
|
|
36
|
+
]),
|
|
37
|
+
el("div", { class: "network__actions" }, [
|
|
38
|
+
el("button", { class: "btn btn--secondary btn--sm", text: "Message", attrs: { type: "button", "data-not-simulated": "Messaging" } }),
|
|
39
|
+
el("button", { class: "icon-btn", title: "More actions", attrs: { type: "button", "aria-label": `More actions for ${name}`, "data-not-simulated": "Removing connections" } }, icon("ellipsis")),
|
|
40
|
+
]),
|
|
41
|
+
]));
|
|
42
|
+
}
|
|
43
|
+
loaded += page.elements.length;
|
|
44
|
+
if (loaded === 0) list.append(el("li", { class: "network__empty" }, query
|
|
45
|
+
? [el("p", { class: "t-bold", text: "No results found" }), el("p", { class: "muted", text: "Try searching for a different name." })]
|
|
46
|
+
: [el("p", { class: "t-bold", text: "You don't have any connections yet" }), el("p", { class: "muted", text: "Connections you make will appear here." })]));
|
|
47
|
+
if (loaded < total) more.append(el("button", { class: "btn btn--tertiary", text: "Show more results", attrs: { type: "button" }, on: { click: (e) => busy(e.currentTarget, () => load(false)) } }));
|
|
48
|
+
} catch (error) {
|
|
49
|
+
if (mine !== token) return;
|
|
50
|
+
list.replaceChildren(el("li", { class: "network__empty", attrs: { role: "alert" } }, [el("p", { class: "t-bold", text: "Couldn't load your connections" }), el("p", { class: "muted", text: describe(error) }), el("button", { class: "btn btn--secondary btn--sm", text: "Retry", attrs: { type: "button" }, on: { click: () => load(true) } })]));
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
let timer;
|
|
55
|
+
input.addEventListener("input", () => { clearTimeout(timer); timer = setTimeout(() => { query = input.value.trim().slice(0, 200); void load(true); }, 200); });
|
|
56
|
+
|
|
57
|
+
const manage = el("nav", { class: "card network-manage", attrs: { "aria-label": "Manage my network" } }, [
|
|
58
|
+
el("h2", { class: "network-manage__title", text: "Manage my network" }),
|
|
59
|
+
el("a", { class: "network-manage__item network-manage__item--active", attrs: { href: "#/mynetwork", "aria-current": "page" } }, [icon("nav-network"), el("span", { text: "Connections" })]),
|
|
60
|
+
...[["people", "Following & followers"], ["group", "Groups"], ["calendar", "Events"], ["nav-grid", "Pages"], ["newsletter", "Newsletters"]].map(([glyph, label]) =>
|
|
61
|
+
el("button", { class: "network-manage__item", attrs: { type: "button", "data-not-simulated": label } }, [icon(glyph), el("span", { text: label })])),
|
|
62
|
+
]);
|
|
63
|
+
|
|
64
|
+
main.replaceChildren(el("div", { class: "network-layout" }, [
|
|
65
|
+
el("aside", {}, [manage, footer()]),
|
|
66
|
+
el("section", { class: "card network" }, [
|
|
67
|
+
header,
|
|
68
|
+
el("div", { class: "network__toolbar" }, [
|
|
69
|
+
el("span", { class: "network__sort" }, [el("span", { class: "muted", text: "Sort by: " }), el("button", { class: "network__sort-btn", attrs: { type: "button", "data-not-simulated": "Other connection sort orders" } }, [el("span", { text: "Recently added" }), icon("caret-down")])]),
|
|
70
|
+
el("div", { class: "network__search" }, [icon("search"), el("label", { class: "visually-hidden", text: "Search connections by name", attrs: { for: "network-search" } }), input]),
|
|
71
|
+
]),
|
|
72
|
+
list, more,
|
|
73
|
+
]),
|
|
74
|
+
]));
|
|
75
|
+
void load(true);
|
|
76
|
+
return { refresh: () => load(true) };
|
|
77
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
.modal-scrim { position: fixed; inset: 0; z-index: 40; display: flex; align-items: flex-start; justify-content: center; padding: 48px 16px; background: rgba(0, 0, 0, 0.75); overflow-y: auto; }
|
|
2
|
+
.modal { width: 100%; max-width: 552px; background: #fff; border-radius: 8px; box-shadow: 0 0 0 1px var(--hairline), 0 4px 16px rgba(0, 0, 0, .3); display: flex; flex-direction: column; max-height: calc(100vh - 96px); }
|
|
3
|
+
.modal__header { display: flex; align-items: center; justify-content: space-between; padding: 12px 12px 12px 24px; border-bottom: 1px solid var(--divider); }
|
|
4
|
+
.modal__title { font-size: 20px; line-height: 1.4; font-weight: 400; color: var(--text); }
|
|
5
|
+
.modal__body { padding: 16px 24px; overflow-y: auto; }
|
|
6
|
+
.modal__footer { display: flex; justify-content: flex-end; align-items: center; gap: 8px; padding: 12px 16px; border-top: 1px solid var(--divider); }
|
|
7
|
+
.modal__footer:empty { display: none; }
|
|
8
|
+
.modal--confirm { max-width: 480px; margin-top: 15vh; }
|
|
9
|
+
.confirm__text { font-size: 14px; color: var(--text); }
|
|
10
|
+
.not-sim { display: flex; gap: 12px; }
|
|
11
|
+
.not-sim__icon { color: var(--blue); }
|
|
12
|
+
.not-sim p + p { margin-top: 4px; }
|
|
13
|
+
|
|
14
|
+
.modal--composer { max-width: 744px; min-height: 480px; }
|
|
15
|
+
.modal--composer .modal__header { border-bottom: 0; }
|
|
16
|
+
.modal--composer .modal__header { padding: 16px 12px 0 16px; align-items: flex-start; }
|
|
17
|
+
.composer__who { display: inline-flex; align-items: center; gap: 12px; padding: 8px 12px 8px 8px; border: 0; border-radius: 12px; background: none; text-align: left; cursor: pointer; }
|
|
18
|
+
.composer__who:hover:not(:disabled) { background: var(--hover); }
|
|
19
|
+
.composer__who:disabled { cursor: default; }
|
|
20
|
+
.composer__who-text { display: flex; flex-direction: column; }
|
|
21
|
+
.composer__who-name { display: flex; align-items: center; gap: 2px; font-size: 20px; font-weight: 600; line-height: 1.4; }
|
|
22
|
+
.composer__who-vis { display: flex; align-items: center; gap: 4px; font-size: 14px; color: var(--text); }
|
|
23
|
+
.composer__who-vis .icon { width: 16px; height: 16px; }
|
|
24
|
+
.composer__text { display: block; width: 100%; min-height: 240px; margin-top: 4px; padding: 0; border: 0; resize: vertical; font-size: 18px; line-height: 1.5; color: var(--text); background: transparent; }
|
|
25
|
+
.composer__text:focus { outline: none; }
|
|
26
|
+
.composer__text::placeholder { color: rgba(0, 0, 0, 0.6); }
|
|
27
|
+
.composer__embed { margin-top: 8px; padding: 12px; border: 1px solid #e0dfdc; border-radius: 8px; }
|
|
28
|
+
.composer__embed-head { display: flex; align-items: center; gap: 8px; margin-bottom: 8px; }
|
|
29
|
+
.composer__embed-text { color: var(--text); white-space: pre-wrap; display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden; }
|
|
30
|
+
.composer__link { display: grid; gap: 4px; margin-top: 12px; padding: 12px; border-radius: 8px; background: #f3f6f8; }
|
|
31
|
+
.composer__error { margin-top: 12px; padding: 8px 12px; border-radius: 4px; background: #fce8e6; color: #b3080f; font-size: 14px; }
|
|
32
|
+
.modal--composer .modal__footer { justify-content: space-between; border-top: 0; padding: 8px 24px 16px; }
|
|
33
|
+
.composer__tools { display: flex; gap: 4px; }
|
|
34
|
+
.composer__tools .icon-btn { color: var(--text-2); }
|
|
35
|
+
.composer__submit-row { display: flex; align-items: center; gap: 12px; }
|
|
36
|
+
.composer__counter { font-size: 14px; color: var(--text-2); }
|
|
37
|
+
.composer__counter--over { color: var(--danger); font-weight: 600; }
|
|
38
|
+
|
|
39
|
+
.field__label { font-size: 14px; color: var(--text-2); margin-top: 4px; }
|
|
40
|
+
.field__input { width: 100%; height: 32px; padding: 4px 8px; border: 1px solid rgba(0, 0, 0, .6); border-radius: 4px; background: #fff; font-size: 14px; }
|
|
41
|
+
.field__input:focus { outline: none; box-shadow: inset 0 0 0 1px #000; }
|
|
42
|
+
|
|
43
|
+
.reactors__tabs { display: flex; gap: 0; border-bottom: 1px solid var(--divider); margin: -16px -24px 0; padding: 0 16px; overflow-x: auto; }
|
|
44
|
+
.reactors__tab { display: inline-flex; align-items: center; gap: 4px; padding: 12px; border: 0; background: none; font-weight: 600; color: var(--text-2); cursor: pointer; border-bottom: 2px solid transparent; }
|
|
45
|
+
.reactors__tab[aria-selected="true"] { color: var(--green); border-bottom-color: var(--green); }
|
|
46
|
+
.reactors__tab:hover { background: var(--hover); }
|
|
47
|
+
.reactors__list { margin: 0 -24px; }
|
|
48
|
+
.reactors__row { display: flex; align-items: center; gap: 12px; padding: 8px 24px; }
|
|
49
|
+
.reactors__avatar { position: relative; }
|
|
50
|
+
.reactors__avatar .reaction-badge { position: absolute; right: -2px; bottom: -2px; border: 2px solid #fff; border-radius: 50%; box-sizing: content-box; }
|
|
51
|
+
.reactors__meta { min-width: 0; flex: 1; border-bottom: 1px solid var(--divider); padding-bottom: 8px; }
|
|
52
|
+
.reactors__name { font-weight: 600; color: var(--text); }
|
|
53
|
+
.reactors__sub { font-size: 12px; color: var(--text-2); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
54
|
+
.modal--reactors .modal__body { min-height: 320px; }
|
|
55
|
+
|
|
56
|
+
.toast { position: fixed; left: 24px; bottom: 24px; z-index: 60; display: flex; align-items: center; gap: 8px; min-width: 320px; max-width: 480px; padding: 12px 8px 12px 16px; border-radius: 8px; background: #fff; box-shadow: 0 0 0 1px var(--hairline), 0 4px 16px rgba(0, 0, 0, .2); font-size: 14px; color: var(--text); animation: pop .15s ease-out; }
|
|
57
|
+
.toast::before { content: ""; position: absolute; left: 0; top: 0; bottom: 0; width: 4px; border-radius: 8px 0 0 8px; background: #057642; }
|
|
58
|
+
.toast[data-error="true"]::before { background: var(--danger); }
|
|
59
|
+
.toast__icon { color: #057642; }
|
|
60
|
+
.toast[data-error="true"] .toast__icon { color: var(--danger); }
|
|
61
|
+
.toast__text { flex: 1; }
|
|
62
|
+
.toast__action { border: 0; background: none; color: var(--blue); font-weight: 600; cursor: pointer; }
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
// Modals, confirmation dialogs, dropdown menus and the "not simulated" panel.
|
|
2
|
+
import { $, el, iconButton } from "./ui.js";
|
|
3
|
+
import { icon } from "./icons.js";
|
|
4
|
+
|
|
5
|
+
let openModalState = null;
|
|
6
|
+
|
|
7
|
+
/** Open a modal dialog. Returns { body, footer, close }. */
|
|
8
|
+
export function openModal({ title, className = "", onClose, labelledBy } = {}) {
|
|
9
|
+
closeModal();
|
|
10
|
+
const titleId = `modal-title-${Math.random().toString(36).slice(2)}`;
|
|
11
|
+
const heading = el("h2", { class: "modal__title", text: title, attrs: { id: titleId } });
|
|
12
|
+
const closeButton = iconButton("close", "Dismiss", () => closeModal());
|
|
13
|
+
const body = el("div", { class: "modal__body" });
|
|
14
|
+
const footer = el("div", { class: "modal__footer" });
|
|
15
|
+
const dialog = el("div", { class: `modal ${className}`.trim(), attrs: { role: "dialog", "aria-modal": "true", "aria-labelledby": labelledBy ?? titleId } }, [
|
|
16
|
+
el("div", { class: "modal__header" }, [heading, closeButton]), body, footer,
|
|
17
|
+
]);
|
|
18
|
+
const scrim = el("div", { class: "modal-scrim", on: { mousedown: (event) => { if (event.target === scrim) closeModal(); } } }, dialog);
|
|
19
|
+
const previous = document.activeElement;
|
|
20
|
+
const onKey = (event) => { if (event.key === "Escape") { event.preventDefault(); closeModal(); } };
|
|
21
|
+
document.addEventListener("keydown", onKey);
|
|
22
|
+
$("#overlay-root").append(scrim);
|
|
23
|
+
openModalState = { scrim, onKey, previous, onClose };
|
|
24
|
+
queueMicrotask(() => (dialog.querySelector("textarea, input, [data-autofocus]") ?? closeButton).focus());
|
|
25
|
+
return { dialog, body, footer, heading, close: closeModal };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function closeModal() {
|
|
29
|
+
if (!openModalState) return;
|
|
30
|
+
const { scrim, onKey, previous, onClose } = openModalState;
|
|
31
|
+
openModalState = null;
|
|
32
|
+
document.removeEventListener("keydown", onKey);
|
|
33
|
+
scrim.remove();
|
|
34
|
+
if (onClose) onClose();
|
|
35
|
+
if (previous && typeof previous.focus === "function" && document.contains(previous)) previous.focus();
|
|
36
|
+
}
|
|
37
|
+
export const modalOpen = () => openModalState !== null;
|
|
38
|
+
|
|
39
|
+
/** LinkedIn-style confirmation: resolves true when the primary button is chosen. */
|
|
40
|
+
export function confirmDialog({ title, message, confirmLabel, cancelLabel = "Cancel", danger = false }) {
|
|
41
|
+
return new Promise((resolve) => {
|
|
42
|
+
let answered = false;
|
|
43
|
+
const modal = openModal({ title, className: "modal--confirm", onClose: () => { if (!answered) resolve(false); } });
|
|
44
|
+
modal.body.append(el("p", { class: "confirm__text", text: message }));
|
|
45
|
+
const cancel = el("button", { class: "btn btn--muted", text: cancelLabel, attrs: { type: "button" }, on: { click: () => closeModal() } });
|
|
46
|
+
const ok = el("button", { class: `btn ${danger ? "btn--primary" : "btn--primary"}`, text: confirmLabel, attrs: { type: "button", "data-autofocus": true }, on: { click: () => { answered = true; closeModal(); resolve(true); } } });
|
|
47
|
+
modal.footer.append(cancel, ok);
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Explain that a visible control belongs to a part of the product this Tool does not simulate. */
|
|
52
|
+
export function notSimulated(feature) {
|
|
53
|
+
const modal = openModal({ title: feature, className: "modal--confirm" });
|
|
54
|
+
modal.body.append(
|
|
55
|
+
el("div", { class: "not-sim" }, [
|
|
56
|
+
el("span", { class: "not-sim__icon" }, icon("info")),
|
|
57
|
+
el("div", {}, [
|
|
58
|
+
el("p", { class: "t-bold", text: "Not simulated by this Tool" }),
|
|
59
|
+
el("p", { class: "muted", text: `${feature} is part of the real product but is outside this Firedrill Tool's scope. Nothing was changed and no data is shown for it.` }),
|
|
60
|
+
]),
|
|
61
|
+
]),
|
|
62
|
+
);
|
|
63
|
+
modal.footer.append(el("button", { class: "btn btn--primary", text: "Got it", attrs: { type: "button", "data-autofocus": true }, on: { click: () => closeModal() } }));
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
let openMenu = null;
|
|
67
|
+
export function closeMenu() {
|
|
68
|
+
if (!openMenu) return;
|
|
69
|
+
openMenu.menu.remove();
|
|
70
|
+
openMenu.trigger.setAttribute("aria-expanded", "false");
|
|
71
|
+
document.removeEventListener("mousedown", openMenu.outside, true);
|
|
72
|
+
document.removeEventListener("keydown", openMenu.onKey, true);
|
|
73
|
+
openMenu = null;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Dropdown menu anchored under a trigger. items: [{icon, label, sub, onSelect, disabled}] */
|
|
77
|
+
export function showMenu(trigger, items, { align = "right" } = {}) {
|
|
78
|
+
const wasOpen = openMenu?.trigger === trigger;
|
|
79
|
+
closeMenu();
|
|
80
|
+
if (wasOpen) return;
|
|
81
|
+
const menu = el("div", { class: `dropdown dropdown--${align}`, attrs: { role: "menu" } });
|
|
82
|
+
for (const item of items) {
|
|
83
|
+
const button = el("button", { class: "dropdown__item", attrs: { type: "button", role: "menuitem", disabled: item.disabled ? true : undefined }, on: { click: () => { closeMenu(); item.onSelect?.(); } } }, [
|
|
84
|
+
item.icon ? icon(item.icon) : null,
|
|
85
|
+
el("span", { class: "dropdown__text" }, [el("span", { class: "dropdown__label", text: item.label }), item.sub ? el("span", { class: "dropdown__sub", text: item.sub }) : null]),
|
|
86
|
+
]);
|
|
87
|
+
menu.append(button);
|
|
88
|
+
}
|
|
89
|
+
trigger.parentElement.style.position = trigger.parentElement.style.position || "relative";
|
|
90
|
+
trigger.after(menu);
|
|
91
|
+
trigger.setAttribute("aria-expanded", "true");
|
|
92
|
+
const outside = (event) => { if (!menu.contains(event.target) && event.target !== trigger && !trigger.contains(event.target)) closeMenu(); };
|
|
93
|
+
const onKey = (event) => { if (event.key === "Escape") { closeMenu(); trigger.focus(); } };
|
|
94
|
+
document.addEventListener("mousedown", outside, true);
|
|
95
|
+
document.addEventListener("keydown", onKey, true);
|
|
96
|
+
openMenu = { menu, trigger, outside, onKey };
|
|
97
|
+
menu.querySelector("button:not([disabled])")?.focus();
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** Delegate every [data-not-simulated] control in the document to the panel. */
|
|
101
|
+
export function wireNotSimulated() {
|
|
102
|
+
document.addEventListener("click", (event) => {
|
|
103
|
+
const target = event.target instanceof Element ? event.target.closest("[data-not-simulated]") : null;
|
|
104
|
+
if (!target) return;
|
|
105
|
+
event.preventDefault();
|
|
106
|
+
notSimulated(target.getAttribute("data-not-simulated"));
|
|
107
|
+
});
|
|
108
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
.profile-layout { display: grid; grid-template-columns: minmax(0, 804px) 300px; gap: 24px; justify-content: center; align-items: start; }
|
|
2
|
+
.profile-main > * + *, .company-main > * + * { margin-top: 8px; }
|
|
3
|
+
.profile-aside > * + * { margin-top: 8px; }
|
|
4
|
+
.aside-card { padding: 16px 24px; }
|
|
5
|
+
.aside-card__title { font-size: 20px; font-weight: 600; margin-bottom: 4px; }
|
|
6
|
+
|
|
7
|
+
.profile-top { position: relative; overflow: hidden; }
|
|
8
|
+
.profile-top__loading { display: flex; justify-content: center; padding: 64px; }
|
|
9
|
+
.profile-top__cover { height: 201px; background: linear-gradient(120deg, #a0b4b7 0%, #c8d4d6 45%, #e6ecec 100%); }
|
|
10
|
+
.profile-top__cover--company { height: 170px; background: linear-gradient(120deg, #9db3c8 0%, #c7d6e4 55%, #e3ebf2 100%); }
|
|
11
|
+
.profile-top__body { position: relative; padding: 0 24px 24px; }
|
|
12
|
+
.profile-top__avatar { margin-top: -112px; margin-bottom: 12px; }
|
|
13
|
+
.profile-top__avatar .avatar { box-shadow: none; }
|
|
14
|
+
.profile-top__edit { position: absolute; top: 12px; right: 16px; }
|
|
15
|
+
.profile-top__name { font-size: 24px; line-height: 1.25; font-weight: 600; }
|
|
16
|
+
.profile-top__headline { margin-top: 2px; font-size: 16px; line-height: 1.5; color: var(--text); }
|
|
17
|
+
.profile-top__meta { margin-top: 8px; font-size: 14px; color: var(--text-2); }
|
|
18
|
+
.profile-top__contact { padding: 0; border: 0; background: none; color: var(--blue); font-weight: 600; cursor: pointer; }
|
|
19
|
+
.profile-top__contact:hover { text-decoration: underline; }
|
|
20
|
+
.profile-top__connections { margin-top: 4px; font-weight: 600; }
|
|
21
|
+
.profile-top__actions { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 12px; }
|
|
22
|
+
.contact__title { font-size: 20px; font-weight: 600; margin-bottom: 12px; }
|
|
23
|
+
.contact__row { display: flex; gap: 16px; margin-bottom: 16px; }
|
|
24
|
+
.company-note { padding: 12px 24px; border-top: 1px solid var(--divider); color: var(--text-2); }
|
|
25
|
+
|
|
26
|
+
.section-head { display: flex; justify-content: space-between; align-items: flex-start; gap: 12px; padding: 12px 24px 0; }
|
|
27
|
+
.section-head__title { font-size: 20px; line-height: 1.4; font-weight: 600; }
|
|
28
|
+
.section-head__sub { display: block; font-size: 14px; font-weight: 600; }
|
|
29
|
+
.profile-activity { padding-bottom: 8px; }
|
|
30
|
+
.profile-activity__empty { padding: 16px 24px; color: var(--text-2); }
|
|
31
|
+
.chips { display: flex; gap: 8px; padding: 12px 24px 4px; }
|
|
32
|
+
.chip { height: 32px; padding: 0 12px; border: 0; border-radius: 16px; box-shadow: inset 0 0 0 1px rgba(0, 0, 0, .6); background: #fff; font-weight: 600; color: var(--text-2); cursor: pointer; }
|
|
33
|
+
.chip:hover { box-shadow: inset 0 0 0 2px rgba(0, 0, 0, .75); background: var(--hover); }
|
|
34
|
+
.chip[aria-pressed="true"] { background: var(--green); color: #fff; box-shadow: none; }
|
|
35
|
+
.activity-list { padding: 0 24px; }
|
|
36
|
+
.activity-list > .spinner { margin: 16px auto; display: block; }
|
|
37
|
+
.activity-row { padding: 12px 0; border-bottom: 1px solid var(--divider); }
|
|
38
|
+
.activity-row__meta { display: flex; align-items: center; gap: 8px; font-size: 12px; color: var(--text-2); }
|
|
39
|
+
.activity-row__text { margin-top: 4px; white-space: pre-wrap; display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden; }
|
|
40
|
+
.activity-row__actions { margin-top: 8px; }
|
|
41
|
+
.activity-more { display: flex; justify-content: center; padding: 8px; }
|
|
42
|
+
.badge { padding: 0 6px; border-radius: 4px; font-size: 12px; font-weight: 600; }
|
|
43
|
+
.badge--draft { background: #fcf0d2; color: #915907; }
|
|
44
|
+
|
|
45
|
+
.company-about { padding: 16px 24px; }
|
|
46
|
+
.company-about .section-head__title { margin-bottom: 8px; }
|
|
47
|
+
.company-about__text { white-space: pre-wrap; margin-bottom: 12px; }
|
|
48
|
+
.company-about__facts { margin: 0; }
|
|
49
|
+
.company-about__facts dt { font-weight: 600; margin-top: 12px; }
|
|
50
|
+
.company-about__facts dd { margin: 2px 0 0; color: var(--text-2); overflow-wrap: anywhere; }
|
|
51
|
+
.company-admins__row { display: flex; gap: 12px; align-items: center; padding: 8px 0; }
|
|
52
|
+
|
|
53
|
+
.network-layout { display: grid; grid-template-columns: 300px minmax(0, 804px); gap: 24px; justify-content: center; align-items: start; }
|
|
54
|
+
.network-manage { padding: 12px 0; }
|
|
55
|
+
.network-manage__title { padding: 0 16px 8px; font-size: 16px; font-weight: 600; }
|
|
56
|
+
.network-manage__item { display: flex; align-items: center; gap: 12px; width: 100%; padding: 10px 16px; border: 0; background: none; font-size: 16px; font-weight: 600; color: var(--text-2); cursor: pointer; text-decoration: none !important; }
|
|
57
|
+
.network-manage__item:hover { background: var(--hover); color: var(--text); }
|
|
58
|
+
.network-manage__item--active { color: var(--green); box-shadow: inset 2px 0 0 var(--green); }
|
|
59
|
+
.network { padding-bottom: 8px; }
|
|
60
|
+
.network__count { padding: 16px 24px 8px; font-size: 20px; font-weight: 400; }
|
|
61
|
+
.network__toolbar { display: flex; justify-content: space-between; align-items: center; gap: 16px; padding: 0 24px 8px; border-bottom: 1px solid var(--divider); }
|
|
62
|
+
.network__sort { display: flex; align-items: center; font-size: 14px; }
|
|
63
|
+
.network__sort-btn { display: inline-flex; align-items: center; gap: 2px; border: 0; background: none; font-weight: 600; color: var(--text-2); cursor: pointer; }
|
|
64
|
+
.network__sort-btn .icon { width: 16px; height: 16px; }
|
|
65
|
+
.network__search { position: relative; display: flex; align-items: center; }
|
|
66
|
+
.network__search > .icon { position: absolute; left: 8px; width: 16px; height: 16px; color: var(--text-2); }
|
|
67
|
+
.network__search-input { width: 240px; height: 32px; padding: 0 8px 0 32px; border: 1px solid rgba(0, 0, 0, .6); border-radius: 4px; }
|
|
68
|
+
.network__row { display: flex; align-items: center; gap: 16px; margin: 0 24px; padding: 16px 0; border-bottom: 1px solid var(--divider); }
|
|
69
|
+
.network__meta { display: flex; flex-direction: column; flex: 1; min-width: 0; }
|
|
70
|
+
.network__name { font-size: 16px; font-weight: 600; color: var(--text); }
|
|
71
|
+
.network__name:hover { color: var(--blue); }
|
|
72
|
+
.network__headline { font-size: 14px; color: var(--text-2); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
73
|
+
.network__date { margin-top: 4px; font-size: 12px; color: var(--text-2); }
|
|
74
|
+
.network__actions { display: flex; align-items: center; gap: 4px; }
|
|
75
|
+
.network__empty, .network__loading { padding: 32px 24px; text-align: center; }
|
|
76
|
+
.network__more { display: flex; justify-content: center; padding: 8px; }
|
|
77
|
+
|
|
78
|
+
@media (max-width: 991px) { .profile-layout, .network-layout { grid-template-columns: minmax(0, 1fr); } .profile-aside { display: none; } }
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
// Like (with the six-reaction picker), Comment, Repost and Send buttons of a post card.
|
|
2
|
+
import { el, call, newKey, describe, toast } from "./ui.js";
|
|
3
|
+
import { icon } from "./icons.js";
|
|
4
|
+
import { REACTIONS, REACTION_BY_TYPE, reactionBadge } from "./reactions.js";
|
|
5
|
+
import { session } from "./store.js";
|
|
6
|
+
import { toggleComments } from "./comments.js";
|
|
7
|
+
import { openComposer } from "./composer.js";
|
|
8
|
+
import { notSimulated } from "./overlay.js";
|
|
9
|
+
|
|
10
|
+
export function renderActions(host, entry, state, { paint, commentsHost, ctx }) {
|
|
11
|
+
const { post } = entry;
|
|
12
|
+
const likeWrap = el("div", { class: "social-actions__like" });
|
|
13
|
+
const likeButton = el("button", { class: "social-action", attrs: { type: "button", "aria-pressed": "false" } });
|
|
14
|
+
let picker = null;
|
|
15
|
+
let hoverTimer;
|
|
16
|
+
let pendingReaction = false;
|
|
17
|
+
|
|
18
|
+
const paintLike = () => {
|
|
19
|
+
const reaction = state.viewerReaction ? REACTION_BY_TYPE.get(state.viewerReaction) : null;
|
|
20
|
+
likeButton.replaceChildren(reaction && reaction.type !== "LIKE" ? reactionBadge(reaction.type, 20) : icon("like"), el("span", { text: reaction ? reaction.label : "Like" }));
|
|
21
|
+
likeButton.setAttribute("aria-pressed", String(Boolean(reaction)));
|
|
22
|
+
likeButton.setAttribute("aria-label", reaction ? `Undo ${reaction.label} reaction` : "React Like");
|
|
23
|
+
if (reaction) likeButton.dataset.reaction = reaction.type; else delete likeButton.dataset.reaction;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const react = async (type) => {
|
|
27
|
+
if (pendingReaction) return;
|
|
28
|
+
pendingReaction = true;
|
|
29
|
+
closePicker();
|
|
30
|
+
const actor = session.viewer.personUrn;
|
|
31
|
+
const previous = state.viewerReaction;
|
|
32
|
+
try {
|
|
33
|
+
if (type === null) await call("reactions.delete", { actor, entity: post.id }, newKey());
|
|
34
|
+
else await call("reactions.create", { actor, root: post.id, reactionType: type }, newKey());
|
|
35
|
+
state.viewerReaction = type;
|
|
36
|
+
paintLike();
|
|
37
|
+
await host.closest(".feed-post")?.refreshSocial?.();
|
|
38
|
+
} catch (error) {
|
|
39
|
+
state.viewerReaction = previous;
|
|
40
|
+
paintLike();
|
|
41
|
+
toast(describe(error), { error: true });
|
|
42
|
+
} finally {
|
|
43
|
+
pendingReaction = false;
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const openPicker = () => {
|
|
48
|
+
if (picker) return;
|
|
49
|
+
picker = el("div", { class: "reaction-picker", attrs: { role: "toolbar", "aria-label": "Reactions" } }, REACTIONS.map((reaction) =>
|
|
50
|
+
el("button", { class: "reaction-picker__item", attrs: { type: "button", "aria-label": reaction.label }, on: { click: () => react(reaction.type) } }, [
|
|
51
|
+
reactionBadge(reaction.type, 36), el("span", { class: "reaction-picker__tip", text: reaction.label, attrs: { "aria-hidden": "true" } }),
|
|
52
|
+
])));
|
|
53
|
+
picker.addEventListener("mouseenter", () => clearTimeout(hoverTimer));
|
|
54
|
+
picker.addEventListener("mouseleave", () => { hoverTimer = setTimeout(closePicker, 300); });
|
|
55
|
+
picker.addEventListener("keydown", (event) => { if (event.key === "Escape") { closePicker(); likeButton.focus(); } });
|
|
56
|
+
likeWrap.append(picker);
|
|
57
|
+
};
|
|
58
|
+
const closePicker = () => { picker?.remove(); picker = null; };
|
|
59
|
+
|
|
60
|
+
likeButton.addEventListener("click", () => react(state.viewerReaction ? null : "LIKE"));
|
|
61
|
+
likeButton.addEventListener("mouseenter", () => { clearTimeout(hoverTimer); hoverTimer = setTimeout(openPicker, 500); });
|
|
62
|
+
likeButton.addEventListener("mouseleave", () => { clearTimeout(hoverTimer); hoverTimer = setTimeout(closePicker, 400); });
|
|
63
|
+
likeButton.addEventListener("keydown", (event) => {
|
|
64
|
+
if (event.key === "ArrowUp" || (event.key === "Enter" && event.shiftKey)) { event.preventDefault(); openPicker(); picker.querySelector("button").focus(); }
|
|
65
|
+
});
|
|
66
|
+
paintLike();
|
|
67
|
+
likeWrap.append(likeButton);
|
|
68
|
+
|
|
69
|
+
const commentButton = el("button", { class: "social-action", attrs: { type: "button", "aria-label": "Comment" }, on: {
|
|
70
|
+
click: () => toggleComments(commentsHost, entry, { focus: true, onChanged: () => host.closest(".feed-post")?.refreshSocial?.() }),
|
|
71
|
+
} }, [icon("comment"), el("span", { text: "Comment" })]);
|
|
72
|
+
|
|
73
|
+
const reshareDisabled = post.isReshareDisabledByAuthor || post.visibility !== "PUBLIC";
|
|
74
|
+
const repostTarget = entry.reshareOf && !post.commentary ? entry.reshareOf : entry;
|
|
75
|
+
const repostButton = el("button", { class: "social-action", title: reshareDisabled ? "The author has disabled reposting for this post" : "Repost", attrs: { type: "button", "aria-label": "Repost", disabled: reshareDisabled ? true : undefined }, on: {
|
|
76
|
+
click: () => openComposer({ repostOf: repostTarget, onDone: ctx.onReload }),
|
|
77
|
+
} }, [icon("repost"), el("span", { text: "Repost" })]);
|
|
78
|
+
|
|
79
|
+
const sendButton = el("button", { class: "social-action", attrs: { type: "button", "aria-label": "Send in a private message" }, on: { click: () => notSimulated("Sending posts in messages") } }, [icon("send"), el("span", { text: "Send" })]);
|
|
80
|
+
|
|
81
|
+
host.append(likeWrap, el("div", {}, commentButton), el("div", {}, repostButton), el("div", {}, sendButton));
|
|
82
|
+
void paint;
|
|
83
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
// The "…" control menu on a post: edit, delete, comments on/off, copy link, and not-simulated entries.
|
|
2
|
+
import { call, newKey, describe, toast } from "./ui.js";
|
|
3
|
+
import { confirmDialog, notSimulated } from "./overlay.js";
|
|
4
|
+
import { openComposer } from "./composer.js";
|
|
5
|
+
import { canManage } from "./post.js";
|
|
6
|
+
|
|
7
|
+
export function postMenu(entry, ctx) {
|
|
8
|
+
const { post } = entry;
|
|
9
|
+
const items = [
|
|
10
|
+
{ icon: "bookmark", label: "Save", sub: "Save for later", onSelect: () => notSimulated("Saved items") },
|
|
11
|
+
{ icon: "link", label: "Copy link to post", onSelect: () => copyLink(post.id) },
|
|
12
|
+
];
|
|
13
|
+
if (canManage(post.author)) {
|
|
14
|
+
items.push(
|
|
15
|
+
{ icon: "pencil", label: "Edit post", onSelect: () => openComposer({ edit: entry, onDone: ctx.onReload }) },
|
|
16
|
+
{ icon: "trash", label: "Delete post", onSelect: () => deletePost(post, ctx) },
|
|
17
|
+
);
|
|
18
|
+
const closed = entry.social.commentsState === "CLOSED";
|
|
19
|
+
if (post.author === entry.actor.urn) {
|
|
20
|
+
items.push({
|
|
21
|
+
icon: closed ? "comment" : "comment-off",
|
|
22
|
+
label: closed ? "Turn on commenting" : "Turn off commenting",
|
|
23
|
+
sub: closed ? "Let people comment on this post" : "Existing comments will be deleted",
|
|
24
|
+
onSelect: () => setCommentsState(entry, closed ? "OPEN" : "CLOSED", ctx),
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
} else {
|
|
28
|
+
items.push({ icon: "close", label: "Not interested", sub: "See fewer posts like this", onSelect: () => notSimulated("Feed preferences") });
|
|
29
|
+
}
|
|
30
|
+
return items;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async function copyLink(urn) {
|
|
34
|
+
const text = `https://www.linkedin.com/feed/update/${urn}/`;
|
|
35
|
+
try {
|
|
36
|
+
await navigator.clipboard.writeText(text);
|
|
37
|
+
toast("Link copied to clipboard.");
|
|
38
|
+
} catch {
|
|
39
|
+
toast(`Copy this link: ${text}`);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function deletePost(post, ctx) {
|
|
44
|
+
const ok = await confirmDialog({ title: "Delete post?", message: "Are you sure you want to permanently remove this post from LinkedIn?", confirmLabel: "Delete", danger: true });
|
|
45
|
+
if (!ok) return;
|
|
46
|
+
try {
|
|
47
|
+
await call("posts.delete", { postUrn: post.id }, newKey());
|
|
48
|
+
toast("Post deleted.");
|
|
49
|
+
await ctx.onReload();
|
|
50
|
+
} catch (error) {
|
|
51
|
+
toast(describe(error), { error: true });
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async function setCommentsState(entry, next, ctx) {
|
|
56
|
+
if (next === "CLOSED") {
|
|
57
|
+
const count = entry.social.commentSummary?.count ?? 0;
|
|
58
|
+
const ok = await confirmDialog({
|
|
59
|
+
title: "Turn off commenting?",
|
|
60
|
+
message: count > 0 ? `This will delete the ${count} existing comment${count === 1 ? "" : "s"} on this post. People won't be able to comment.` : "People won't be able to comment on this post.",
|
|
61
|
+
confirmLabel: "Turn off",
|
|
62
|
+
});
|
|
63
|
+
if (!ok) return;
|
|
64
|
+
}
|
|
65
|
+
try {
|
|
66
|
+
await call("social_metadata.set_comments_state", { entity: entry.post.id, actor: entry.post.author, commentsState: next }, newKey());
|
|
67
|
+
toast(next === "CLOSED" ? "Commenting turned off." : "Commenting turned on.");
|
|
68
|
+
await ctx.onReload();
|
|
69
|
+
} catch (error) {
|
|
70
|
+
toast(describe(error), { error: true });
|
|
71
|
+
}
|
|
72
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
.feed-post { position: relative; padding-top: 12px; }
|
|
2
|
+
.feed-post__controls { position: absolute; top: 4px; right: 8px; display: flex; }
|
|
3
|
+
.feed-post__controls .icon-btn { width: 32px; height: 32px; }
|
|
4
|
+
.feed-post__controls .icon { width: 24px; height: 24px; }
|
|
5
|
+
|
|
6
|
+
.post-actor { display: flex; gap: 8px; padding: 0 16px; margin-right: 72px; margin-bottom: 8px; }
|
|
7
|
+
.post-actor__avatar { flex: none; }
|
|
8
|
+
.post-actor__meta { display: flex; flex-direction: column; min-width: 0; }
|
|
9
|
+
.post-actor__name-line { font-size: 14px; line-height: 20px; }
|
|
10
|
+
.post-actor__name { font-weight: 600; color: var(--text); }
|
|
11
|
+
.post-actor__name:hover { color: var(--blue); text-decoration: underline; }
|
|
12
|
+
.post-actor__degree { color: var(--text-2); font-weight: 400; }
|
|
13
|
+
.post-actor__headline { font-size: 12px; line-height: 16px; color: var(--text-2); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
14
|
+
.post-actor__time { display: block; white-space: pre; font-size: 12px; line-height: 16px; color: var(--text-2); }
|
|
15
|
+
.post-actor__vis { display: inline-block; vertical-align: -3px; }
|
|
16
|
+
.post-actor__vis .icon { width: 16px; height: 16px; }
|
|
17
|
+
.post-actor--compact { padding: 0; margin: 0 0 8px; }
|
|
18
|
+
|
|
19
|
+
.post-text { position: relative; padding: 0 16px; margin-bottom: 8px; font-size: 14px; line-height: 20px; color: var(--text); }
|
|
20
|
+
.post-text__inner { display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 3; overflow: hidden; white-space: pre-wrap; overflow-wrap: anywhere; }
|
|
21
|
+
.post-text--expanded .post-text__inner { display: block; -webkit-line-clamp: unset; }
|
|
22
|
+
.post-text__more { position: absolute; right: 16px; bottom: 0; padding: 0 0 0 24px; border: 0; background: linear-gradient(90deg, rgba(255,255,255,0), #fff 24px); color: var(--text-2); font-size: 14px; line-height: 20px; cursor: pointer; }
|
|
23
|
+
.post-text__more:hover { color: var(--blue); text-decoration: underline; }
|
|
24
|
+
.hashtag, .mention { font-weight: 600; color: var(--blue); }
|
|
25
|
+
|
|
26
|
+
.post-article { display: flex; align-items: stretch; margin: 4px 0 0; background: #eef3f8; color: var(--text); text-decoration: none !important; }
|
|
27
|
+
.post-article__art { display: flex; align-items: center; justify-content: center; flex: none; width: 138px; min-height: 72px; background: #dce6f1; color: #56687a; }
|
|
28
|
+
.post-article__meta { display: flex; flex-direction: column; justify-content: center; padding: 8px 12px; min-width: 0; }
|
|
29
|
+
.post-article__title { font-weight: 600; font-size: 14px; line-height: 20px; overflow: hidden; text-overflow: ellipsis; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; }
|
|
30
|
+
.post-article__source { font-size: 12px; color: var(--text-2); }
|
|
31
|
+
.post-article:hover .post-article__title { color: var(--blue); text-decoration: underline; }
|
|
32
|
+
|
|
33
|
+
.post-reshare { margin: 4px 16px 8px; padding: 12px 0 4px; border: 1px solid #e0dfdc; border-radius: 8px; }
|
|
34
|
+
.post-reshare .post-actor { padding: 0 12px; }
|
|
35
|
+
.post-reshare .post-text { padding: 0 12px; }
|
|
36
|
+
.post-reshare .post-article { margin: 0 0 8px; }
|
|
37
|
+
.post-reshare--gone { padding: 16px; color: var(--text-2); }
|
|
38
|
+
|
|
39
|
+
.social-counts { display: flex; align-items: center; justify-content: space-between; margin: 0 16px; padding: 8px 0; border-bottom: 1px solid var(--divider); font-size: 12px; color: var(--text-2); }
|
|
40
|
+
.social-counts__reactions { display: inline-flex; align-items: center; gap: 4px; padding: 0; border: 0; background: none; font-size: 12px; color: var(--text-2); cursor: pointer; }
|
|
41
|
+
.social-counts__reactions:hover { color: var(--blue); text-decoration: underline; }
|
|
42
|
+
.social-counts__badges { display: inline-flex; }
|
|
43
|
+
.social-counts__badges .reaction-badge { margin-right: -4px; border: 1px solid #fff; border-radius: 50%; box-sizing: content-box; }
|
|
44
|
+
.social-counts__badges .reaction-badge:last-child { margin-right: 2px; }
|
|
45
|
+
.social-counts__right { margin-left: auto; display: inline-flex; gap: 4px; }
|
|
46
|
+
.social-counts__link { padding: 0; border: 0; background: none; font-size: 12px; color: var(--text-2); cursor: pointer; }
|
|
47
|
+
.social-counts__link:hover { color: var(--blue); text-decoration: underline; }
|
|
48
|
+
.social-counts__link--static:hover { color: var(--text-2); text-decoration: none; cursor: default; }
|
|
49
|
+
|
|
50
|
+
.social-actions { display: flex; justify-content: space-around; padding: 4px 8px; }
|
|
51
|
+
.social-actions > * { flex: 1; display: flex; justify-content: center; position: relative; }
|
|
52
|
+
.social-action { display: inline-flex; align-items: center; justify-content: center; gap: 4px; width: 100%; min-height: 48px; padding: 10px 8px; border: 0; border-radius: 4px; background: none; font-size: 14px; font-weight: 600; color: var(--text-2); cursor: pointer; }
|
|
53
|
+
.social-action:hover { background: var(--hover); color: var(--text); }
|
|
54
|
+
.social-action:disabled { color: rgba(0, 0, 0, 0.3); cursor: not-allowed; background: none; }
|
|
55
|
+
.social-action .icon { width: 20px; height: 20px; }
|
|
56
|
+
.social-action .icon--repost { transform: scale(.85); }
|
|
57
|
+
.social-action .reaction-badge { width: 20px; height: 20px; }
|
|
58
|
+
.social-action[data-reaction="LIKE"] { color: #378fe9; }
|
|
59
|
+
.social-action[data-reaction="PRAISE"] { color: #44712e; }
|
|
60
|
+
.social-action[data-reaction="APPRECIATION"] { color: #715e86; }
|
|
61
|
+
.social-action[data-reaction="EMPATHY"] { color: #b24020; }
|
|
62
|
+
.social-action[data-reaction="INTEREST"] { color: #915907; }
|
|
63
|
+
.social-action[data-reaction="ENTERTAINMENT"] { color: #1a707e; }
|
|
64
|
+
.social-action[data-reaction="LIKE"] .icon--like { transform: scaleX(-1) rotate(0deg); }
|
|
65
|
+
|
|
66
|
+
.reaction-picker { position: absolute; bottom: 46px; left: 0; display: flex; gap: 2px; padding: 4px 6px; background: #fff; border-radius: 24px; box-shadow: 0 0 0 1px var(--hairline), 0 4px 12px rgba(0, 0, 0, .15); z-index: 10; animation: pop .12s ease-out; }
|
|
67
|
+
.reaction-picker__item { position: relative; display: flex; padding: 2px; border: 0; background: none; border-radius: 50%; cursor: pointer; transition: transform .1s; }
|
|
68
|
+
.reaction-picker__item:hover, .reaction-picker__item:focus-visible { transform: translateY(-6px) scale(1.25); }
|
|
69
|
+
.reaction-picker__item .reaction-badge { width: 36px; height: 36px; }
|
|
70
|
+
.reaction-picker__tip { position: absolute; bottom: 48px; left: 50%; transform: translateX(-50%); padding: 2px 8px; border-radius: 12px; background: #1d2226; color: #fff; font-size: 12px; font-weight: 600; white-space: nowrap; opacity: 0; pointer-events: none; }
|
|
71
|
+
.reaction-picker__item:hover .reaction-picker__tip, .reaction-picker__item:focus-visible .reaction-picker__tip { opacity: 1; }
|
|
72
|
+
@keyframes pop { from { transform: translateY(6px); opacity: 0; } to { transform: none; opacity: 1; } }
|
|
73
|
+
|
|
74
|
+
.dropdown { position: absolute; top: 36px; min-width: 264px; padding: 8px 0; background: #fff; border-radius: 8px 0 8px 8px; box-shadow: 0 0 0 1px var(--hairline), 0 4px 12px rgba(0, 0, 0, .15); z-index: 15; }
|
|
75
|
+
.dropdown--right { right: 0; }
|
|
76
|
+
.dropdown--left { left: 0; border-radius: 0 8px 8px 8px; }
|
|
77
|
+
.dropdown__item { display: flex; align-items: flex-start; gap: 12px; width: 100%; padding: 12px 16px; border: 0; background: none; text-align: left; color: var(--text-2); cursor: pointer; }
|
|
78
|
+
.dropdown__item:hover, .dropdown__item:focus-visible { background: var(--hover); color: var(--text); outline: none; }
|
|
79
|
+
.dropdown__item:disabled { opacity: .5; cursor: not-allowed; }
|
|
80
|
+
.dropdown__text { display: flex; flex-direction: column; }
|
|
81
|
+
.dropdown__label { font-size: 14px; font-weight: 600; color: var(--text); }
|
|
82
|
+
.dropdown__sub { font-size: 12px; color: var(--text-2); }
|