My agentic slop goes here. Not intended for anyone else!
1class BushelAuthor extends HTMLElement {
2 constructor() {
3 super();
4 this.attachShadow({ mode: 'open' });
5 }
6
7 static get observedAttributes() {
8 return [
9 'name', 'given-name', 'family-name', 'nickname',
10 'email', 'website', 'github', 'mastodon', 'twitter',
11 'photo', 'bio', 'org', 'job-title',
12 'display-mode', 'link-profile'
13 ];
14 }
15
16 connectedCallback() {
17 this.render();
18 }
19
20 attributeChangedCallback() {
21 this.render();
22 }
23
24 get displayMode() {
25 return this.getAttribute('display-mode') || 'inline';
26 }
27
28 get shouldLinkProfile() {
29 return this.hasAttribute('link-profile');
30 }
31
32 buildHCard() {
33 const name = this.getAttribute('name');
34 const givenName = this.getAttribute('given-name');
35 const familyName = this.getAttribute('family-name');
36 const nickname = this.getAttribute('nickname');
37 const email = this.getAttribute('email');
38 const website = this.getAttribute('website');
39 const github = this.getAttribute('github');
40 const mastodon = this.getAttribute('mastodon');
41 const twitter = this.getAttribute('twitter');
42 const photo = this.getAttribute('photo');
43 const bio = this.getAttribute('bio');
44 const org = this.getAttribute('org');
45 const jobTitle = this.getAttribute('job-title');
46
47 const displayName = name || `${givenName || ''} ${familyName || ''}`.trim() || nickname;
48
49 const hcard = document.createElement('div');
50 hcard.className = 'h-card bushel-author-card';
51
52 // Photo
53 if (photo) {
54 const img = document.createElement('img');
55 img.className = 'u-photo author-photo';
56 img.src = photo;
57 img.alt = `Photo of ${displayName}`;
58 img.loading = 'lazy';
59 hcard.appendChild(img);
60 }
61
62 const infoContainer = document.createElement('div');
63 infoContainer.className = 'author-info';
64
65 // Name (with optional link)
66 const nameElement = document.createElement(this.shouldLinkProfile && website ? 'a' : 'span');
67 nameElement.className = 'p-name author-name';
68 nameElement.textContent = displayName;
69
70 if (this.shouldLinkProfile && website) {
71 nameElement.href = website;
72 nameElement.className += ' u-url';
73 }
74
75 // Hidden structured name parts
76 if (givenName) {
77 const givenSpan = document.createElement('span');
78 givenSpan.className = 'p-given-name visually-hidden';
79 givenSpan.textContent = givenName;
80 nameElement.appendChild(givenSpan);
81 }
82
83 if (familyName) {
84 const familySpan = document.createElement('span');
85 familySpan.className = 'p-family-name visually-hidden';
86 familySpan.textContent = familyName;
87 nameElement.appendChild(familySpan);
88 }
89
90 if (nickname && nickname !== displayName) {
91 const nickSpan = document.createElement('span');
92 nickSpan.className = 'p-nickname visually-hidden';
93 nickSpan.textContent = nickname;
94 nameElement.appendChild(nickSpan);
95 }
96
97 infoContainer.appendChild(nameElement);
98
99 // Job title and organization
100 if (jobTitle || org) {
101 const titleElement = document.createElement('div');
102 titleElement.className = 'author-title';
103
104 if (jobTitle) {
105 const jobSpan = document.createElement('span');
106 jobSpan.className = 'p-job-title';
107 jobSpan.textContent = jobTitle;
108 titleElement.appendChild(jobSpan);
109
110 if (org) {
111 titleElement.appendChild(document.createTextNode(' at '));
112 }
113 }
114
115 if (org) {
116 const orgSpan = document.createElement('span');
117 orgSpan.className = 'p-org';
118 orgSpan.textContent = org;
119 titleElement.appendChild(orgSpan);
120 }
121
122 infoContainer.appendChild(titleElement);
123 }
124
125 // Bio/note
126 if (bio) {
127 const bioElement = document.createElement('div');
128 bioElement.className = 'p-note author-bio';
129 bioElement.textContent = bio;
130 infoContainer.appendChild(bioElement);
131 }
132
133 // Social links
134 const socialContainer = document.createElement('div');
135 socialContainer.className = 'author-social';
136 let hasSocial = false;
137
138 if (email) {
139 const emailLink = document.createElement('a');
140 emailLink.className = 'u-email social-link';
141 emailLink.href = `mailto:${email}`;
142 emailLink.textContent = 'Email';
143 emailLink.setAttribute('aria-label', `Email ${displayName}`);
144 socialContainer.appendChild(emailLink);
145 hasSocial = true;
146 }
147
148 if (github) {
149 const githubLink = document.createElement('a');
150 githubLink.className = 'u-url social-link';
151 githubLink.href = `https://github.com/${github}`;
152 githubLink.textContent = 'GitHub';
153 githubLink.setAttribute('aria-label', `${displayName} on GitHub`);
154 githubLink.rel = 'me';
155 socialContainer.appendChild(githubLink);
156 hasSocial = true;
157 }
158
159 if (mastodon) {
160 const mastodonLink = document.createElement('a');
161 mastodonLink.className = 'u-url social-link';
162 mastodonLink.href = mastodon;
163 mastodonLink.textContent = 'Mastodon';
164 mastodonLink.setAttribute('aria-label', `${displayName} on Mastodon`);
165 mastodonLink.rel = 'me';
166 socialContainer.appendChild(mastodonLink);
167 hasSocial = true;
168 }
169
170 if (twitter) {
171 const twitterLink = document.createElement('a');
172 twitterLink.className = 'u-url social-link';
173 twitterLink.href = `https://twitter.com/${twitter}`;
174 twitterLink.textContent = 'Twitter';
175 twitterLink.setAttribute('aria-label', `${displayName} on Twitter`);
176 twitterLink.rel = 'me';
177 socialContainer.appendChild(twitterLink);
178 hasSocial = true;
179 }
180
181 if (website && !this.shouldLinkProfile) {
182 const websiteLink = document.createElement('a');
183 websiteLink.className = 'u-url social-link';
184 websiteLink.href = website;
185 websiteLink.textContent = 'Website';
186 websiteLink.setAttribute('aria-label', `${displayName}'s website`);
187 socialContainer.appendChild(websiteLink);
188 hasSocial = true;
189 }
190
191 if (hasSocial) {
192 infoContainer.appendChild(socialContainer);
193 }
194
195 hcard.appendChild(infoContainer);
196 return hcard;
197 }
198
199 render() {
200 const styles = `
201 <style>
202 :host {
203 display: var(--bushel-author-display, inline-block);
204 font-family: var(--bushel-author-font-family, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif);
205 }
206
207 .h-card {
208 display: flex;
209 align-items: var(--bushel-author-align, flex-start);
210 gap: var(--bushel-author-gap, 0.75rem);
211 padding: var(--bushel-author-padding, 0);
212 background: var(--bushel-author-background, transparent);
213 border: var(--bushel-author-border, none);
214 border-radius: var(--bushel-author-border-radius, 0);
215 }
216
217 :host([display-mode="card"]) .h-card {
218 flex-direction: column;
219 text-align: center;
220 padding: var(--bushel-author-card-padding, 1rem);
221 background: var(--bushel-author-card-background, #f8f9fa);
222 border: var(--bushel-author-card-border, 1px solid #e9ecef);
223 border-radius: var(--bushel-author-card-border-radius, 0.5rem);
224 }
225
226 :host([display-mode="compact"]) .h-card {
227 gap: var(--bushel-author-compact-gap, 0.5rem);
228 }
229
230 .author-photo {
231 width: var(--bushel-author-photo-size, 2.5rem);
232 height: var(--bushel-author-photo-size, 2.5rem);
233 border-radius: var(--bushel-author-photo-radius, 50%);
234 object-fit: cover;
235 flex-shrink: 0;
236 }
237
238 :host([display-mode="card"]) .author-photo {
239 width: var(--bushel-author-card-photo-size, 4rem);
240 height: var(--bushel-author-card-photo-size, 4rem);
241 }
242
243 .author-info {
244 display: flex;
245 flex-direction: column;
246 gap: var(--bushel-author-info-gap, 0.25rem);
247 min-width: 0;
248 }
249
250 .author-name {
251 font-weight: var(--bushel-author-name-weight, 600);
252 color: var(--bushel-author-name-color, inherit);
253 text-decoration: none;
254 font-size: var(--bushel-author-name-size, 1rem);
255 }
256
257 .author-name:hover {
258 text-decoration: underline;
259 }
260
261 .author-title {
262 font-size: var(--bushel-author-title-size, 0.875rem);
263 color: var(--bushel-author-title-color, #666);
264 }
265
266 .author-bio {
267 font-size: var(--bushel-author-bio-size, 0.875rem);
268 color: var(--bushel-author-bio-color, #555);
269 line-height: 1.4;
270 }
271
272 .author-social {
273 display: flex;
274 gap: var(--bushel-author-social-gap, 0.75rem);
275 margin-top: var(--bushel-author-social-margin, 0.25rem);
276 }
277
278 :host([display-mode="card"]) .author-social {
279 justify-content: center;
280 }
281
282 :host([display-mode="compact"]) .author-social {
283 gap: var(--bushel-author-compact-social-gap, 0.5rem);
284 }
285
286 .social-link {
287 font-size: var(--bushel-author-social-size, 0.8125rem);
288 color: var(--bushel-author-social-color, #007bff);
289 text-decoration: none;
290 padding: var(--bushel-author-social-padding, 0.125rem 0.25rem);
291 border-radius: var(--bushel-author-social-radius, 0.25rem);
292 }
293
294 .social-link:hover {
295 background: var(--bushel-author-social-hover-bg, rgba(0, 123, 255, 0.1));
296 text-decoration: none;
297 }
298
299 .visually-hidden {
300 position: absolute !important;
301 width: 1px !important;
302 height: 1px !important;
303 padding: 0 !important;
304 margin: -1px !important;
305 overflow: hidden !important;
306 clip: rect(0, 0, 0, 0) !important;
307 white-space: nowrap !important;
308 border: 0 !important;
309 }
310
311 @media (max-width: 600px) {
312 :host([display-mode="card"]) .h-card {
313 padding: var(--bushel-author-mobile-padding, 0.75rem);
314 }
315
316 .author-social {
317 flex-wrap: wrap;
318 }
319 }
320 </style>
321 `;
322
323 this.shadowRoot.innerHTML = styles + this.buildHCard().outerHTML;
324 }
325}
326
327customElements.define('bushel-author', BushelAuthor);