@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.
Files changed (111) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +189 -0
  3. package/firedrill/agent.target.json +16 -0
  4. package/firedrill/baseline.scenario.json +1834 -0
  5. package/firedrill/comment-throttled.scenario.json +11 -0
  6. package/firedrill/conformance.suite.json +27 -0
  7. package/firedrill/linkedin-comment-throttled.drill.json +51 -0
  8. package/firedrill/linkedin-comments-connection.drill.json +43 -0
  9. package/firedrill/linkedin-comments.drill.json +196 -0
  10. package/firedrill/linkedin-denied.drill.json +383 -0
  11. package/firedrill/linkedin-fresh-install.drill.json +43 -0
  12. package/firedrill/linkedin-identity.drill.json +73 -0
  13. package/firedrill/linkedin-invalid-token.drill.json +393 -0
  14. package/firedrill/linkedin-limited-scopes.drill.json +88 -0
  15. package/firedrill/linkedin-openid-only.drill.json +383 -0
  16. package/firedrill/linkedin-orgs-connection.drill.json +28 -0
  17. package/firedrill/linkedin-orgs-network.drill.json +193 -0
  18. package/firedrill/linkedin-post-lost-response.drill.json +61 -0
  19. package/firedrill/linkedin-posts-lifecycle.drill.json +228 -0
  20. package/firedrill/linkedin-reactions-metadata.drill.json +196 -0
  21. package/firedrill/linkedin-reactions-outage.drill.json +66 -0
  22. package/firedrill/linkedin-tight-limits.drill.json +228 -0
  23. package/firedrill/linkedin-ugc-legacy.drill.json +43 -0
  24. package/firedrill/linkedin-visibility-connection.drill.json +73 -0
  25. package/firedrill/linkedin-visibility-stranger.drill.json +88 -0
  26. package/firedrill/linkedin-wire-errors.drill.json +353 -0
  27. package/firedrill/post-lost-response.scenario.json +11 -0
  28. package/firedrill/reactions-outage.scenario.json +11 -0
  29. package/firedrill/tight-limits.scenario.json +16 -0
  30. package/firedrill/tools/linkedin/app/assets/ATTRIBUTION.md +28 -0
  31. package/firedrill/tools/linkedin/app/assets/fonts/OFL.txt +93 -0
  32. package/firedrill/tools/linkedin/app/assets/linkedin-in.svg +1 -0
  33. package/firedrill/tools/linkedin/app/assets/linkedin-wordmark.svg +1 -0
  34. package/firedrill/tools/linkedin/app/assets/linkedin.svg +1 -0
  35. package/firedrill/tools/linkedin/app/site/app.js +80 -0
  36. package/firedrill/tools/linkedin/app/site/assets/fonts/fira-sans-latin-400-normal.woff2 +0 -0
  37. package/firedrill/tools/linkedin/app/site/assets/fonts/fira-sans-latin-600-normal.woff2 +0 -0
  38. package/firedrill/tools/linkedin/app/site/assets/fonts/fira-sans-latin-700-normal.woff2 +0 -0
  39. package/firedrill/tools/linkedin/app/site/assets/fonts/fira-sans-latin-ext-400-normal.woff2 +0 -0
  40. package/firedrill/tools/linkedin/app/site/assets/fonts/fira-sans-latin-ext-600-normal.woff2 +0 -0
  41. package/firedrill/tools/linkedin/app/site/assets/fonts/fira-sans-latin-ext-700-normal.woff2 +0 -0
  42. package/firedrill/tools/linkedin/app/site/assets/linkedin-wordmark.svg +1 -0
  43. package/firedrill/tools/linkedin/app/site/assets/linkedin.svg +1 -0
  44. package/firedrill/tools/linkedin/app/site/base.css +87 -0
  45. package/firedrill/tools/linkedin/app/site/comment-box.js +119 -0
  46. package/firedrill/tools/linkedin/app/site/comments.css +59 -0
  47. package/firedrill/tools/linkedin/app/site/comments.js +188 -0
  48. package/firedrill/tools/linkedin/app/site/company.js +115 -0
  49. package/firedrill/tools/linkedin/app/site/composer.js +112 -0
  50. package/firedrill/tools/linkedin/app/site/feed.css +105 -0
  51. package/firedrill/tools/linkedin/app/site/feed.js +134 -0
  52. package/firedrill/tools/linkedin/app/site/icons.js +70 -0
  53. package/firedrill/tools/linkedin/app/site/index.html +58 -0
  54. package/firedrill/tools/linkedin/app/site/nav.css +51 -0
  55. package/firedrill/tools/linkedin/app/site/network.js +77 -0
  56. package/firedrill/tools/linkedin/app/site/overlay.css +62 -0
  57. package/firedrill/tools/linkedin/app/site/overlay.js +108 -0
  58. package/firedrill/tools/linkedin/app/site/pages.css +78 -0
  59. package/firedrill/tools/linkedin/app/site/post-actions.js +83 -0
  60. package/firedrill/tools/linkedin/app/site/post-menu.js +72 -0
  61. package/firedrill/tools/linkedin/app/site/post.css +82 -0
  62. package/firedrill/tools/linkedin/app/site/post.js +131 -0
  63. package/firedrill/tools/linkedin/app/site/profile.js +139 -0
  64. package/firedrill/tools/linkedin/app/site/reactions.js +46 -0
  65. package/firedrill/tools/linkedin/app/site/reactors.js +71 -0
  66. package/firedrill/tools/linkedin/app/site/shell.js +107 -0
  67. package/firedrill/tools/linkedin/app/site/store.js +62 -0
  68. package/firedrill/tools/linkedin/app/site/text.js +80 -0
  69. package/firedrill/tools/linkedin/app/site/ui.js +142 -0
  70. package/firedrill/tools/linkedin/behavior.mjs +66 -0
  71. package/firedrill/tools/linkedin/lib/access.mjs +69 -0
  72. package/firedrill/tools/linkedin/lib/auth.mjs +115 -0
  73. package/firedrill/tools/linkedin/lib/cascade.mjs +28 -0
  74. package/firedrill/tools/linkedin/lib/errors.mjs +50 -0
  75. package/firedrill/tools/linkedin/lib/message.mjs +43 -0
  76. package/firedrill/tools/linkedin/lib/paging.mjs +54 -0
  77. package/firedrill/tools/linkedin/lib/postinput.mjs +47 -0
  78. package/firedrill/tools/linkedin/lib/render.mjs +95 -0
  79. package/firedrill/tools/linkedin/lib/routes.mjs +219 -0
  80. package/firedrill/tools/linkedin/lib/social.mjs +25 -0
  81. package/firedrill/tools/linkedin/lib/store.mjs +54 -0
  82. package/firedrill/tools/linkedin/lib/text.mjs +55 -0
  83. package/firedrill/tools/linkedin/lib/urn.mjs +93 -0
  84. package/firedrill/tools/linkedin/lib/wire.mjs +125 -0
  85. package/firedrill/tools/linkedin/linkedin.tool.json +8935 -0
  86. package/firedrill/tools/linkedin/ops/comments-read.mjs +57 -0
  87. package/firedrill/tools/linkedin/ops/comments-write.mjs +104 -0
  88. package/firedrill/tools/linkedin/ops/feed.mjs +74 -0
  89. package/firedrill/tools/linkedin/ops/network.mjs +46 -0
  90. package/firedrill/tools/linkedin/ops/orgs.mjs +93 -0
  91. package/firedrill/tools/linkedin/ops/posts-edit.mjs +71 -0
  92. package/firedrill/tools/linkedin/ops/posts-read.mjs +54 -0
  93. package/firedrill/tools/linkedin/ops/posts-write.mjs +74 -0
  94. package/firedrill/tools/linkedin/ops/profile.mjs +38 -0
  95. package/firedrill/tools/linkedin/ops/reactions.mjs +78 -0
  96. package/firedrill/tools/linkedin/ops/social.mjs +34 -0
  97. package/firedrill/world.json +2585 -0
  98. package/firedrill.json +5 -0
  99. package/package.json +52 -0
  100. package/starter.json +1833 -0
  101. package/test/client.mjs +71 -0
  102. package/test/conformance.mjs +20 -0
  103. package/test/fixtures.mjs +182 -0
  104. package/test/flows/comments.mjs +116 -0
  105. package/test/flows/every.mjs +38 -0
  106. package/test/flows/faults.mjs +56 -0
  107. package/test/flows/identity.mjs +70 -0
  108. package/test/flows/orgs.mjs +107 -0
  109. package/test/flows/posts.mjs +196 -0
  110. package/test/flows/reactions.mjs +101 -0
  111. package/test/flows/wire.mjs +62 -0
@@ -0,0 +1,112 @@
1
+ // "Create a post" modal: post-as selector, visibility, text with counter, link attachment, repost embed and edit mode.
2
+ import { el, call, newKey, describe, toast, avatar, busy } from "./ui.js";
3
+ import { icon } from "./icons.js";
4
+ import { openModal, closeModal, showMenu } from "./overlay.js";
5
+ import { session, actingPages } from "./store.js";
6
+ import { escapeLittleText, littleTextToPlain } from "./text.js";
7
+
8
+ const MAX = 3000;
9
+ const VIS = { PUBLIC: ["globe", "Anyone"], CONNECTIONS: ["people", "Connections only"] };
10
+ let draft = { text: "", visibility: "PUBLIC" };
11
+
12
+ /** options: { repostOf?: feed entry, edit?: feed entry, draftPost?: post (publish/edit a draft), onDone } */
13
+ export function openComposer({ repostOf = null, edit = null, onDone } = {}) {
14
+ const viewer = session.viewer;
15
+ if (!viewer) return;
16
+ const editing = Boolean(edit);
17
+ let author = editing ? edit.post.author : viewer.personUrn;
18
+ let visibility = editing ? edit.post.visibility : repostOf ? "PUBLIC" : draft.visibility;
19
+ const modal = openModal({ title: editing ? "Edit post" : "Create a post", className: "modal--composer", onClose: () => { if (!editing && !repostOf) draft = { text: textarea.value, visibility }; } });
20
+
21
+ const who = el("button", { class: "composer__who", attrs: { type: "button", "aria-haspopup": "menu", "aria-expanded": "false", disabled: editing ? true : undefined } });
22
+ const paintWho = () => {
23
+ const page = actingPages().find((p) => p.organizationUrn === author);
24
+ const name = page ? page.name : viewer.name;
25
+ const [visIcon, visLabel] = VIS[visibility] ?? VIS.PUBLIC;
26
+ who.replaceChildren(
27
+ avatar(name, author, 56, Boolean(page)),
28
+ el("span", { class: "composer__who-text" }, [
29
+ el("span", { class: "composer__who-name" }, [el("span", { text: name }), editing ? null : icon("caret-down")]),
30
+ el("span", { class: "composer__who-vis" }, [icon(visIcon), el("span", { text: `Post to ${visLabel}` })]),
31
+ ]),
32
+ );
33
+ who.setAttribute("aria-label", `Posting as ${name}, visible to ${visLabel}. Change`);
34
+ };
35
+ who.addEventListener("click", () => {
36
+ const choices = [{ urn: viewer.personUrn, name: viewer.name, sub: "Your profile" }, ...actingPages().map((p) => ({ urn: p.organizationUrn, name: p.name, sub: "Page you manage" }))];
37
+ showMenu(who, [
38
+ ...choices.map((c) => ({ label: `${c.urn === author ? "✓ " : ""}${c.name}`, sub: c.sub, onSelect: () => { author = c.urn; if (c.urn !== viewer.personUrn) visibility = "PUBLIC"; paintWho(); } })),
39
+ ...Object.entries(VIS).map(([key, [visIcon, label]]) => ({ icon: visIcon, label: `${key === visibility ? "✓ " : ""}${label}`, sub: key === "PUBLIC" ? "Anyone on or off LinkedIn" : "Connections on LinkedIn", disabled: key === "CONNECTIONS" && author !== viewer.personUrn, onSelect: () => { visibility = key; paintWho(); } })),
40
+ ], { align: "left" });
41
+ });
42
+ paintWho();
43
+
44
+ const textarea = el("textarea", { class: "composer__text", attrs: { "aria-label": "Text editor for creating content", placeholder: repostOf ? "Start writing or use @ to mention people" : "What do you want to talk about?", rows: "8" } });
45
+ textarea.value = editing ? littleTextToPlain(edit.post.commentary) : repostOf ? "" : draft.text;
46
+ const counter = el("span", { class: "composer__counter", attrs: { "aria-live": "polite" } });
47
+ const error = el("p", { class: "composer__error", attrs: { role: "alert" }, hidden: true });
48
+ error.hidden = true;
49
+
50
+ const link = { open: false };
51
+ const linkBox = el("div", { class: "composer__link", hidden: true });
52
+ linkBox.hidden = true;
53
+ const linkUrl = el("input", { class: "field__input", attrs: { id: "composer-link-url", type: "url", placeholder: "https://" } });
54
+ const linkTitle = el("input", { class: "field__input", attrs: { id: "composer-link-title", type: "text", maxlength: "400" } });
55
+ linkBox.append(
56
+ el("label", { class: "field__label", text: "Link URL", attrs: { for: "composer-link-url" } }), linkUrl,
57
+ el("label", { class: "field__label", text: "Headline", attrs: { for: "composer-link-title" } }), linkTitle,
58
+ );
59
+
60
+ const embed = repostOf ? el("div", { class: "composer__embed" }, [
61
+ el("div", { class: "composer__embed-head" }, [avatar(repostOf.actor.name, repostOf.actor.urn, 32, repostOf.actor.kind === "organization"), el("span", { class: "t-bold", text: repostOf.actor.name })]),
62
+ el("p", { class: "composer__embed-text", text: littleTextToPlain(repostOf.post.commentary) }),
63
+ ]) : null;
64
+
65
+ modal.heading.classList.add("visually-hidden");
66
+ modal.heading.before(who);
67
+ modal.body.append(...[textarea, embed, linkBox, error].filter(Boolean));
68
+ const tools = el("div", { class: "composer__tools" }, [
69
+ el("button", { class: "icon-btn", title: "Add a link", attrs: { type: "button", "aria-label": "Add a link", disabled: editing || repostOf ? true : undefined }, on: { click: () => { link.open = !link.open; linkBox.hidden = !link.open; if (link.open) linkUrl.focus(); } } }, icon("link")),
70
+ el("button", { class: "icon-btn", title: "Add media", attrs: { type: "button", "aria-label": "Add media", "data-not-simulated": "Photo and video uploads" } }, icon("photo")),
71
+ el("button", { class: "icon-btn", title: "Create an event", attrs: { type: "button", "aria-label": "Create an event", "data-not-simulated": "Events" } }, icon("calendar")),
72
+ el("button", { class: "icon-btn", title: "More", attrs: { type: "button", "aria-label": "More", "data-not-simulated": "Polls, documents and other post types" } }, icon("plus")),
73
+ ]);
74
+ const submit = el("button", { class: "btn btn--primary btn--sm composer__submit", text: editing ? "Save" : "Post", attrs: { type: "button" } });
75
+ modal.footer.append(tools, el("div", { class: "composer__submit-row" }, [counter, submit]));
76
+
77
+ const validate = () => {
78
+ const length = [...textarea.value].length;
79
+ counter.textContent = length > MAX - 200 ? `${MAX - length}` : "";
80
+ counter.classList.toggle("composer__counter--over", length > MAX);
81
+ const empty = textarea.value.trim() === "" && !repostOf && !(link.open && linkUrl.value.trim());
82
+ submit.disabled = empty || length > MAX;
83
+ };
84
+ textarea.addEventListener("input", () => { error.hidden = true; validate(); });
85
+ linkUrl.addEventListener("input", validate);
86
+ validate();
87
+
88
+ const key = newKey(); // reused if the same submission is retried after an uncertain failure
89
+ submit.addEventListener("click", () => busy(submit, async () => {
90
+ error.hidden = true;
91
+ const commentary = escapeLittleText(textarea.value.trim());
92
+ try {
93
+ if (editing) {
94
+ await call("posts.update", { postUrn: edit.post.id, commentary }, key);
95
+ } else {
96
+ const args = { author, commentary, visibility, distribution: { feedDistribution: "MAIN_FEED", targetEntities: [], thirdPartyDistributionChannels: [] }, lifecycleState: "PUBLISHED", isReshareDisabledByAuthor: false };
97
+ if (repostOf) args.reshareContext = { parent: repostOf.post.id };
98
+ if (link.open && linkUrl.value.trim()) args.content = { article: { source: linkUrl.value.trim(), title: linkTitle.value.trim() || linkUrl.value.trim() } };
99
+ await call("posts.create", args, key);
100
+ if (!repostOf) draft = { text: "", visibility: "PUBLIC" };
101
+ }
102
+ closeModal();
103
+ toast(editing ? "Post updated." : repostOf ? "Repost successful." : "Post successful.");
104
+ await onDone?.();
105
+ } catch (failure) {
106
+ error.hidden = false;
107
+ error.textContent = failure?.code === "INTERNAL_SERVER_ERROR" && !editing
108
+ ? "Something went wrong. Check your recent activity before posting again — your post may have been published."
109
+ : describe(failure);
110
+ }
111
+ }));
112
+ }
@@ -0,0 +1,105 @@
1
+ .home { display: grid; grid-template-columns: 225px minmax(0, 555px) 300px; gap: 24px; justify-content: center; align-items: start; }
2
+ .rail-left, .rail-right { position: sticky; top: 76px; }
3
+
4
+ /* Identity card */
5
+ .identity { overflow: hidden; }
6
+ .identity__cover { height: 58px; background: linear-gradient(135deg, #a0b4b7 0%, #c7d2d4 55%, #dfe6e7 100%); }
7
+ .identity__main { padding: 0 12px 16px; text-align: left; }
8
+ .identity__avatar { display: inline-block; margin-top: -38px; border-radius: 50%; }
9
+ .identity__name { display: block; margin-top: 8px; font-size: 18px; line-height: 1.33; font-weight: 600; color: var(--text); }
10
+ .identity__name:hover { text-decoration: underline; color: var(--text); }
11
+ .identity__headline { margin-top: 4px; font-size: 12px; line-height: 1.33; color: var(--text); }
12
+ .identity__location { margin-top: 4px; font-size: 12px; color: var(--text-2); }
13
+ .identity__page { display: flex; align-items: center; gap: 8px; margin-top: 12px; font-size: 12px; font-weight: 600; color: var(--text); }
14
+ .identity__page:hover { text-decoration: none; }
15
+ .identity__page .avatar { width: 20px; height: 20px; font-size: 10px; }
16
+ .identity__rows { border-top: 1px solid var(--divider); padding: 12px 0; }
17
+ .identity__row { display: flex; justify-content: space-between; align-items: flex-start; width: 100%; padding: 4px 12px; border: 0; background: none; text-align: left; font-size: 12px; font-weight: 600; color: var(--text-2); cursor: pointer; }
18
+ .identity__row:hover { background: var(--hover); text-decoration: none; }
19
+ .identity__row-sub { display: block; font-weight: 600; color: var(--text); }
20
+ .identity__row-value { color: var(--blue); }
21
+ .identity__premium { display: block; width: 100%; padding: 12px; border: 0; border-top: 1px solid var(--divider); background: none; text-align: left; font-size: 12px; color: var(--text-2); cursor: pointer; }
22
+ .identity__premium strong { display: flex; align-items: center; gap: 4px; margin-top: 2px; color: var(--text); }
23
+ .identity__premium:hover { background: var(--hover); }
24
+ .identity__premium-mark { display: inline-block; width: 14px; height: 14px; border-radius: 2px; background: linear-gradient(135deg, #f8c77e, #e7a33e); }
25
+ .shortcuts { padding: 8px 0; }
26
+ .shortcuts__item { display: flex; align-items: center; gap: 8px; width: 100%; padding: 8px 12px; border: 0; background: none; font-size: 12px; font-weight: 600; color: var(--text); cursor: pointer; }
27
+ .shortcuts__item:hover { background: var(--hover); }
28
+
29
+ /* Share box */
30
+ .share-box { padding: 8px 16px 4px; }
31
+ .share-box__top { display: flex; gap: 8px; align-items: center; }
32
+ .share-box__trigger { flex: 1; height: 48px; padding: 10px 16px; border: 0; border-radius: 35px; box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.6); background: #fff; text-align: left; font-size: 14px; font-weight: 600; color: var(--text-2); cursor: pointer; }
33
+ .share-box__trigger:hover { background: var(--hover); }
34
+ .share-box__actions { display: flex; justify-content: space-between; margin-top: 4px; }
35
+ .share-box__action { display: inline-flex; align-items: center; gap: 8px; padding: 12px 8px; border: 0; border-radius: 4px; background: none; font-size: 14px; font-weight: 600; color: var(--text-2); cursor: pointer; }
36
+ .share-box__action:hover { background: var(--hover); }
37
+ .share-box__action--video .icon { color: #5f9b41; }
38
+ .share-box__action--photo .icon { color: #378fe9; }
39
+ .share-box__action--article .icon { color: #e06847; }
40
+
41
+ .sort-bar { display: flex; align-items: center; gap: 8px; margin: 8px 0; font-size: 12px; color: var(--text-2); }
42
+ .sort-bar::before { content: ""; flex: 1; height: 1px; background: #d6d6d6; }
43
+ .sort-bar strong { color: var(--text); display: inline-flex; align-items: center; }
44
+ .sort-bar button { border: 0; background: none; font-size: 12px; color: var(--text-2); cursor: pointer; display: inline-flex; align-items: center; gap: 2px; }
45
+
46
+ .new-posts { position: sticky; top: 64px; z-index: 5; display: flex; justify-content: center; height: 0; }
47
+ .new-posts button { display: inline-flex; align-items: center; gap: 4px; height: 32px; padding: 0 16px; border: 0; border-radius: 16px; background: var(--blue); color: #fff; font-weight: 600; box-shadow: 0 4px 12px rgba(0, 0, 0, .2); cursor: pointer; }
48
+ .new-posts button .icon { width: 16px; height: 16px; }
49
+
50
+ .feed-list > * + * { margin-top: 8px; }
51
+ .feed-more { display: flex; justify-content: center; margin-top: 8px; }
52
+ .feed-more .btn { width: 100%; background: #fff; box-shadow: var(--shadow); border-radius: 8px; color: var(--text-2); font-size: 14px; }
53
+ .feed-more .btn:hover { background: #f3f3f3; color: var(--text); }
54
+ .feed-end { padding: 16px; text-align: center; color: var(--text-2); font-size: 14px; }
55
+
56
+ .state-card { padding: 32px 24px; text-align: center; }
57
+ .state-card__title { font-size: 20px; font-weight: 600; margin-bottom: 8px; }
58
+ .state-card__text { color: var(--text-2); margin-bottom: 16px; }
59
+ .state-card--error { border-left: 4px solid var(--danger); text-align: left; }
60
+
61
+ .skeleton { padding: 12px 16px 16px; }
62
+ .skeleton__row { display: flex; gap: 8px; margin-bottom: 12px; }
63
+ .skeleton__circle { width: 48px; height: 48px; border-radius: 50%; }
64
+ .skeleton__line { height: 12px; border-radius: 6px; margin: 6px 0; }
65
+ .skeleton__circle, .skeleton__line { background: linear-gradient(90deg, #eeebe6 25%, #f7f5f2 50%, #eeebe6 75%); background-size: 400% 100%; animation: shimmer 1.4s ease infinite; }
66
+ @keyframes shimmer { from { background-position: 100% 0; } to { background-position: 0 0; } }
67
+
68
+ /* Right rail */
69
+ .news { padding: 12px 0 8px; }
70
+ .news__head { display: flex; justify-content: space-between; align-items: center; padding: 0 12px 0 16px; }
71
+ .news__title { font-size: 16px; font-weight: 600; }
72
+ .news__sub { padding: 4px 16px 0; font-size: 14px; font-weight: 600; color: var(--text-2); }
73
+ .news__empty { padding: 8px 16px; font-size: 12px; color: var(--text-2); }
74
+ .follow { padding: 12px 0 8px; }
75
+ .follow__item { display: flex; gap: 8px; padding: 8px 16px; }
76
+ .follow__name { display: block; font-weight: 600; color: var(--text); }
77
+ .follow__sub { font-size: 12px; color: var(--text-2); }
78
+ .follow__item .btn { margin-top: 8px; }
79
+ .rail-footer { padding: 16px 8px; text-align: center; font-size: 12px; color: var(--text-2); }
80
+ .rail-footer__links { display: flex; flex-wrap: wrap; justify-content: center; gap: 4px 12px; }
81
+ .rail-footer__links button { border: 0; background: none; padding: 0; font-size: 12px; color: var(--text-2); cursor: pointer; }
82
+ .rail-footer__links button:hover { color: var(--blue); text-decoration: underline; }
83
+ .rail-footer__brand { display: flex; align-items: center; justify-content: center; gap: 6px; margin-top: 8px; color: var(--text); }
84
+ .rail-footer__brand img { height: 14px; width: auto; }
85
+
86
+ @media (max-width: 1199px) { .home { grid-template-columns: 225px minmax(0, 1fr) 300px; } }
87
+ @media (max-width: 991px) { .home { grid-template-columns: 225px minmax(0, 1fr); } .rail-right { display: none; } }
88
+ @media (max-width: 767px) {
89
+ .home { grid-template-columns: minmax(0, 1fr); }
90
+ .home > * { min-width: 0; max-width: 100%; }
91
+ .rail-left { position: static; }
92
+ .app-main { padding: 12px 12px 48px; }
93
+ .global-nav { padding: 0 8px; }
94
+ .global-nav__logo { margin-right: 2px; }
95
+ .search { flex: 1 1 0; min-width: 72px; margin-right: 4px; }
96
+ .search:focus-within { flex-basis: 0; }
97
+ .search__input { padding-left: 32px; }
98
+ .search__icon { left: 8px; }
99
+ .typeahead { width: min(380px, calc(100vw - 16px)); }
100
+ .global-nav__primary { flex: none; margin-left: 0; }
101
+ .nav-item { min-width: 40px; padding: 0 2px; }
102
+ .nav-item__label, .nav-premium, .nav-divider, .global-nav__items > li:has(> [data-not-simulated="For Business"]) { display: none; }
103
+ }
104
+ .skeleton__grow { flex: 1; }
105
+ .skeleton__line--w40 { width: 40%; } .skeleton__line--w60 { width: 60%; } .skeleton__line--w70 { width: 70%; } .skeleton__line--w85 { width: 85%; }
@@ -0,0 +1,134 @@
1
+ // Home: identity card and shortcuts (left), share box + paged feed (centre), pages and footer (right).
2
+ import { el, call, describe, avatar, setServerNow, busy } from "./ui.js";
3
+ import { icon } from "./icons.js";
4
+ import { session } from "./store.js";
5
+ import { renderPostCard } from "./post.js";
6
+ import { openComposer } from "./composer.js";
7
+ import { modalOpen } from "./overlay.js";
8
+
9
+ const PAGE = 10;
10
+
11
+ function skeleton() {
12
+ return el("div", { class: "card skeleton", attrs: { "aria-hidden": "true" } }, [
13
+ el("div", { class: "skeleton__row" }, [el("div", { class: "skeleton__circle" }), el("div", { class: "skeleton__grow" }, [el("div", { class: "skeleton__line skeleton__line--w40" }), el("div", { class: "skeleton__line skeleton__line--w60" })])]),
14
+ el("div", { class: "skeleton__line" }), el("div", { class: "skeleton__line skeleton__line--w85" }), el("div", { class: "skeleton__line skeleton__line--w70" }),
15
+ ]);
16
+ }
17
+
18
+ export function identityCard() {
19
+ const v = session.viewer;
20
+ const href = `#/in/${encodeURIComponent(v.personUrn.split(":").pop())}`;
21
+ const pages = v.pages.slice(0, 1);
22
+ return el("div", { class: "card identity" }, [
23
+ el("div", { class: "identity__cover", attrs: { "aria-hidden": "true" } }),
24
+ el("div", { class: "identity__main" }, [
25
+ el("a", { class: "identity__avatar", attrs: { href, "aria-label": v.name, tabindex: "-1" } }, avatar(v.name, v.personUrn, 72)),
26
+ el("a", { class: "identity__name", text: v.name, attrs: { href } }),
27
+ v.headline ? el("p", { class: "identity__headline", text: v.headline }) : null,
28
+ ...pages.map((p) => el("a", { class: "identity__page", attrs: { href: `#/company/${encodeURIComponent(p.vanityName)}` } }, [avatar(p.name, p.organizationUrn, 24, true), el("span", { text: p.name })])),
29
+ ]),
30
+ el("div", { class: "identity__rows" }, [
31
+ el("a", { class: "identity__row", attrs: { href: "#/mynetwork" } }, [el("span", {}, [el("span", { text: "Connections" }), el("span", { class: "identity__row-sub", text: "Grow your network" })]), el("span", { class: "identity__row-value", text: String(v.connectionCount) })]),
32
+ el("button", { class: "identity__row", attrs: { type: "button", "data-not-simulated": "Profile viewers" } }, el("span", { text: "Profile viewers" })),
33
+ el("button", { class: "identity__row", attrs: { type: "button", "data-not-simulated": "Post impressions" } }, el("span", { text: "Post impressions" })),
34
+ ]),
35
+ el("button", { class: "identity__premium", attrs: { type: "button", "data-not-simulated": "Premium" } }, [el("span", { text: "Access exclusive tools & insights" }), el("strong", {}, [el("span", { class: "identity__premium-mark", attrs: { "aria-hidden": "true" } }), el("span", { text: "Try Premium for $0" })])]),
36
+ el("button", { class: "identity__premium", attrs: { type: "button", "data-not-simulated": "Saved items" } }, el("strong", {}, [icon("bookmark"), el("span", { text: "Saved items" })])),
37
+ ]);
38
+ }
39
+
40
+ function shortcuts() {
41
+ return el("div", { class: "card shortcuts" }, [["group", "Groups"], ["newsletter", "Newsletters"], ["calendar", "Events"]].map(([glyph, label]) =>
42
+ el("button", { class: "shortcuts__item", attrs: { type: "button", "data-not-simulated": label } }, [icon(glyph), el("span", { text: label })])));
43
+ }
44
+
45
+ function rightRail() {
46
+ const pages = session.viewer.pages;
47
+ return el("aside", { class: "rail-right" }, [
48
+ el("div", { class: "card news" }, [
49
+ el("div", { class: "news__head" }, [el("h2", { class: "news__title", text: "LinkedIn News" }), el("button", { class: "icon-btn icon-btn--sm", title: "About news", attrs: { type: "button", "aria-label": "About LinkedIn News", "data-not-simulated": "LinkedIn News" } }, icon("info"))]),
50
+ el("p", { class: "news__sub", text: "Top stories" }),
51
+ el("p", { class: "news__empty", text: "News stories aren't simulated in this environment." }),
52
+ ]),
53
+ pages.length ? el("div", { class: "card follow" }, [
54
+ el("div", { class: "news__head" }, el("h2", { class: "news__title", text: "Your pages" })),
55
+ ...pages.map((p) => el("div", { class: "follow__item" }, [avatar(p.name, p.organizationUrn, 48, true), el("div", {}, [
56
+ el("a", { class: "follow__name", text: p.name, attrs: { href: `#/company/${encodeURIComponent(p.vanityName)}` } }),
57
+ el("span", { class: "follow__sub", text: p.roles.map((r) => r.charAt(0) + r.slice(1).toLowerCase().replaceAll("_", " ")).join(", ") }),
58
+ el("div", {}, el("a", { class: "btn btn--muted btn--sm", text: "View page", attrs: { href: `#/company/${encodeURIComponent(p.vanityName)}` } })),
59
+ ])])),
60
+ ]) : null,
61
+ footer(),
62
+ ]);
63
+ }
64
+
65
+ export function footer() {
66
+ const links = ["About", "Accessibility", "Help Center", "Privacy & Terms", "Ad Choices", "Advertising", "Business Services", "Get the LinkedIn app", "More"];
67
+ return el("footer", { class: "rail-footer" }, [
68
+ el("div", { class: "rail-footer__links" }, links.map((label) => el("button", { text: label, attrs: { type: "button", "data-not-simulated": label } }))),
69
+ el("div", { class: "rail-footer__brand" }, [el("img", { attrs: { src: "./assets/linkedin-wordmark.svg", alt: "LinkedIn", width: "56", height: "14" } }), el("span", { text: "Simulated in a Firedrill world" })]),
70
+ ]);
71
+ }
72
+
73
+ export function renderHome(main) {
74
+ const v = session.viewer;
75
+ const list = el("div", { class: "feed-list", attrs: { "aria-busy": "true" } }, [skeleton(), skeleton()]);
76
+ const more = el("div", { class: "feed-more" });
77
+ const newPosts = el("div", { class: "new-posts" });
78
+ newPosts.hidden = true;
79
+ let loaded = 0;
80
+ let total = 0;
81
+
82
+ const load = async ({ reset }) => {
83
+ const start = reset ? 0 : loaded;
84
+ const count = reset ? Math.max(PAGE, loaded) : PAGE;
85
+ try {
86
+ const page = await call("feed.list", { start, count });
87
+ setServerNow(page.serverTimeMs);
88
+ session.viewer = page.viewer;
89
+ if (reset) { list.replaceChildren(); loaded = 0; }
90
+ for (const entry of page.elements) list.append(renderPostCard(entry, { onReload: () => load({ reset: true }) }));
91
+ loaded = start + page.elements.length;
92
+ total = page.paging.total ?? loaded;
93
+ list.removeAttribute("aria-busy");
94
+ more.replaceChildren();
95
+ if (loaded === 0) list.append(el("div", { class: "card state-card" }, [el("h2", { class: "state-card__title", text: "Start a conversation" }), el("p", { class: "state-card__text", text: "Your feed is empty. Posts from you, your connections and pages you follow will show up here." }), el("button", { class: "btn btn--primary", text: "Start a post", attrs: { type: "button" }, on: { click: () => openComposer({ onDone: () => load({ reset: true }) }) } })]));
96
+ else if (loaded < total) more.append(el("button", { class: "btn", text: "Show more feed updates", attrs: { type: "button" }, on: { click: (event) => busy(event.currentTarget, () => load({ reset: false })) } }));
97
+ else more.append(el("p", { class: "feed-end", text: "You're all caught up." }));
98
+ } catch (error) {
99
+ list.removeAttribute("aria-busy");
100
+ if (reset) list.replaceChildren();
101
+ more.replaceChildren(el("div", { class: "card state-card state-card--error", attrs: { role: "alert" } }, [el("h2", { class: "state-card__title", text: "Couldn't load your feed" }), el("p", { class: "state-card__text", text: describe(error) }), el("button", { class: "btn btn--secondary", text: "Retry", attrs: { type: "button" }, on: { click: () => load({ reset }) } })]));
102
+ }
103
+ };
104
+
105
+ const shareBox = el("div", { class: "card share-box" }, [
106
+ el("div", { class: "share-box__top" }, [
107
+ el("a", { attrs: { href: `#/in/${encodeURIComponent(v.personUrn.split(":").pop())}`, "aria-label": "View your profile", tabindex: "-1" } }, avatar(v.name, v.personUrn, 48)),
108
+ el("button", { class: "share-box__trigger", text: "Start a post", attrs: { type: "button" }, on: { click: () => openComposer({ onDone: () => load({ reset: true }) }) } }),
109
+ ]),
110
+ el("div", { class: "share-box__actions" }, [
111
+ el("button", { class: "share-box__action share-box__action--video", attrs: { type: "button", "data-not-simulated": "Video posts" } }, [icon("video"), el("span", { text: "Video" })]),
112
+ el("button", { class: "share-box__action share-box__action--photo", attrs: { type: "button", "data-not-simulated": "Photo posts" } }, [icon("photo"), el("span", { text: "Photo" })]),
113
+ el("button", { class: "share-box__action share-box__action--article", attrs: { type: "button", "data-not-simulated": "Articles" } }, [icon("article"), el("span", { text: "Write article" })]),
114
+ ]),
115
+ ]);
116
+ const sortBar = el("div", { class: "sort-bar" }, [el("span", { text: "Sort by:" }), el("button", { attrs: { type: "button", "aria-label": "Sort feed: Recent", "data-not-simulated": "Feed sort options" } }, [el("strong", { text: "Recent" }), icon("caret-down")])]);
117
+ newPosts.append(el("button", { attrs: { type: "button" }, on: { click: () => { newPosts.hidden = true; window.scrollTo({ top: 0 }); void load({ reset: true }); } } }, [icon("arrow-up"), el("span", { text: "New posts" })]));
118
+
119
+ main.replaceChildren(el("div", { class: "home" }, [
120
+ el("aside", { class: "rail-left" }, [identityCard(), shortcuts()]),
121
+ el("div", { class: "feed-main" }, [shareBox, sortBar, newPosts, list, more]),
122
+ rightRail(),
123
+ ]));
124
+ void load({ reset: true });
125
+
126
+ return {
127
+ /** World changed elsewhere: reload unless the reader has unsent work, in which case offer "New posts". */
128
+ refresh: () => {
129
+ const typing = [...main.querySelectorAll(".comment-box__input")].some((input) => input.value.trim()) || main.querySelector(".comment-box .btn[data-busy='true']") || modalOpen();
130
+ if (typing) newPosts.hidden = false; else return load({ reset: true });
131
+ return undefined;
132
+ },
133
+ };
134
+ }
@@ -0,0 +1,70 @@
1
+ // Monochrome 24px interface glyphs (original paths) and the six coloured reaction badges.
2
+ const NS = "http://www.w3.org/2000/svg";
3
+
4
+ const PATHS = {
5
+ "nav-home": "M23 9v2h-2v7a3 3 0 01-3 3h-4v-6h-4v6H6a3 3 0 01-3-3v-7H1V9l11-7 5 3.18V2h3v5.09z",
6
+ "nav-network": "M12 16v6H3v-6a3 3 0 013-3h3a3 3 0 013 3zm5.5-3A3.5 3.5 0 1014 9.5a3.5 3.5 0 003.5 3.5zm1 2h-2a2.5 2.5 0 00-2.5 2.5V22h7v-4.5a2.5 2.5 0 00-2.5-2.5zM7.5 2A4.5 4.5 0 1012 6.5 4.49 4.49 0 007.5 2z",
7
+ "nav-jobs": "M17 6V5a3 3 0 00-3-3h-4a3 3 0 00-3 3v1H2v4a3 3 0 003 3h14a3 3 0 003-3V6zM9 5a1 1 0 011-1h4a1 1 0 011 1v1H9zm10 9a4 4 0 003-1.38V17a3 3 0 01-3 3H5a3 3 0 01-3-3v-4.38A4 4 0 005 14z",
8
+ "nav-messaging": "M16 4H8a7 7 0 000 14h4v4l8.16-5.39A6.78 6.78 0 0023 11a7 7 0 00-7-7zm-8 8.25A1.25 1.25 0 119.25 11 1.25 1.25 0 018 12.25zm4 0A1.25 1.25 0 1113.25 11 1.25 1.25 0 0112 12.25zm4 0A1.25 1.25 0 1117.25 11 1.25 1.25 0 0116 12.25z",
9
+ "nav-bell": "M22 19h-8.28a2 2 0 11-3.44 0H2v-1a4.52 4.52 0 011.17-2.83l1-1.17h15.7l1 1.17A4.42 4.42 0 0122 18zM18.21 7.44A6.27 6.27 0 0012 2a6.27 6.27 0 00-6.21 5.44L5 13h14z",
10
+ "nav-grid": "M3 3h4v4H3zm7 4h4V3h-4zm7-4v4h4V3zM3 14h4v-4H3zm7 0h4v-4h-4zm7 0h4v-4h-4zM3 21h4v-4H3zm7 0h4v-4h-4zm7 0h4v-4h-4z",
11
+ search: "M14.56 12.44L11.3 9.18a5.51 5.51 0 10-2.12 2.12l3.26 3.26a1.5 1.5 0 102.12-2.12zM3 6.5A3.5 3.5 0 116.5 10 3.5 3.5 0 013 6.5z",
12
+ "caret-down": "M8 11L3 6h10z",
13
+ like: "M19.46 11l-3.91-3.91a7 7 0 01-1.69-2.74l-.49-1.47A2.76 2.76 0 0010.76 1 2.75 2.75 0 008 3.74v1.12a9.19 9.19 0 00.46 2.85L8.89 9H4.12A2.12 2.12 0 002 11.12a2.16 2.16 0 00.92 1.76A2.11 2.11 0 002 14.62a2.14 2.14 0 001.28 2 2 2 0 00-.28 1 2.12 2.12 0 002 2.12v.14A2.12 2.12 0 007.12 22h7.49a8.08 8.08 0 003.58-.84l.31-.16H21V11zM19 19h-1l-.73.37a6.14 6.14 0 01-2.69.63H7.72a1 1 0 01-1-.72l-.25-.87-.85-.41A1 1 0 015 17l.17-1-.76-.74A1 1 0 014.27 14l.66-1.09-.73-1.1a.49.49 0 01.08-.7.48.48 0 01.34-.11h7.05l-1.31-3.92A7 7 0 0110 4.86V3.75a.77.77 0 01.75-.75.75.75 0 01.71.51L12 5a9 9 0 002.13 3.5l4.5 4.5H19z",
14
+ comment: "M7 9h10v1H7zm0 4h7v-1H7zm16-2a6.78 6.78 0 01-2.84 5.61L12 22v-4H8A7 7 0 018 4h8a7 7 0 017 7zm-2 0a5 5 0 00-5-5H8a5 5 0 000 10h6v2.28L19 15a4.79 4.79 0 002-4z",
15
+ repost: "M13.96 5H6c-.55 0-1 .45-1 1v10H3V6c0-1.66 1.34-3 3-3h7.96L12 0h2.37L17 4l-2.63 4H12l1.96-3zm5.54 3H19v10c0 .55-.45 1-1 1h-7.96L12 16H9.63L7 20l2.63 4H12l-1.96-3H18c1.66 0 3-1.34 3-3V8h-1.5z",
16
+ send: "M21 3L0 10l7.66 4.26L16 8l-6.26 8.34L14 24l7-21z",
17
+ ellipsis: "M14 12a2 2 0 11-2-2 2 2 0 012 2zM4 10a2 2 0 102 2 2 2 0 00-2-2zm16 0a2 2 0 102 2 2 2 0 00-2-2z",
18
+ close: "M13.42 12L20 18.58 18.58 20 12 13.42 5.42 20 4 18.58 10.58 12 4 5.42 5.42 4 12 10.58 18.58 4 20 5.42z",
19
+ "close-sm": "M14 3.41L9.41 8 14 12.59 12.59 14 8 9.41 3.41 14 2 12.59 6.59 8 2 3.41 3.41 2 8 6.59 12.59 2z",
20
+ globe: "M8 1a7 7 0 107 7 7 7 0 00-7-7zM3 8a5 5 0 011-3l.55.55A1.5 1.5 0 015 6.62v1.07a.75.75 0 00.22.53l.56.56a.75.75 0 00.53.22H7v.69a.75.75 0 00.22.53l.56.56a.75.75 0 01.22.53V13a5 5 0 01-5-5zm6.24 4.83l2-2.46a.75.75 0 00.09-.8l-.58-1.16A.76.76 0 0010 8H7v-.19a.51.51 0 01.28-.45l.38-.19a.74.74 0 01.68 0L9 7.5l.38-.7a1 1 0 00.12-.48v-.85a.78.78 0 01.21-.53l1.07-1.09a5 5 0 01-1.54 9z",
21
+ people: "M9.5 8A2.5 2.5 0 107 5.5 2.5 2.5 0 009.5 8zm0 1C7.57 9 6 10.12 6 11.5V13h7v-1.5C13 10.12 11.43 9 9.5 9zM4.5 7A1.5 1.5 0 103 5.5 1.5 1.5 0 004.5 7zM5 11.5a3.16 3.16 0 01.52-1.72A2.47 2.47 0 004.5 9.5C3.12 9.5 2 10.29 2 11.25V13h3z",
22
+ lock: "M12 7V6a4 4 0 00-8 0v1H3v8h10V7zM6 6a2 2 0 014 0v1H6z",
23
+ pencil: "M21.13 2.86a3 3 0 00-4.17 0l-13 13L2 22l6.19-2L21.13 7a3 3 0 000-4.16zM6.77 18.57l-1.35-1.34L16.64 6 18 7.35z",
24
+ trash: "M20 4v1H4V4a1 1 0 011-1h4a1 1 0 011-1h4a1 1 0 011 1h4a1 1 0 011 1zM5 6h14v13a3 3 0 01-3 3H8a3 3 0 01-3-3zm9 13h1V9h-1zm-5 0h1V9H9z",
25
+ "comment-off": "M2.41 1L1 2.41l3.13 3.13A7 7 0 008 18h4v4l5.3-3.5 4.29 4.29L23 21.41zM8 16a5 5 0 01-2.44-9.36L14.92 16zm15-5a6.83 6.83 0 01-2.23 5.13l-1.42-1.42A4.87 4.87 0 0021 11a5 5 0 00-5-5H9.24l-2-2H16a7 7 0 017 7z",
26
+ link: "M18 10.5a3.5 3.5 0 01-1 2.5l-3.5 3.5a3.5 3.5 0 01-5-5l.5-.5 1.4 1.4-.5.5a1.5 1.5 0 002.1 2.1l3.5-3.5a1.5 1.5 0 00-2.1-2.1l-.4.4L11.6 8l.4-.4a3.5 3.5 0 016 2.9zM12.4 16l-.4.4a1.5 1.5 0 01-2.1-2.1l3.5-3.5a1.5 1.5 0 012.1 0L17 9.4a3.5 3.5 0 00-5-.4L8.5 12.5a3.5 3.5 0 005 5l.4-.4z",
27
+ photo: "M19 4H5a3 3 0 00-3 3v10a3 3 0 003 3h14a3 3 0 003-3V7a3 3 0 00-3-3zm1 13a1 1 0 01-.29.71L16 14l-2 2-6-6-4 4V7a1 1 0 011-1h14a1 1 0 011 1zm-2-7a2 2 0 11-2-2 2 2 0 012 2z",
28
+ video: "M19 4H5a3 3 0 00-3 3v10a3 3 0 003 3h14a3 3 0 003-3V7a3 3 0 00-3-3zm-9 12V8l6 4z",
29
+ article: "M21 3v2H3V3zm-6 6h6V7h-6zm0 4h6v-2h-6zm0 4h6v-2h-6zM3 21h18v-2H3zM13 7H3v10h10z",
30
+ bookmark: "M13 4a3 3 0 00-3-3H3v14l5-4.5 5 4.5z",
31
+ group: "M8.5 7h-1A1.5 1.5 0 006 8.5V14h4V8.5A1.5 1.5 0 008.5 7zM12.75 8h-.5A1.25 1.25 0 0011 9.25V14h3V9.25A1.25 1.25 0 0012.75 8zM3.75 8h-.5A1.25 1.25 0 002 9.25V14h3V9.25A1.25 1.25 0 003.75 8zM8 6a2 2 0 10-2-2 2 2 0 002 2zm4.5 1A1.5 1.5 0 1011 5.5 1.5 1.5 0 0012.5 7zm-9 0A1.5 1.5 0 102 5.5 1.5 1.5 0 003.5 7z",
32
+ newsletter: "M13 13H3V3h10zM2 1a1 1 0 00-1 1v12a1 1 0 001 1h12a1 1 0 001-1V2a1 1 0 00-1-1zm3 4h6v1H5zm0 2.5h6v1H5zM5 10h4v1H5z",
33
+ calendar: "M2 2v9a3 3 0 003 3h6a3 3 0 003-3V2zm10 9a1 1 0 01-1 1H5a1 1 0 01-1-1V6h8zM7 8H5V7h2zm4 0H8V7h3zM7 11H5v-1h2zm4 0H8v-1h3z",
34
+ info: "M12 2a10 10 0 1010 10A10 10 0 0012 2zm1 15h-2v-6h2zm0-8h-2V7h2z",
35
+ "arrow-up": "M12 3l7 7-1.4 1.4-4.6-4.6V21h-2V6.8L6.4 11.4 5 10z",
36
+ plus: "M21 13h-8v8h-2v-8H3v-2h8V3h2v8h8z",
37
+ emoji: "M8 1a7 7 0 107 7 7 7 0 00-7-7zm3 3.5A1.5 1.5 0 119.5 6 1.5 1.5 0 0111 4.5zm-6 0A1.5 1.5 0 113.5 6 1.5 1.5 0 015 4.5zM8 13a4.5 4.5 0 01-4.24-3h8.48A4.5 4.5 0 018 13z",
38
+ check: "M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z",
39
+ alert: "M12 2a10 10 0 1010 10A10 10 0 0012 2zm1 15h-2v-2h2zm0-4h-2V7h2z",
40
+ "back": "M20 11H7.83l5.59-5.59L12 4l-8 8 8 8 1.41-1.41L7.83 13H20z",
41
+ };
42
+ const SMALL = new Set(["caret-down", "close-sm", "globe", "people", "lock", "bookmark", "group", "newsletter", "calendar", "emoji"]);
43
+
44
+ /** An `<svg>` glyph filled with currentColor. */
45
+ export function icon(name, className) {
46
+ const svg = document.createElementNS(NS, "svg");
47
+ const size = SMALL.has(name) ? 16 : 24;
48
+ svg.setAttribute("viewBox", name === "search" ? "0 0 16 16" : `0 0 ${size} ${size}`);
49
+ svg.setAttribute("width", String(size));
50
+ svg.setAttribute("height", String(size));
51
+ svg.setAttribute("aria-hidden", "true");
52
+ svg.setAttribute("focusable", "false");
53
+ svg.setAttribute("class", `icon icon--${name}${className ? ` ${className}` : ""}`);
54
+ const path = document.createElementNS(NS, "path");
55
+ path.setAttribute("d", PATHS[name] ?? PATHS.info);
56
+ path.setAttribute("fill", "currentColor");
57
+ svg.append(path);
58
+ return svg;
59
+ }
60
+
61
+ /** Replace every `[data-icon]` placeholder under root with its glyph. */
62
+ export function hydrateIcons(root = document) {
63
+ for (const holder of root.querySelectorAll("[data-icon]")) {
64
+ const name = holder.getAttribute("data-icon");
65
+ holder.removeAttribute("data-icon");
66
+ holder.append(icon(name));
67
+ }
68
+ }
69
+
70
+ export { reactionBadge, REACTIONS } from "./reactions.js";
@@ -0,0 +1,58 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
6
+ <title>Feed | LinkedIn (synthetic)</title>
7
+ <link rel="icon" type="image/svg+xml" href="./assets/linkedin.svg" />
8
+ <link rel="stylesheet" href="./base.css" />
9
+ <link rel="stylesheet" href="./nav.css" />
10
+ <link rel="stylesheet" href="./feed.css" />
11
+ <link rel="stylesheet" href="./post.css" />
12
+ <link rel="stylesheet" href="./overlay.css" />
13
+ <link rel="stylesheet" href="./comments.css" />
14
+ <link rel="stylesheet" href="./pages.css" />
15
+ <script type="module" src="./app.js"></script>
16
+ </head>
17
+ <body>
18
+ <div id="boot" class="boot" role="status" aria-live="polite">
19
+ <img src="./assets/linkedin-wordmark.svg" alt="LinkedIn" width="168" height="42" />
20
+ <div class="boot-bar"><span></span></div>
21
+ </div>
22
+ <header class="global-nav" id="global-nav" hidden>
23
+ <div class="global-nav__content">
24
+ <a class="global-nav__logo" href="#/feed" aria-label="LinkedIn home" title="Home"><img src="./assets/linkedin.svg" alt="" width="34" height="34" /></a>
25
+ <div class="search" role="search">
26
+ <span class="search__icon" data-icon="search"></span>
27
+ <label class="visually-hidden" for="search-input">Search</label>
28
+ <input id="search-input" class="search__input" type="text" placeholder="Search" autocomplete="off" spellcheck="false" aria-expanded="false" aria-controls="search-results" />
29
+ <div class="typeahead" id="search-results" role="listbox" aria-label="Search suggestions" hidden></div>
30
+ </div>
31
+ <nav class="global-nav__primary" aria-label="Primary Navigation">
32
+ <ul class="global-nav__items">
33
+ <li><a class="nav-item" href="#/feed" data-nav="feed"><span data-icon="nav-home"></span><span class="nav-item__label">Home</span></a></li>
34
+ <li><a class="nav-item" href="#/mynetwork" data-nav="mynetwork"><span data-icon="nav-network"></span><span class="nav-item__label">My Network</span></a></li>
35
+ <li><button type="button" class="nav-item" data-not-simulated="Jobs"><span data-icon="nav-jobs"></span><span class="nav-item__label">Jobs</span></button></li>
36
+ <li><button type="button" class="nav-item" data-not-simulated="Messaging"><span data-icon="nav-messaging"></span><span class="nav-item__label">Messaging</span></button></li>
37
+ <li><button type="button" class="nav-item" data-not-simulated="Notifications"><span data-icon="nav-bell"></span><span class="nav-item__label">Notifications</span></button></li>
38
+ <li class="nav-me">
39
+ <button type="button" class="nav-item" id="me-button" aria-haspopup="menu" aria-expanded="false" data-nav="me"><span class="avatar avatar--24" id="me-avatar" aria-hidden="true"></span><span class="nav-item__label">Me <span data-icon="caret-down" class="caret"></span></span></button>
40
+ <div class="me-menu" id="me-menu" role="menu" hidden></div>
41
+ </li>
42
+ <li class="nav-divider" aria-hidden="true"></li>
43
+ <li><button type="button" class="nav-item" data-not-simulated="For Business"><span data-icon="nav-grid"></span><span class="nav-item__label">For Business <span data-icon="caret-down" class="caret"></span></span></button></li>
44
+ <li><button type="button" class="nav-premium" data-not-simulated="Premium">Try Premium for $0</button></li>
45
+ </ul>
46
+ </nav>
47
+ </div>
48
+ </header>
49
+ <main id="main" class="app-main" tabindex="-1" hidden></main>
50
+ <div id="overlay-root"></div>
51
+ <div class="toast" id="toast" role="status" aria-live="polite" hidden>
52
+ <span class="toast__icon" id="toast-icon"></span>
53
+ <span class="toast__text" id="toast-text"></span>
54
+ <button type="button" class="toast__action" id="toast-action" aria-label="Toast action" hidden></button>
55
+ <button type="button" class="icon-btn icon-btn--sm" id="toast-close" aria-label="Dismiss" title="Dismiss"><span data-icon="close-sm"></span></button>
56
+ </div>
57
+ </body>
58
+ </html>
@@ -0,0 +1,51 @@
1
+ .global-nav { position: sticky; top: 0; z-index: 20; height: 52px; background: #fff; border-bottom: 1px solid rgba(0, 0, 0, 0.08); padding: 0 24px; }
2
+ .global-nav__content { display: flex; align-items: center; max-width: 1128px; height: 100%; margin: 0 auto; }
3
+ .global-nav__logo { display: flex; flex: none; margin-right: 8px; border-radius: 4px; }
4
+ .global-nav__logo img { display: block; width: 34px; height: 34px; }
5
+ .search { position: relative; flex: 0 1 280px; margin-right: 12px; }
6
+ .search__icon { position: absolute; left: 10px; top: 50%; transform: translateY(-50%); color: rgba(0, 0, 0, 0.9); pointer-events: none; }
7
+ .search__icon .icon { width: 16px; height: 16px; }
8
+ .search__input { width: 100%; height: 34px; padding: 0 8px 0 40px; border: 0; border-radius: 4px; background: var(--search); font-size: 14px; box-shadow: none; transition: width .15s; }
9
+ .search__input::placeholder { color: rgba(0, 0, 0, 0.6); }
10
+ .search__input:focus { outline: none; box-shadow: inset 0 0 0 2px #000; }
11
+ .search:focus-within { flex-basis: 380px; }
12
+ .typeahead { position: absolute; top: 40px; left: 0; width: 380px; max-height: 480px; overflow-y: auto; background: #fff; border-radius: 0 0 8px 8px; box-shadow: 0 0 0 1px var(--hairline), 0 4px 12px rgba(0, 0, 0, 0.15); padding: 8px 0; }
13
+ .typeahead__heading { padding: 4px 16px; font-size: 12px; font-weight: 600; color: var(--text-2); }
14
+ .typeahead__item { display: flex; align-items: center; gap: 8px; width: 100%; padding: 6px 16px; border: 0; background: transparent; text-align: left; cursor: pointer; color: var(--text); }
15
+ .typeahead__item:hover, .typeahead__item[aria-selected="true"] { background: var(--hover); text-decoration: none; }
16
+ .typeahead__name { font-weight: 600; }
17
+ .typeahead__sub { color: var(--text-2); font-size: 12px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
18
+ .typeahead__empty { padding: 12px 16px; color: var(--text-2); }
19
+ .typeahead__footer { display: block; width: 100%; padding: 10px 16px 4px; border: 0; border-top: 1px solid var(--divider); margin-top: 6px; background: none; color: var(--blue); font-weight: 600; text-align: center; cursor: pointer; }
20
+
21
+ .global-nav__primary { margin-left: auto; height: 100%; }
22
+ .global-nav__items { display: flex; align-items: stretch; height: 100%; }
23
+ .global-nav__items > li { display: flex; position: relative; }
24
+ .nav-item { position: relative; display: flex; flex-direction: column; align-items: center; justify-content: center; min-width: 80px; height: 52px; padding: 0 4px; border: 0; background: none; color: rgba(0, 0, 0, 0.6); cursor: pointer; font-size: 12px; line-height: 1.33; text-decoration: none !important; }
25
+ .nav-item:hover { color: rgba(0, 0, 0, 0.9); }
26
+ .nav-item .icon { width: 24px; height: 24px; }
27
+ .nav-item__label { display: flex; align-items: center; font-weight: 400; white-space: nowrap; }
28
+ .nav-item .caret { display: inline-flex; margin-left: 1px; }
29
+ .nav-item .caret .icon { width: 16px; height: 16px; }
30
+ .nav-item[aria-current="page"] { color: rgba(0, 0, 0, 0.9); }
31
+ .nav-item[aria-current="page"]::after { content: ""; position: absolute; left: 0; right: 0; bottom: 0; height: 2px; background: rgba(0, 0, 0, 0.9); }
32
+ .nav-me .avatar { margin-bottom: 0; }
33
+ .nav-divider { width: 1px; margin: 0 8px; background: rgba(0, 0, 0, 0.08); }
34
+ .nav-premium { align-self: center; max-width: 90px; padding: 0 4px; border: 0; background: none; color: #915907; font-size: 12px; line-height: 1.33; text-align: center; text-decoration: underline; cursor: pointer; }
35
+ .nav-premium:hover { color: #5d3b09; }
36
+
37
+ .me-menu { position: absolute; top: 48px; right: 0; width: 264px; background: #fff; border-radius: 8px 0 8px 8px; box-shadow: 0 0 0 1px var(--hairline), 0 4px 12px rgba(0, 0, 0, 0.15); padding: 8px 0; z-index: 30; }
38
+ .me-menu__head { display: flex; gap: 8px; padding: 4px 12px 8px; }
39
+ .me-menu__name { font-size: 16px; font-weight: 600; }
40
+ .me-menu__headline { font-size: 14px; color: var(--text); }
41
+ .me-menu__view { display: block; margin: 4px 12px 8px; }
42
+ .me-menu__view .btn { width: 100%; }
43
+ .me-menu__section { border-top: 1px solid var(--divider); padding: 8px 0 4px; }
44
+ .me-menu__title { padding: 0 12px 4px; font-size: 16px; font-weight: 600; }
45
+ .me-menu__link { display: block; width: 100%; padding: 4px 12px; border: 0; background: none; text-align: left; color: var(--text-2); font-size: 14px; cursor: pointer; }
46
+ .me-menu__link:hover { text-decoration: underline; color: var(--text); }
47
+ .me-menu__link:disabled { cursor: not-allowed; opacity: .6; text-decoration: none; }
48
+
49
+ .app-main { max-width: 1176px; margin: 0 auto; padding: 24px 24px 48px; }
50
+ .app-main:focus, .app-main:focus-visible { outline: none; }
51
+ .typeahead__text { display: block; flex: 1; min-width: 0; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }