feat(org-chart): add core cards and fixed-layout integration
This commit is contained in:
1 parent
d18e3c3380
commit
61c6f8d1f1
161 files changed
+13044
-1
No files matched your search
@@ -0,0 +1,129 @@
|
||||
import type { Employee } from '../data/index.js';
|
||||
import personPlaceholder from '../assets/person-placeholder.svg?inline';
|
||||
import { createAffiliationMark } from './affiliation-mark.js';
|
||||
import type { DepartmentCardAsset } from './department-card-assets.js';
|
||||
import type { RepresentativeCardAsset } from './representative-card-asset.js';
|
||||
import { observePersonNameOverflow } from './person-name-overflow.js';
|
||||
|
||||
const statusNames = { active: '재직', dispatched: '파견', onLeave: '휴직' } as const;
|
||||
|
||||
export interface EmployeeCardOptions {
|
||||
kind: 'topLeader' | 'leader' | 'employee';
|
||||
affiliationName?: string;
|
||||
cardAsset?: DepartmentCardAsset | RepresentativeCardAsset;
|
||||
/** Full Composition presentation only; the employee dataset is untouched. */
|
||||
workdeskPresentation?: boolean;
|
||||
/** Compact read-only status groups already convey this label in their header. */
|
||||
hideStatusLabel?: boolean;
|
||||
}
|
||||
|
||||
function text(document: Document, tag: string, className: string, value: string): HTMLElement {
|
||||
const element = document.createElement(tag);
|
||||
element.className = className;
|
||||
element.textContent = value;
|
||||
return element;
|
||||
}
|
||||
|
||||
/** Content renderer shared by the hierarchy and supplementary status groups. */
|
||||
export function createEmployeeCard(
|
||||
document: Document, employee: Employee, options: EmployeeCardOptions, signal: AbortSignal,
|
||||
): HTMLElement {
|
||||
const leader = options.kind !== 'employee';
|
||||
const representative = options.kind === 'topLeader' || options.cardAsset?.size === 'representative';
|
||||
const compactStatus = Boolean(options.workdeskPresentation && options.hideStatusLabel && !options.cardAsset);
|
||||
const styledCard = Boolean(options.cardAsset) || compactStatus;
|
||||
const presentationName = options.workdeskPresentation || representative;
|
||||
const displayName = presentationName ? '홍길동' : employee.name;
|
||||
const displayedName = presentationName ? '홍 길 동' : displayName;
|
||||
const card = document.createElement('div');
|
||||
card.className = `hm-orgchart-employee${leader ? ' hm-orgchart-leader' : ''}`;
|
||||
card.dataset.employeeId = employee.id;
|
||||
card.dataset.cardKind = options.kind;
|
||||
card.dataset.workStatus = employee.workStatus;
|
||||
if (options.workdeskPresentation) card.dataset.workdeskPresentation = 'true';
|
||||
if (compactStatus) card.dataset.cardLayout = 'compact';
|
||||
if (options.cardAsset) {
|
||||
const asset = options.cardAsset;
|
||||
card.dataset.cardAsset = 'departmentId' in asset ? asset.departmentId : 'representative';
|
||||
card.dataset.cardAssetSize = asset.size;
|
||||
const background = document.createElement('img');
|
||||
background.className = 'hm-orgchart-card-background';
|
||||
background.alt = '';
|
||||
background.setAttribute('aria-hidden', 'true');
|
||||
background.width = asset.width;
|
||||
background.height = asset.height;
|
||||
background.src = asset.url;
|
||||
card.append(background);
|
||||
}
|
||||
|
||||
// One neutral icon is shared by every card type and both runtime adapters.
|
||||
// The approved rectangular photo slots keep their existing dimensions.
|
||||
const avatar = document.createElement('span');
|
||||
avatar.className = 'hm-orgchart-avatar';
|
||||
avatar.setAttribute('aria-hidden', 'true');
|
||||
const image = document.createElement('img');
|
||||
image.alt = '';
|
||||
image.loading = 'lazy';
|
||||
image.decoding = 'async';
|
||||
image.referrerPolicy = 'no-referrer';
|
||||
const showPlaceholder = () => {
|
||||
card.dataset.photoState = 'placeholder';
|
||||
image.dataset.photoPlaceholder = 'true';
|
||||
image.src = personPlaceholder;
|
||||
};
|
||||
if (employee.profileImage) {
|
||||
card.dataset.photoState = 'image';
|
||||
image.addEventListener('error', showPlaceholder, { once: true, signal });
|
||||
// Images have already passed the dataset URL validation. No HTML parsing.
|
||||
image.src = employee.profileImage;
|
||||
} else showPlaceholder();
|
||||
avatar.append(image);
|
||||
|
||||
const details = document.createElement('div');
|
||||
details.className = 'hm-orgchart-employee-details';
|
||||
const displayedPosition = representative ? '대표이사' : employee.position.replace(/\s+/gu, '');
|
||||
const position = text(document, 'span', 'hm-orgchart-person-role', displayedPosition);
|
||||
const name = text(document, 'strong', 'hm-orgchart-person-name', displayedName);
|
||||
if (presentationName) {
|
||||
position.setAttribute('aria-label', representative ? displayedPosition : employee.position);
|
||||
name.setAttribute('aria-label', displayName);
|
||||
}
|
||||
const inlineRoleBadge = styledCard && !representative;
|
||||
const positionRow = inlineRoleBadge ? document.createElement('div') : null;
|
||||
if (positionRow) {
|
||||
positionRow.className = 'hm-orgchart-position-row';
|
||||
positionRow.append(position);
|
||||
}
|
||||
if (styledCard) {
|
||||
const personText = document.createElement('div'); personText.className = 'hm-orgchart-person-text';
|
||||
personText.append(positionRow ?? position, name); details.append(personText);
|
||||
observePersonNameOverflow(card, name, signal);
|
||||
} else details.append(position, name);
|
||||
const attributes = document.createElement('div');
|
||||
attributes.className = 'hm-orgchart-person-attributes';
|
||||
// Representative cards show only position/name in every consumer. Raw employee
|
||||
// roles remain available to dialogs/statistics; other cards keep their policy.
|
||||
const hideExecutiveBadge = options.workdeskPresentation && employee.jobTitle?.trim() === '경영진';
|
||||
if (!representative && !hideExecutiveBadge && (employee.jobTitle || (leader && !options.cardAsset))) {
|
||||
const rawBadge = employee.jobTitle || '조직장';
|
||||
const badge = text(document, 'span', 'hm-orgchart-job-title', options.workdeskPresentation ? rawBadge.replace(/\s+/gu, '') : rawBadge);
|
||||
if (options.workdeskPresentation) badge.setAttribute('aria-label', employee.jobTitle || '조직장');
|
||||
badge.title = badge.textContent!;
|
||||
(positionRow ?? attributes).append(badge);
|
||||
}
|
||||
if (!representative && !options.hideStatusLabel) attributes.append(text(document, 'span', 'hm-orgchart-status-label', statusNames[employee.workStatus]));
|
||||
if (styledCard) {
|
||||
card.append(avatar, details);
|
||||
if (attributes.childElementCount) card.append(attributes);
|
||||
} else {
|
||||
if (attributes.childElementCount) details.append(attributes);
|
||||
card.append(avatar, details);
|
||||
}
|
||||
|
||||
if (options.affiliationName) {
|
||||
const affiliation = createAffiliationMark(document, employee.affiliationId, options.affiliationName, signal);
|
||||
card.dataset.hasAffiliation = 'true';
|
||||
card.append(affiliation);
|
||||
}
|
||||
return card;
|
||||
}
|
||||
Reference in new issue
Block a user