···
1
+
class BushelAuthor extends HTMLElement {
4
+
this.attachShadow({ mode: 'open' });
7
+
static get observedAttributes() {
9
+
'name', 'given-name', 'family-name', 'nickname',
10
+
'email', 'website', 'github', 'mastodon', 'twitter',
11
+
'photo', 'bio', 'org', 'job-title',
12
+
'display-mode', 'link-profile'
16
+
connectedCallback() {
20
+
attributeChangedCallback() {
25
+
return this.getAttribute('display-mode') || 'inline';
28
+
get shouldLinkProfile() {
29
+
return this.hasAttribute('link-profile');
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');
47
+
const displayName = name || `${givenName || ''} ${familyName || ''}`.trim() || nickname;
49
+
const hcard = document.createElement('div');
50
+
hcard.className = 'h-card bushel-author-card';
54
+
const img = document.createElement('img');
55
+
img.className = 'u-photo author-photo';
57
+
img.alt = `Photo of ${displayName}`;
58
+
img.loading = 'lazy';
59
+
hcard.appendChild(img);
62
+
const infoContainer = document.createElement('div');
63
+
infoContainer.className = 'author-info';
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;
70
+
if (this.shouldLinkProfile && website) {
71
+
nameElement.href = website;
72
+
nameElement.className += ' u-url';
75
+
// Hidden structured name parts
77
+
const givenSpan = document.createElement('span');
78
+
givenSpan.className = 'p-given-name visually-hidden';
79
+
givenSpan.textContent = givenName;
80
+
nameElement.appendChild(givenSpan);
84
+
const familySpan = document.createElement('span');
85
+
familySpan.className = 'p-family-name visually-hidden';
86
+
familySpan.textContent = familyName;
87
+
nameElement.appendChild(familySpan);
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);
97
+
infoContainer.appendChild(nameElement);
99
+
// Job title and organization
100
+
if (jobTitle || org) {
101
+
const titleElement = document.createElement('div');
102
+
titleElement.className = 'author-title';
105
+
const jobSpan = document.createElement('span');
106
+
jobSpan.className = 'p-job-title';
107
+
jobSpan.textContent = jobTitle;
108
+
titleElement.appendChild(jobSpan);
111
+
titleElement.appendChild(document.createTextNode(' at '));
116
+
const orgSpan = document.createElement('span');
117
+
orgSpan.className = 'p-org';
118
+
orgSpan.textContent = org;
119
+
titleElement.appendChild(orgSpan);
122
+
infoContainer.appendChild(titleElement);
127
+
const bioElement = document.createElement('div');
128
+
bioElement.className = 'p-note author-bio';
129
+
bioElement.textContent = bio;
130
+
infoContainer.appendChild(bioElement);
134
+
const socialContainer = document.createElement('div');
135
+
socialContainer.className = 'author-social';
136
+
let hasSocial = false;
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);
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);
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);
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);
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);
192
+
infoContainer.appendChild(socialContainer);
195
+
hcard.appendChild(infoContainer);
203
+
display: var(--bushel-author-display, inline-block);
204
+
font-family: var(--bushel-author-font-family, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif);
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);
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);
226
+
:host([display-mode="compact"]) .h-card {
227
+
gap: var(--bushel-author-compact-gap, 0.5rem);
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%);
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);
245
+
flex-direction: column;
246
+
gap: var(--bushel-author-info-gap, 0.25rem);
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);
257
+
.author-name:hover {
258
+
text-decoration: underline;
262
+
font-size: var(--bushel-author-title-size, 0.875rem);
263
+
color: var(--bushel-author-title-color, #666);
267
+
font-size: var(--bushel-author-bio-size, 0.875rem);
268
+
color: var(--bushel-author-bio-color, #555);
274
+
gap: var(--bushel-author-social-gap, 0.75rem);
275
+
margin-top: var(--bushel-author-social-margin, 0.25rem);
278
+
:host([display-mode="card"]) .author-social {
279
+
justify-content: center;
282
+
:host([display-mode="compact"]) .author-social {
283
+
gap: var(--bushel-author-compact-social-gap, 0.5rem);
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);
294
+
.social-link:hover {
295
+
background: var(--bushel-author-social-hover-bg, rgba(0, 123, 255, 0.1));
296
+
text-decoration: none;
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;
311
+
@media (max-width: 600px) {
312
+
:host([display-mode="card"]) .h-card {
313
+
padding: var(--bushel-author-mobile-padding, 0.75rem);
323
+
this.shadowRoot.innerHTML = styles + this.buildHCard().outerHTML;
327
+
customElements.define('bushel-author', BushelAuthor);