feat: add favorites drawer management

This commit is contained in:
wxs
2026-07-17 12:56:19 +08:00
parent e08fc99def
commit 2f2a590df4
4 changed files with 1558 additions and 0 deletions
@@ -0,0 +1,250 @@
const activeDialogs = new WeakMap<
Document,
{
input: HTMLInputElement;
promise: Promise<string | null>;
}
>();
export function promptForFavoriteFolderName(
document: Document,
options: {
initialValue?: string;
title: "新建收藏夹" | "重命名收藏夹";
}
): Promise<string | null> {
const activeDialog = activeDialogs.get(document);
if (activeDialog) {
activeDialog.input.focus();
activeDialog.input.select();
return activeDialog.promise;
}
const previousFocusedElement = document.activeElement;
const root = document.createElement("div");
root.dataset.scesFavoriteFolderDialog = "root";
root.setAttribute("role", "dialog");
root.setAttribute("aria-modal", "true");
root.setAttribute("aria-labelledby", "sces-favorite-folder-dialog-title");
applyOverlayStyles(root);
const panel = document.createElement("section");
applyPanelStyles(panel);
const title = document.createElement("h2");
title.id = "sces-favorite-folder-dialog-title";
title.textContent = options.title;
applyTitleStyles(title);
const input = document.createElement("input");
input.type = "text";
input.value = options.initialValue ?? "";
input.maxLength = 60;
input.placeholder = "请输入收藏夹名称";
input.dataset.scesFavoriteFolderDialog = "input";
input.setAttribute("aria-label", "收藏夹名称");
applyInputStyles(input);
const error = document.createElement("p");
error.dataset.scesFavoriteFolderDialog = "error";
error.setAttribute("aria-live", "polite");
applyErrorStyles(error);
const actions = document.createElement("div");
applyActionStyles(actions);
const cancelButton = createButton(document, "取消");
cancelButton.dataset.scesFavoriteFolderDialog = "cancel";
applySecondaryButtonStyles(cancelButton);
const confirmButton = createButton(document, "确认");
confirmButton.dataset.scesFavoriteFolderDialog = "confirm";
applyPrimaryButtonStyles(confirmButton);
actions.append(cancelButton, confirmButton);
panel.append(title, input, error, actions);
root.append(panel);
(document.body ?? document.documentElement).append(root);
let closeDialog: (value: string | null) => void = () => {};
const promise = new Promise<string | null>((resolve) => {
let closed = false;
const clearError = () => {
if (!input.value.trim()) {
return;
}
error.textContent = "";
input.removeAttribute("aria-invalid");
};
const submit = () => {
const value = input.value.trim();
if (!value) {
error.textContent = "请输入收藏夹名称";
input.setAttribute("aria-invalid", "true");
input.focus();
return;
}
closeDialog(value);
};
const onKeydown = (event: KeyboardEvent) => {
if (event.key === "Escape") {
event.preventDefault();
closeDialog(null);
return;
}
if (event.key === "Tab") {
const focusableElements = Array.from(
root.querySelectorAll<HTMLElement>("button:not(:disabled), input:not(:disabled)")
);
const activeIndex = focusableElements.indexOf(
document.activeElement as HTMLElement
);
const nextIndex = event.shiftKey
? activeIndex <= 0
? focusableElements.length - 1
: activeIndex - 1
: activeIndex === focusableElements.length - 1
? 0
: activeIndex + 1;
const nextElement = focusableElements[nextIndex];
if (nextElement) {
event.preventDefault();
nextElement.focus();
}
return;
}
if (event.key === "Enter") {
event.preventDefault();
submit();
}
};
const onOverlayClick = (event: MouseEvent) => {
if (event.target === root) {
closeDialog(null);
}
};
const onCancel = () => closeDialog(null);
const onConfirm = () => submit();
closeDialog = (value) => {
if (closed) {
return;
}
closed = true;
activeDialogs.delete(document);
input.removeEventListener("input", clearError);
cancelButton.removeEventListener("click", onCancel);
confirmButton.removeEventListener("click", onConfirm);
root.removeEventListener("click", onOverlayClick);
document.removeEventListener("keydown", onKeydown, true);
root.remove();
if (
previousFocusedElement instanceof HTMLElement &&
previousFocusedElement.isConnected
) {
previousFocusedElement.focus();
}
resolve(value);
};
input.addEventListener("input", clearError);
cancelButton.addEventListener("click", onCancel);
confirmButton.addEventListener("click", onConfirm);
root.addEventListener("click", onOverlayClick);
document.addEventListener("keydown", onKeydown, true);
});
activeDialogs.set(document, { input, promise });
input.focus();
return promise;
}
function createButton(document: Document, label: string): HTMLButtonElement {
const button = document.createElement("button");
button.type = "button";
button.textContent = label;
return button;
}
function applyOverlayStyles(root: HTMLElement): void {
root.style.position = "fixed";
root.style.inset = "0";
root.style.zIndex = "2147483647";
root.style.display = "flex";
root.style.alignItems = "center";
root.style.justifyContent = "center";
root.style.padding = "24px";
root.style.boxSizing = "border-box";
root.style.background = "rgba(15, 23, 42, 0.38)";
}
function applyPanelStyles(panel: HTMLElement): void {
panel.style.width = "min(420px, calc(100vw - 32px))";
panel.style.padding = "20px";
panel.style.boxSizing = "border-box";
panel.style.border = "1px solid #d7dde6";
panel.style.borderRadius = "8px";
panel.style.background = "#ffffff";
panel.style.boxShadow = "0 18px 45px rgba(15, 23, 42, 0.22)";
}
function applyTitleStyles(title: HTMLElement): void {
title.style.margin = "0";
title.style.color = "#172033";
title.style.fontSize = "18px";
title.style.lineHeight = "26px";
title.style.fontWeight = "700";
}
function applyInputStyles(input: HTMLInputElement): void {
input.style.width = "100%";
input.style.height = "40px";
input.style.marginTop = "18px";
input.style.padding = "0 12px";
input.style.boxSizing = "border-box";
input.style.border = "1px solid #b9c4d3";
input.style.borderRadius = "6px";
input.style.color = "#172033";
input.style.fontSize = "14px";
}
function applyErrorStyles(error: HTMLElement): void {
error.style.minHeight = "18px";
error.style.margin = "6px 0 0";
error.style.color = "#b42318";
error.style.fontSize = "12px";
error.style.lineHeight = "18px";
}
function applyActionStyles(actions: HTMLElement): void {
actions.style.display = "flex";
actions.style.justifyContent = "flex-end";
actions.style.gap = "8px";
actions.style.marginTop = "16px";
}
function applySecondaryButtonStyles(button: HTMLButtonElement): void {
button.style.height = "34px";
button.style.padding = "0 14px";
button.style.border = "1px solid #b9c4d3";
button.style.borderRadius = "6px";
button.style.background = "#ffffff";
button.style.color = "#334155";
button.style.cursor = "pointer";
}
function applyPrimaryButtonStyles(button: HTMLButtonElement): void {
button.style.height = "34px";
button.style.padding = "0 14px";
button.style.border = "1px solid #0f766e";
button.style.borderRadius = "6px";
button.style.background = "#0f766e";
button.style.color = "#ffffff";
button.style.cursor = "pointer";
}
+769
View File
@@ -0,0 +1,769 @@
import type {
FavoriteCreator,
FavoritesStateV1
} from "./favorites-store";
export interface FavoritesDrawerHandlers {
onCreateFolder(): Promise<void>;
onDeleteFolder(folderId: string): Promise<void>;
onImportFolder(folderId: string, authorIds: string[]): Promise<void>;
onImportSelected(authorIds: string[]): Promise<void>;
onRemoveCreatorFromFolder(authorId: string, folderId: string): Promise<void>;
onRenameFolder(folderId: string): Promise<void>;
}
export interface FavoritesDrawerController {
dispose(): void;
render(state: FavoritesStateV1): void;
setBusy(isBusy: boolean): void;
setError(message: string): void;
}
const DRAWER_ID = "sces-favorites-drawer";
const activeDrawers = new WeakMap<Document, FavoritesDrawerController>();
export function createFavoritesDrawer(
document: Document,
handlers: FavoritesDrawerHandlers,
confirmImport: (message: string) => boolean
): FavoritesDrawerController {
activeDrawers.get(document)?.dispose();
document
.querySelectorAll(
'[data-sces-favorites-tab="button"], [data-sces-favorites-drawer="root"]'
)
.forEach((element) => element.remove());
const tab = createButton(document, "收藏夹");
tab.dataset.scesFavoritesTab = "button";
tab.setAttribute("aria-controls", DRAWER_ID);
tab.setAttribute("aria-expanded", "false");
tab.setAttribute("aria-haspopup", "dialog");
applyTabStyles(tab);
const drawer = document.createElement("aside");
drawer.id = DRAWER_ID;
drawer.dataset.scesFavoritesDrawer = "root";
drawer.hidden = true;
drawer.tabIndex = -1;
drawer.setAttribute("role", "dialog");
drawer.setAttribute("aria-label", "收藏夹");
drawer.setAttribute("aria-modal", "false");
drawer.setAttribute("aria-hidden", "true");
applyDrawerStyles(drawer);
(document.body ?? document.documentElement).append(tab, drawer);
let busy = false;
let disposed = false;
let errorMessage = "";
let isOpen = false;
let searchQuery = "";
let state = emptyState();
let view: "all" | string = "all";
let selectedAuthorIds = new Set<string>();
const controller: FavoritesDrawerController = {
dispose,
render(nextState) {
if (disposed) {
return;
}
state = nextState;
renderDrawer();
},
setBusy(isBusy) {
if (disposed) {
return;
}
busy = isBusy;
renderDrawer();
},
setError(message) {
if (disposed) {
return;
}
errorMessage = message;
renderDrawer();
}
};
const onTabClick = () => {
if (isOpen) {
closeDrawer();
} else {
openDrawer();
}
};
const onDocumentKeydown = (event: KeyboardEvent) => {
if (event.key !== "Escape" || !isOpen) {
return;
}
event.preventDefault();
closeDrawer();
};
const onDrawerClick = (event: MouseEvent) => {
const target = event.target;
if (!(target instanceof Element)) {
return;
}
const button = target.closest("button");
if (!(button instanceof HTMLButtonElement)) {
return;
}
if (button.dataset.scesFavoritesDrawer === "close") {
closeDrawer();
return;
}
if (button.dataset.scesFavoritesDrawer === "view-all") {
view = "all";
renderDrawer();
return;
}
const folderId = button.dataset.scesFavoritesFolderId;
if (folderId) {
view = folderId;
renderDrawer();
return;
}
if (busy) {
return;
}
if (button.dataset.scesFavoritesDrawer === "create-folder") {
invokeHandler(handlers.onCreateFolder);
return;
}
const renameFolderId = button.dataset.scesFavoritesFolderRename;
if (renameFolderId) {
invokeHandler(() => handlers.onRenameFolder(renameFolderId));
return;
}
const deleteFolderId = button.dataset.scesFavoritesFolderDelete;
if (deleteFolderId) {
invokeHandler(() => handlers.onDeleteFolder(deleteFolderId));
return;
}
const removeAuthorId = button.dataset.scesFavoritesRemoveAuthorId;
if (removeAuthorId && view !== "all") {
invokeHandler(() => handlers.onRemoveCreatorFromFolder(removeAuthorId, view));
return;
}
if (button.dataset.scesFavoritesDrawer === "import-selected") {
const authorIds = selectedCreatorsInCurrentView().map((creator) => creator.authorId);
if (authorIds.length > 0) {
invokeHandler(() => handlers.onImportSelected(authorIds));
}
return;
}
if (button.dataset.scesFavoritesDrawer === "import-folder" && view !== "all") {
const authorIds = creatorsInCurrentView().map((creator) => creator.authorId);
const message = `将当前收藏夹的 ${authorIds.length} 位达人导入秒探吗?`;
if (confirmImport(message)) {
invokeHandler(() => handlers.onImportFolder(view, authorIds));
}
}
};
const onDrawerInput = (event: Event) => {
const target = event.target;
if (
target instanceof HTMLInputElement &&
target.dataset.scesFavoritesDrawer === "search"
) {
searchQuery = target.value;
const selectionStart = target.selectionStart;
const selectionEnd = target.selectionEnd;
renderDrawer();
const search = drawer.querySelector(
'[data-sces-favorites-drawer="search"]'
);
if (search instanceof HTMLInputElement) {
search.focus();
if (selectionStart !== null && selectionEnd !== null) {
search.setSelectionRange(selectionStart, selectionEnd);
}
}
}
};
const onDrawerChange = (event: Event) => {
const target = event.target;
if (
busy ||
!(target instanceof HTMLInputElement) ||
target.type !== "checkbox"
) {
return;
}
const authorId = target.dataset.scesFavoritesSelectAuthorId;
if (!authorId) {
return;
}
if (target.checked) {
selectedAuthorIds.add(authorId);
} else {
selectedAuthorIds.delete(authorId);
}
renderDrawer();
};
tab.addEventListener("click", onTabClick);
drawer.addEventListener("click", onDrawerClick);
drawer.addEventListener("input", onDrawerInput);
drawer.addEventListener("change", onDrawerChange);
document.addEventListener("keydown", onDocumentKeydown);
renderDrawer();
activeDrawers.set(document, controller);
return controller;
function openDrawer(): void {
if (disposed) {
return;
}
isOpen = true;
drawer.hidden = false;
drawer.setAttribute("aria-hidden", "false");
tab.setAttribute("aria-expanded", "true");
renderDrawer();
const closeButton = drawer.querySelector(
'[data-sces-favorites-drawer="close"]'
);
if (closeButton instanceof HTMLButtonElement) {
closeButton.focus();
} else {
drawer.focus();
}
}
function closeDrawer(): void {
if (disposed || !isOpen) {
return;
}
isOpen = false;
drawer.hidden = true;
drawer.setAttribute("aria-hidden", "true");
tab.setAttribute("aria-expanded", "false");
if (tab.isConnected) {
tab.focus();
}
}
function renderDrawer(): void {
if (view !== "all" && !state.folders.some((folder) => folder.id === view)) {
view = "all";
}
const currentCreators = creatorsInCurrentView();
const currentAuthorIds = new Set(currentCreators.map((creator) => creator.authorId));
selectedAuthorIds = new Set(
Array.from(selectedAuthorIds).filter((authorId) => currentAuthorIds.has(authorId))
);
const query = searchQuery.trim().toLocaleLowerCase();
const visibleCreators = currentCreators.filter((creator) =>
creator.authorName.toLocaleLowerCase().includes(query)
);
const selectedCount = selectedCreatorsInCurrentView().length;
const fragment = document.createDocumentFragment();
fragment.append(createHeader());
if (errorMessage) {
const error = document.createElement("p");
error.dataset.scesFavoritesDrawer = "error";
error.textContent = errorMessage;
applyErrorStyles(error);
fragment.append(error);
}
fragment.append(createFolderNavigation());
fragment.append(createSearch());
fragment.append(createCreatorList(visibleCreators));
fragment.append(createFooter(selectedCount));
drawer.replaceChildren(fragment);
}
function createHeader(): HTMLElement {
const header = document.createElement("header");
applyHeaderStyles(header);
const title = document.createElement("h2");
title.textContent = "收藏夹";
applyTitleStyles(title);
const actions = document.createElement("div");
applyHeaderActionsStyles(actions);
const createFolder = createButton(document, "新建收藏夹");
createFolder.dataset.scesFavoritesDrawer = "create-folder";
createFolder.disabled = busy;
applySecondaryButtonStyles(createFolder);
const close = createButton(document, "关闭收藏夹");
close.dataset.scesFavoritesDrawer = "close";
close.setAttribute("aria-label", "关闭收藏夹");
applyIconButtonStyles(close);
actions.append(createFolder, close);
header.append(title, actions);
return header;
}
function createFolderNavigation(): HTMLElement {
const navigation = document.createElement("nav");
navigation.setAttribute("aria-label", "收藏夹列表");
applyNavigationStyles(navigation);
const allButton = createButton(document, "全部收藏");
allButton.dataset.scesFavoritesDrawer = "view-all";
allButton.setAttribute("aria-current", String(view === "all"));
applyViewButtonStyles(allButton, view === "all");
navigation.append(allButton);
state.folders.forEach((folder) => {
const row = document.createElement("div");
applyFolderRowStyles(row);
const viewButton = createButton(document, folder.name);
viewButton.dataset.scesFavoritesFolderId = folder.id;
viewButton.setAttribute("aria-current", String(view === folder.id));
applyViewButtonStyles(viewButton, view === folder.id);
const rename = createButton(document, "重命名");
rename.dataset.scesFavoritesFolderRename = folder.id;
rename.setAttribute("aria-label", `重命名收藏夹 ${folder.name}`);
rename.disabled = busy;
applyTextActionStyles(rename);
const remove = createButton(document, "删除");
remove.dataset.scesFavoritesFolderDelete = folder.id;
remove.setAttribute("aria-label", `删除收藏夹 ${folder.name}`);
remove.disabled = busy;
applyDangerActionStyles(remove);
row.append(viewButton, rename, remove);
navigation.append(row);
});
return navigation;
}
function createSearch(): HTMLInputElement {
const search = document.createElement("input");
search.type = "search";
search.value = searchQuery;
search.placeholder = "搜索达人名称";
search.dataset.scesFavoritesDrawer = "search";
search.setAttribute("aria-label", "搜索达人名称");
applySearchStyles(search);
return search;
}
function createCreatorList(creators: FavoriteCreator[]): HTMLElement {
const list = document.createElement("section");
list.dataset.scesFavoritesDrawer = "creators";
list.setAttribute("aria-label", "收藏达人");
applyCreatorListStyles(list);
if (creators.length === 0) {
const empty = document.createElement("p");
empty.dataset.scesFavoritesDrawer = "empty";
empty.textContent = "暂无收藏达人";
applyEmptyStyles(empty);
list.append(empty);
return list;
}
creators.forEach((creator) => {
const row = document.createElement("article");
row.dataset.scesFavoritesAuthorId = creator.authorId;
applyCreatorRowStyles(row);
const selectLabel = document.createElement("label");
applySelectLabelStyles(selectLabel);
const checkbox = document.createElement("input");
checkbox.type = "checkbox";
checkbox.checked = selectedAuthorIds.has(creator.authorId);
checkbox.disabled = busy;
checkbox.dataset.scesFavoritesSelectAuthorId = creator.authorId;
checkbox.setAttribute("aria-label", `选择达人 ${creator.authorName}`);
selectLabel.append(checkbox);
const details = document.createElement("div");
details.style.minWidth = "0";
details.style.flex = "1";
const name = document.createElement("strong");
name.textContent = creator.authorName;
applyCreatorNameStyles(name);
details.append(name);
if (view === "all") {
const affiliation = document.createElement("p");
affiliation.textContent = `所属:${folderNamesForCreator(creator.authorId).join("、") || "未分类"}`;
applyAffiliationStyles(affiliation);
details.append(affiliation);
}
row.append(selectLabel, details);
if (view !== "all") {
const remove = createButton(document, "移出收藏夹");
remove.dataset.scesFavoritesRemoveAuthorId = creator.authorId;
remove.setAttribute("aria-label", `${creator.authorName} 移出当前收藏夹`);
remove.disabled = busy;
applyDangerActionStyles(remove);
row.append(remove);
}
list.append(row);
});
return list;
}
function createFooter(selectedCount: number): HTMLElement {
const footer = document.createElement("footer");
applyFooterStyles(footer);
const selected = document.createElement("span");
selected.textContent = `已选择 ${selectedCount} 位达人`;
applySelectedCountStyles(selected);
const actions = document.createElement("div");
applyFooterActionsStyles(actions);
if (view !== "all") {
const importFolder = createButton(document, "导入当前收藏夹全部达人");
importFolder.dataset.scesFavoritesDrawer = "import-folder";
importFolder.disabled = busy;
applySecondaryButtonStyles(importFolder);
actions.append(importFolder);
}
const importSelected = createButton(document, `导入已选 ${selectedCount} 位达人`);
importSelected.dataset.scesFavoritesDrawer = "import-selected";
importSelected.disabled = busy || selectedCount === 0;
applyPrimaryButtonStyles(importSelected);
actions.append(importSelected);
footer.append(selected, actions);
return footer;
}
function creatorsInCurrentView(): FavoriteCreator[] {
const uniqueCreators = new Map<string, FavoriteCreator>();
if (view === "all") {
state.creators.forEach((creator) => {
if (!uniqueCreators.has(creator.authorId)) {
uniqueCreators.set(creator.authorId, creator);
}
});
return Array.from(uniqueCreators.values());
}
const creatorsById = new Map<string, FavoriteCreator>();
state.creators.forEach((creator) => {
if (!creatorsById.has(creator.authorId)) {
creatorsById.set(creator.authorId, creator);
}
});
state.memberships.forEach((membership) => {
if (membership.folderId !== view || uniqueCreators.has(membership.authorId)) {
return;
}
const creator = creatorsById.get(membership.authorId);
if (creator) {
uniqueCreators.set(creator.authorId, creator);
}
});
return Array.from(uniqueCreators.values());
}
function selectedCreatorsInCurrentView(): FavoriteCreator[] {
return creatorsInCurrentView().filter((creator) => selectedAuthorIds.has(creator.authorId));
}
function folderNamesForCreator(authorId: string): string[] {
const folderNamesById = new Map(state.folders.map((folder) => [folder.id, folder.name]));
const names = new Set<string>();
state.memberships.forEach((membership) => {
if (membership.authorId !== authorId) {
return;
}
const name = folderNamesById.get(membership.folderId);
if (name) {
names.add(name);
}
});
return Array.from(names);
}
function invokeHandler(handler: () => Promise<void>): void {
if (busy || disposed) {
return;
}
void handler().catch((error: unknown) => {
errorMessage = error instanceof Error ? error.message : "操作失败";
renderDrawer();
});
}
function dispose(): void {
if (disposed) {
return;
}
disposed = true;
tab.removeEventListener("click", onTabClick);
drawer.removeEventListener("click", onDrawerClick);
drawer.removeEventListener("input", onDrawerInput);
drawer.removeEventListener("change", onDrawerChange);
document.removeEventListener("keydown", onDocumentKeydown);
tab.remove();
drawer.remove();
if (activeDrawers.get(document) === controller) {
activeDrawers.delete(document);
}
}
}
function emptyState(): FavoritesStateV1 {
return { creators: [], folders: [], memberships: [], version: 1 };
}
function createButton(document: Document, label: string): HTMLButtonElement {
const button = document.createElement("button");
button.type = "button";
button.textContent = label;
return button;
}
function applyTabStyles(tab: HTMLButtonElement): void {
tab.style.position = "fixed";
tab.style.top = "50%";
tab.style.right = "56px";
tab.style.zIndex = "2147483645";
tab.style.padding = "12px 8px";
tab.style.border = "1px solid #0f766e";
tab.style.borderRadius = "6px 0 0 6px";
tab.style.background = "#ffffff";
tab.style.color = "#0f766e";
tab.style.boxShadow = "0 4px 14px rgba(15, 23, 42, 0.14)";
tab.style.cursor = "pointer";
tab.style.fontWeight = "700";
tab.style.writingMode = "vertical-rl";
}
function applyDrawerStyles(drawer: HTMLElement): void {
drawer.style.position = "fixed";
drawer.style.top = "64px";
drawer.style.right = "56px";
drawer.style.bottom = "24px";
drawer.style.zIndex = "2147483646";
drawer.style.display = "flex";
drawer.style.flexDirection = "column";
drawer.style.width = "380px";
drawer.style.maxWidth = "calc(100vw - 72px)";
drawer.style.boxSizing = "border-box";
drawer.style.overflow = "hidden";
drawer.style.border = "1px solid #d7dde6";
drawer.style.borderRadius = "8px";
drawer.style.background = "#ffffff";
drawer.style.boxShadow = "0 18px 45px rgba(15, 23, 42, 0.2)";
drawer.style.color = "#172033";
drawer.style.fontFamily = "Arial, sans-serif";
}
function applyHeaderStyles(header: HTMLElement): void {
header.style.display = "flex";
header.style.alignItems = "center";
header.style.justifyContent = "space-between";
header.style.gap = "12px";
header.style.padding = "16px";
header.style.borderBottom = "1px solid #e2e8f0";
}
function applyTitleStyles(title: HTMLElement): void {
title.style.margin = "0";
title.style.fontSize = "18px";
title.style.lineHeight = "24px";
title.style.fontWeight = "700";
}
function applyHeaderActionsStyles(actions: HTMLElement): void {
actions.style.display = "flex";
actions.style.alignItems = "center";
actions.style.gap = "8px";
}
function applyNavigationStyles(navigation: HTMLElement): void {
navigation.style.display = "flex";
navigation.style.flexDirection = "column";
navigation.style.gap = "4px";
navigation.style.padding = "12px 16px";
navigation.style.borderBottom = "1px solid #e2e8f0";
}
function applyFolderRowStyles(row: HTMLElement): void {
row.style.display = "flex";
row.style.alignItems = "center";
row.style.gap = "4px";
}
function applyViewButtonStyles(button: HTMLButtonElement, active: boolean): void {
button.style.minWidth = "0";
button.style.flex = "1";
button.style.height = "30px";
button.style.padding = "0 8px";
button.style.border = "0";
button.style.borderRadius = "4px";
button.style.background = active ? "#e0f2f1" : "transparent";
button.style.color = active ? "#0f766e" : "#334155";
button.style.cursor = "pointer";
button.style.fontSize = "13px";
button.style.overflow = "hidden";
button.style.textAlign = "left";
button.style.textOverflow = "ellipsis";
button.style.whiteSpace = "nowrap";
}
function applyTextActionStyles(button: HTMLButtonElement): void {
button.style.height = "26px";
button.style.padding = "0 4px";
button.style.border = "0";
button.style.background = "transparent";
button.style.color = "#475569";
button.style.cursor = "pointer";
button.style.fontSize = "12px";
}
function applyDangerActionStyles(button: HTMLButtonElement): void {
applyTextActionStyles(button);
button.style.color = "#b42318";
}
function applySearchStyles(search: HTMLInputElement): void {
search.style.height = "36px";
search.style.margin = "12px 16px";
search.style.padding = "0 10px";
search.style.border = "1px solid #cbd5e1";
search.style.borderRadius = "6px";
search.style.fontSize = "13px";
}
function applyCreatorListStyles(list: HTMLElement): void {
list.style.display = "flex";
list.style.flex = "1";
list.style.flexDirection = "column";
list.style.gap = "8px";
list.style.minHeight = "0";
list.style.padding = "0 16px 16px";
list.style.overflowY = "auto";
}
function applyCreatorRowStyles(row: HTMLElement): void {
row.style.display = "flex";
row.style.alignItems = "center";
row.style.gap = "10px";
row.style.padding = "10px";
row.style.border = "1px solid #e2e8f0";
row.style.borderRadius = "6px";
}
function applySelectLabelStyles(label: HTMLElement): void {
label.style.display = "flex";
label.style.alignItems = "center";
}
function applyCreatorNameStyles(name: HTMLElement): void {
name.style.display = "block";
name.style.overflow = "hidden";
name.style.fontSize = "14px";
name.style.lineHeight = "20px";
name.style.textOverflow = "ellipsis";
name.style.whiteSpace = "nowrap";
}
function applyAffiliationStyles(affiliation: HTMLElement): void {
affiliation.style.margin = "2px 0 0";
affiliation.style.color = "#64748b";
affiliation.style.fontSize = "12px";
affiliation.style.lineHeight = "18px";
}
function applyEmptyStyles(empty: HTMLElement): void {
empty.style.margin = "24px 0";
empty.style.color = "#64748b";
empty.style.fontSize = "13px";
empty.style.textAlign = "center";
}
function applyFooterStyles(footer: HTMLElement): void {
footer.style.display = "flex";
footer.style.alignItems = "center";
footer.style.justifyContent = "space-between";
footer.style.gap = "12px";
footer.style.padding = "12px 16px";
footer.style.borderTop = "1px solid #e2e8f0";
}
function applySelectedCountStyles(selected: HTMLElement): void {
selected.style.flex = "0 0 auto";
selected.style.color = "#475569";
selected.style.fontSize = "12px";
}
function applyFooterActionsStyles(actions: HTMLElement): void {
actions.style.display = "flex";
actions.style.alignItems = "center";
actions.style.justifyContent = "flex-end";
actions.style.gap = "8px";
actions.style.minWidth = "0";
}
function applyErrorStyles(error: HTMLElement): void {
error.style.margin = "0";
error.style.padding = "8px 16px";
error.style.background = "#fef2f2";
error.style.color = "#b42318";
error.style.fontSize = "12px";
error.style.lineHeight = "18px";
}
function applySecondaryButtonStyles(button: HTMLButtonElement): void {
button.style.height = "30px";
button.style.padding = "0 8px";
button.style.border = "1px solid #b9c4d3";
button.style.borderRadius = "5px";
button.style.background = "#ffffff";
button.style.color = "#334155";
button.style.cursor = "pointer";
button.style.fontSize = "12px";
}
function applyPrimaryButtonStyles(button: HTMLButtonElement): void {
button.style.height = "30px";
button.style.padding = "0 8px";
button.style.border = "1px solid #0f766e";
button.style.borderRadius = "5px";
button.style.background = "#0f766e";
button.style.color = "#ffffff";
button.style.cursor = "pointer";
button.style.fontSize = "12px";
}
function applyIconButtonStyles(button: HTMLButtonElement): void {
button.style.width = "30px";
button.style.height = "30px";
button.style.padding = "0";
button.style.border = "1px solid #b9c4d3";
button.style.borderRadius = "5px";
button.style.background = "#ffffff";
button.style.color = "#334155";
button.style.cursor = "pointer";
button.style.fontSize = "12px";
}