generated from nhcarrigan/template
style: fix lint warnings and a11y issues
- Add proper for/id associations for color picker labels (ConfigSidebar) - Add tabindex and svelte-ignore for dialog overlays (GitPanel) - Add standard mask/line-clamp CSS properties for compatibility - Remove unused .stat-highlight CSS selector (StatsDisplay) - Fix SvelteSet reactivity by using .clear() instead of reassignment - Make updateNotification properly reactive with $state
This commit is contained in:
@@ -249,6 +249,9 @@
|
|||||||
-webkit-mask:
|
-webkit-mask:
|
||||||
linear-gradient(#fff 0 0) content-box,
|
linear-gradient(#fff 0 0) content-box,
|
||||||
linear-gradient(#fff 0 0);
|
linear-gradient(#fff 0 0);
|
||||||
|
mask:
|
||||||
|
linear-gradient(#fff 0 0) content-box,
|
||||||
|
linear-gradient(#fff 0 0);
|
||||||
-webkit-mask-composite: xor;
|
-webkit-mask-composite: xor;
|
||||||
mask-composite: exclude;
|
mask-composite: exclude;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
|
|||||||
@@ -458,11 +458,12 @@
|
|||||||
|
|
||||||
<!-- Theme Selection -->
|
<!-- Theme Selection -->
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
<label class="block text-sm text-[var(--text-secondary)] mb-2">Theme</label>
|
<span class="block text-sm text-[var(--text-secondary)] mb-2">Theme</span>
|
||||||
<div class="flex flex-wrap gap-2">
|
<div class="flex flex-wrap gap-2" role="group" aria-label="Theme selection">
|
||||||
<button
|
<button
|
||||||
onclick={() => handleThemeChange("dark")}
|
onclick={() => handleThemeChange("dark")}
|
||||||
class="flex-1 min-w-[70px] px-3 py-2 rounded-lg border transition-colors {config.theme === 'dark'
|
class="flex-1 min-w-[70px] px-3 py-2 rounded-lg border transition-colors {config.theme ===
|
||||||
|
'dark'
|
||||||
? 'bg-[var(--accent-primary)] border-[var(--accent-primary)] text-white'
|
? 'bg-[var(--accent-primary)] border-[var(--accent-primary)] text-white'
|
||||||
: 'bg-[var(--bg-primary)] border-[var(--border-color)] text-[var(--text-secondary)] hover:border-[var(--accent-primary)]'}"
|
: 'bg-[var(--bg-primary)] border-[var(--border-color)] text-[var(--text-secondary)] hover:border-[var(--accent-primary)]'}"
|
||||||
>
|
>
|
||||||
@@ -470,7 +471,8 @@
|
|||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onclick={() => handleThemeChange("light")}
|
onclick={() => handleThemeChange("light")}
|
||||||
class="flex-1 min-w-[70px] px-3 py-2 rounded-lg border transition-colors {config.theme === 'light'
|
class="flex-1 min-w-[70px] px-3 py-2 rounded-lg border transition-colors {config.theme ===
|
||||||
|
'light'
|
||||||
? 'bg-[var(--accent-primary)] border-[var(--accent-primary)] text-white'
|
? 'bg-[var(--accent-primary)] border-[var(--accent-primary)] text-white'
|
||||||
: 'bg-[var(--bg-primary)] border-[var(--border-color)] text-[var(--text-secondary)] hover:border-[var(--accent-primary)]'}"
|
: 'bg-[var(--bg-primary)] border-[var(--border-color)] text-[var(--text-secondary)] hover:border-[var(--accent-primary)]'}"
|
||||||
>
|
>
|
||||||
@@ -488,7 +490,8 @@
|
|||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onclick={() => handleThemeChange("custom")}
|
onclick={() => handleThemeChange("custom")}
|
||||||
class="flex-1 min-w-[70px] px-3 py-2 rounded-lg border transition-colors {config.theme === 'custom'
|
class="flex-1 min-w-[70px] px-3 py-2 rounded-lg border transition-colors {config.theme ===
|
||||||
|
'custom'
|
||||||
? 'bg-[var(--accent-primary)] border-[var(--accent-primary)] text-white'
|
? 'bg-[var(--accent-primary)] border-[var(--accent-primary)] text-white'
|
||||||
: 'bg-[var(--bg-primary)] border-[var(--border-color)] text-[var(--text-secondary)] hover:border-[var(--accent-primary)]'}"
|
: 'bg-[var(--bg-primary)] border-[var(--border-color)] text-[var(--text-secondary)] hover:border-[var(--accent-primary)]'}"
|
||||||
title="Create your own custom theme"
|
title="Create your own custom theme"
|
||||||
@@ -504,9 +507,12 @@
|
|||||||
<h4 class="text-sm font-medium text-[var(--text-primary)] mb-3">Custom Theme Colors</h4>
|
<h4 class="text-sm font-medium text-[var(--text-primary)] mb-3">Custom Theme Colors</h4>
|
||||||
<div class="grid grid-cols-2 gap-3">
|
<div class="grid grid-cols-2 gap-3">
|
||||||
<div class="color-input-group">
|
<div class="color-input-group">
|
||||||
<label class="text-xs text-[var(--text-secondary)]">Background</label>
|
<label class="text-xs text-[var(--text-secondary)]" for="color-bg-primary"
|
||||||
|
>Background</label
|
||||||
|
>
|
||||||
<div class="flex gap-2 items-center">
|
<div class="flex gap-2 items-center">
|
||||||
<input
|
<input
|
||||||
|
id="color-bg-primary"
|
||||||
type="color"
|
type="color"
|
||||||
value={config.custom_theme_colors.bg_primary || defaultDarkColors.bg_primary}
|
value={config.custom_theme_colors.bg_primary || defaultDarkColors.bg_primary}
|
||||||
oninput={(e) => handleCustomColorChange("bg_primary", e.currentTarget.value)}
|
oninput={(e) => handleCustomColorChange("bg_primary", e.currentTarget.value)}
|
||||||
@@ -518,9 +524,12 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="color-input-group">
|
<div class="color-input-group">
|
||||||
<label class="text-xs text-[var(--text-secondary)]">Secondary BG</label>
|
<label class="text-xs text-[var(--text-secondary)]" for="color-bg-secondary"
|
||||||
|
>Secondary BG</label
|
||||||
|
>
|
||||||
<div class="flex gap-2 items-center">
|
<div class="flex gap-2 items-center">
|
||||||
<input
|
<input
|
||||||
|
id="color-bg-secondary"
|
||||||
type="color"
|
type="color"
|
||||||
value={config.custom_theme_colors.bg_secondary || defaultDarkColors.bg_secondary}
|
value={config.custom_theme_colors.bg_secondary || defaultDarkColors.bg_secondary}
|
||||||
oninput={(e) => handleCustomColorChange("bg_secondary", e.currentTarget.value)}
|
oninput={(e) => handleCustomColorChange("bg_secondary", e.currentTarget.value)}
|
||||||
@@ -532,9 +541,12 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="color-input-group">
|
<div class="color-input-group">
|
||||||
<label class="text-xs text-[var(--text-secondary)]">Terminal BG</label>
|
<label class="text-xs text-[var(--text-secondary)]" for="color-bg-terminal"
|
||||||
|
>Terminal BG</label
|
||||||
|
>
|
||||||
<div class="flex gap-2 items-center">
|
<div class="flex gap-2 items-center">
|
||||||
<input
|
<input
|
||||||
|
id="color-bg-terminal"
|
||||||
type="color"
|
type="color"
|
||||||
value={config.custom_theme_colors.bg_terminal || defaultDarkColors.bg_terminal}
|
value={config.custom_theme_colors.bg_terminal || defaultDarkColors.bg_terminal}
|
||||||
oninput={(e) => handleCustomColorChange("bg_terminal", e.currentTarget.value)}
|
oninput={(e) => handleCustomColorChange("bg_terminal", e.currentTarget.value)}
|
||||||
@@ -546,9 +558,10 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="color-input-group">
|
<div class="color-input-group">
|
||||||
<label class="text-xs text-[var(--text-secondary)]">Border</label>
|
<label class="text-xs text-[var(--text-secondary)]" for="color-border">Border</label>
|
||||||
<div class="flex gap-2 items-center">
|
<div class="flex gap-2 items-center">
|
||||||
<input
|
<input
|
||||||
|
id="color-border"
|
||||||
type="color"
|
type="color"
|
||||||
value={config.custom_theme_colors.border_color || defaultDarkColors.border_color}
|
value={config.custom_theme_colors.border_color || defaultDarkColors.border_color}
|
||||||
oninput={(e) => handleCustomColorChange("border_color", e.currentTarget.value)}
|
oninput={(e) => handleCustomColorChange("border_color", e.currentTarget.value)}
|
||||||
@@ -560,11 +573,15 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="color-input-group">
|
<div class="color-input-group">
|
||||||
<label class="text-xs text-[var(--text-secondary)]">Accent Primary</label>
|
<label class="text-xs text-[var(--text-secondary)]" for="color-accent-primary"
|
||||||
|
>Accent Primary</label
|
||||||
|
>
|
||||||
<div class="flex gap-2 items-center">
|
<div class="flex gap-2 items-center">
|
||||||
<input
|
<input
|
||||||
|
id="color-accent-primary"
|
||||||
type="color"
|
type="color"
|
||||||
value={config.custom_theme_colors.accent_primary || defaultDarkColors.accent_primary}
|
value={config.custom_theme_colors.accent_primary ||
|
||||||
|
defaultDarkColors.accent_primary}
|
||||||
oninput={(e) => handleCustomColorChange("accent_primary", e.currentTarget.value)}
|
oninput={(e) => handleCustomColorChange("accent_primary", e.currentTarget.value)}
|
||||||
class="color-picker"
|
class="color-picker"
|
||||||
/>
|
/>
|
||||||
@@ -574,23 +591,32 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="color-input-group">
|
<div class="color-input-group">
|
||||||
<label class="text-xs text-[var(--text-secondary)]">Accent Secondary</label>
|
<label class="text-xs text-[var(--text-secondary)]" for="color-accent-secondary"
|
||||||
|
>Accent Secondary</label
|
||||||
|
>
|
||||||
<div class="flex gap-2 items-center">
|
<div class="flex gap-2 items-center">
|
||||||
<input
|
<input
|
||||||
|
id="color-accent-secondary"
|
||||||
type="color"
|
type="color"
|
||||||
value={config.custom_theme_colors.accent_secondary || defaultDarkColors.accent_secondary}
|
value={config.custom_theme_colors.accent_secondary ||
|
||||||
oninput={(e) => handleCustomColorChange("accent_secondary", e.currentTarget.value)}
|
defaultDarkColors.accent_secondary}
|
||||||
|
oninput={(e) =>
|
||||||
|
handleCustomColorChange("accent_secondary", e.currentTarget.value)}
|
||||||
class="color-picker"
|
class="color-picker"
|
||||||
/>
|
/>
|
||||||
<span class="text-xs text-[var(--text-tertiary)] font-mono">
|
<span class="text-xs text-[var(--text-tertiary)] font-mono">
|
||||||
{config.custom_theme_colors.accent_secondary || defaultDarkColors.accent_secondary}
|
{config.custom_theme_colors.accent_secondary ||
|
||||||
|
defaultDarkColors.accent_secondary}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="color-input-group">
|
<div class="color-input-group">
|
||||||
<label class="text-xs text-[var(--text-secondary)]">Text Primary</label>
|
<label class="text-xs text-[var(--text-secondary)]" for="color-text-primary"
|
||||||
|
>Text Primary</label
|
||||||
|
>
|
||||||
<div class="flex gap-2 items-center">
|
<div class="flex gap-2 items-center">
|
||||||
<input
|
<input
|
||||||
|
id="color-text-primary"
|
||||||
type="color"
|
type="color"
|
||||||
value={config.custom_theme_colors.text_primary || defaultDarkColors.text_primary}
|
value={config.custom_theme_colors.text_primary || defaultDarkColors.text_primary}
|
||||||
oninput={(e) => handleCustomColorChange("text_primary", e.currentTarget.value)}
|
oninput={(e) => handleCustomColorChange("text_primary", e.currentTarget.value)}
|
||||||
@@ -602,11 +628,15 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="color-input-group">
|
<div class="color-input-group">
|
||||||
<label class="text-xs text-[var(--text-secondary)]">Text Secondary</label>
|
<label class="text-xs text-[var(--text-secondary)]" for="color-text-secondary"
|
||||||
|
>Text Secondary</label
|
||||||
|
>
|
||||||
<div class="flex gap-2 items-center">
|
<div class="flex gap-2 items-center">
|
||||||
<input
|
<input
|
||||||
|
id="color-text-secondary"
|
||||||
type="color"
|
type="color"
|
||||||
value={config.custom_theme_colors.text_secondary || defaultDarkColors.text_secondary}
|
value={config.custom_theme_colors.text_secondary ||
|
||||||
|
defaultDarkColors.text_secondary}
|
||||||
oninput={(e) => handleCustomColorChange("text_secondary", e.currentTarget.value)}
|
oninput={(e) => handleCustomColorChange("text_secondary", e.currentTarget.value)}
|
||||||
class="color-picker"
|
class="color-picker"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -279,8 +279,15 @@
|
|||||||
<svelte:window on:keydown={handleKeydown} />
|
<svelte:window on:keydown={handleKeydown} />
|
||||||
|
|
||||||
{#if isOpen}
|
{#if isOpen}
|
||||||
|
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
||||||
<div class="git-panel-overlay" on:click={onClose} role="presentation">
|
<div class="git-panel-overlay" on:click={onClose} role="presentation">
|
||||||
<div class="git-panel" on:click|stopPropagation role="dialog" aria-label="Git Panel">
|
<div
|
||||||
|
class="git-panel"
|
||||||
|
on:click|stopPropagation
|
||||||
|
role="dialog"
|
||||||
|
aria-label="Git Panel"
|
||||||
|
tabindex="-1"
|
||||||
|
>
|
||||||
<div class="git-panel-header">
|
<div class="git-panel-header">
|
||||||
<h2>🔀 Git</h2>
|
<h2>🔀 Git</h2>
|
||||||
<button class="close-btn" on:click={onClose} title="Close (Esc)">✕</button>
|
<button class="close-btn" on:click={onClose} title="Close (Esc)">✕</button>
|
||||||
@@ -580,8 +587,15 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if showDiff}
|
{#if showDiff}
|
||||||
|
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
||||||
<div class="diff-overlay" on:click={() => (showDiff = false)} role="presentation">
|
<div class="diff-overlay" on:click={() => (showDiff = false)} role="presentation">
|
||||||
<div class="diff-modal" on:click|stopPropagation role="dialog" aria-label="Diff View">
|
<div
|
||||||
|
class="diff-modal"
|
||||||
|
on:click|stopPropagation
|
||||||
|
role="dialog"
|
||||||
|
aria-label="Diff View"
|
||||||
|
tabindex="-1"
|
||||||
|
>
|
||||||
<div class="diff-header">
|
<div class="diff-header">
|
||||||
<h3>📄 {diffFile}</h3>
|
<h3>📄 {diffFile}</h3>
|
||||||
<button on:click={() => (showDiff = false)} title="Close">✕</button>
|
<button on:click={() => (showDiff = false)} title="Close">✕</button>
|
||||||
@@ -938,10 +952,6 @@
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.new-branch-input button:first-of-type {
|
|
||||||
/* Styling handled by btn-trans-gradient class */
|
|
||||||
}
|
|
||||||
|
|
||||||
.new-branch-input button:last-of-type {
|
.new-branch-input button:last-of-type {
|
||||||
background: var(--bg-tertiary);
|
background: var(--bg-tertiary);
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
|
|||||||
@@ -39,11 +39,12 @@
|
|||||||
|
|
||||||
let isGeneratingImage = false;
|
let isGeneratingImage = false;
|
||||||
|
|
||||||
$: unlockedCount = Object.values($achievementsStore.achievements).filter((a) => a.unlocked).length;
|
$: unlockedCount = Object.values($achievementsStore.achievements).filter(
|
||||||
|
(a) => a.unlocked
|
||||||
|
).length;
|
||||||
$: totalAchievements = Object.values($achievementsStore.achievements).length;
|
$: totalAchievements = Object.values($achievementsStore.achievements).length;
|
||||||
$: achievementPercentage = totalAchievements > 0
|
$: achievementPercentage =
|
||||||
? Math.round((unlockedCount / totalAchievements) * 100)
|
totalAchievements > 0 ? Math.round((unlockedCount / totalAchievements) * 100) : 0;
|
||||||
: 0;
|
|
||||||
|
|
||||||
async function selectAvatar() {
|
async function selectAvatar() {
|
||||||
try {
|
try {
|
||||||
@@ -82,11 +83,15 @@
|
|||||||
async function loadAvatarAsDataUrl(path: string): Promise<string | null> {
|
async function loadAvatarAsDataUrl(path: string): Promise<string | null> {
|
||||||
try {
|
try {
|
||||||
const data = await readFile(path);
|
const data = await readFile(path);
|
||||||
const extension = path.split('.').pop()?.toLowerCase() || 'png';
|
const extension = path.split(".").pop()?.toLowerCase() || "png";
|
||||||
const mimeType = extension === 'jpg' || extension === 'jpeg' ? 'image/jpeg'
|
const mimeType =
|
||||||
: extension === 'gif' ? 'image/gif'
|
extension === "jpg" || extension === "jpeg"
|
||||||
: extension === 'webp' ? 'image/webp'
|
? "image/jpeg"
|
||||||
: 'image/png';
|
: extension === "gif"
|
||||||
|
? "image/gif"
|
||||||
|
: extension === "webp"
|
||||||
|
? "image/webp"
|
||||||
|
: "image/png";
|
||||||
// Convert Uint8Array to base64 in chunks to avoid stack overflow
|
// Convert Uint8Array to base64 in chunks to avoid stack overflow
|
||||||
const blob = new Blob([data], { type: mimeType });
|
const blob = new Blob([data], { type: mimeType });
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
@@ -123,7 +128,7 @@
|
|||||||
const stripeColors = ["#5bcefa", "#f5a9b8", "#ffffff", "#f5a9b8", "#5bcefa"];
|
const stripeColors = ["#5bcefa", "#f5a9b8", "#ffffff", "#f5a9b8", "#5bcefa"];
|
||||||
stripeColors.forEach((color, i) => {
|
stripeColors.forEach((color, i) => {
|
||||||
ctx.fillStyle = color;
|
ctx.fillStyle = color;
|
||||||
ctx.fillRect(0, i * (stripeHeight / 5) * 2, width, stripeHeight / 5 * 2);
|
ctx.fillRect(0, i * (stripeHeight / 5) * 2, width, (stripeHeight / 5) * 2);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Border
|
// Border
|
||||||
@@ -154,7 +159,13 @@
|
|||||||
img.onerror = () => reject();
|
img.onerror = () => reject();
|
||||||
img.src = dataUrl;
|
img.src = dataUrl;
|
||||||
});
|
});
|
||||||
ctx.drawImage(img, avatarX - avatarRadius, avatarY - avatarRadius, avatarRadius * 2, avatarRadius * 2);
|
ctx.drawImage(
|
||||||
|
img,
|
||||||
|
avatarX - avatarRadius,
|
||||||
|
avatarY - avatarRadius,
|
||||||
|
avatarRadius * 2,
|
||||||
|
avatarRadius * 2
|
||||||
|
);
|
||||||
avatarLoaded = true;
|
avatarLoaded = true;
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
@@ -163,12 +174,22 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!avatarLoaded) {
|
if (!avatarLoaded) {
|
||||||
const avatarGradient = ctx.createLinearGradient(avatarX - avatarRadius, avatarY - avatarRadius, avatarX + avatarRadius, avatarY + avatarRadius);
|
const avatarGradient = ctx.createLinearGradient(
|
||||||
|
avatarX - avatarRadius,
|
||||||
|
avatarY - avatarRadius,
|
||||||
|
avatarX + avatarRadius,
|
||||||
|
avatarY + avatarRadius
|
||||||
|
);
|
||||||
avatarGradient.addColorStop(0, "#5bcefa");
|
avatarGradient.addColorStop(0, "#5bcefa");
|
||||||
avatarGradient.addColorStop(0.5, "#f5a9b8");
|
avatarGradient.addColorStop(0.5, "#f5a9b8");
|
||||||
avatarGradient.addColorStop(1, "#5bcefa");
|
avatarGradient.addColorStop(1, "#5bcefa");
|
||||||
ctx.fillStyle = avatarGradient;
|
ctx.fillStyle = avatarGradient;
|
||||||
ctx.fillRect(avatarX - avatarRadius, avatarY - avatarRadius, avatarRadius * 2, avatarRadius * 2);
|
ctx.fillRect(
|
||||||
|
avatarX - avatarRadius,
|
||||||
|
avatarY - avatarRadius,
|
||||||
|
avatarRadius * 2,
|
||||||
|
avatarRadius * 2
|
||||||
|
);
|
||||||
}
|
}
|
||||||
ctx.restore();
|
ctx.restore();
|
||||||
|
|
||||||
@@ -260,13 +281,23 @@
|
|||||||
progressGradient.addColorStop(1, "#5bcefa");
|
progressGradient.addColorStop(1, "#5bcefa");
|
||||||
ctx.fillStyle = progressGradient;
|
ctx.fillStyle = progressGradient;
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
ctx.roundRect(progressX, progressY, progressWidth * (achievementPercentage / 100), progressHeight, 22);
|
ctx.roundRect(
|
||||||
|
progressX,
|
||||||
|
progressY,
|
||||||
|
progressWidth * (achievementPercentage / 100),
|
||||||
|
progressHeight,
|
||||||
|
22
|
||||||
|
);
|
||||||
ctx.fill();
|
ctx.fill();
|
||||||
|
|
||||||
// Progress text
|
// Progress text
|
||||||
ctx.fillStyle = "#ffffff";
|
ctx.fillStyle = "#ffffff";
|
||||||
ctx.font = "bold 36px system-ui, -apple-system, sans-serif";
|
ctx.font = "bold 36px system-ui, -apple-system, sans-serif";
|
||||||
ctx.fillText(`${unlockedCount} / ${totalAchievements} (${achievementPercentage}%)`, progressX + progressWidth + 40, progressY + 34);
|
ctx.fillText(
|
||||||
|
`${unlockedCount} / ${totalAchievements} (${achievementPercentage}%)`,
|
||||||
|
progressX + progressWidth + 40,
|
||||||
|
progressY + 34
|
||||||
|
);
|
||||||
|
|
||||||
// Hikari branding
|
// Hikari branding
|
||||||
ctx.fillStyle = "#f5a9b8";
|
ctx.fillStyle = "#f5a9b8";
|
||||||
@@ -312,7 +343,14 @@
|
|||||||
<div class="profile-header">
|
<div class="profile-header">
|
||||||
<h2>Profile</h2>
|
<h2>Profile</h2>
|
||||||
<button class="close-btn" on:click={onClose} aria-label="Close profile">
|
<button class="close-btn" on:click={onClose} aria-label="Close profile">
|
||||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
<svg
|
||||||
|
width="20"
|
||||||
|
height="20"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
>
|
||||||
<path d="M18 6L6 18M6 6l12 12" />
|
<path d="M18 6L6 18M6 6l12 12" />
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
@@ -321,7 +359,13 @@
|
|||||||
<div class="profile-content">
|
<div class="profile-content">
|
||||||
<!-- Avatar Section -->
|
<!-- Avatar Section -->
|
||||||
<div class="avatar-section">
|
<div class="avatar-section">
|
||||||
<div class="avatar-container" on:click={selectAvatar} role="button" tabindex="0" on:keydown={(e) => e.key === 'Enter' && selectAvatar()}>
|
<div
|
||||||
|
class="avatar-container"
|
||||||
|
on:click={selectAvatar}
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
on:keydown={(e) => e.key === "Enter" && selectAvatar()}
|
||||||
|
>
|
||||||
{#if avatarDataUrl}
|
{#if avatarDataUrl}
|
||||||
<img src={avatarDataUrl} alt="Profile avatar" class="avatar-image" />
|
<img src={avatarDataUrl} alt="Profile avatar" class="avatar-image" />
|
||||||
<div class="avatar-overlay">
|
<div class="avatar-overlay">
|
||||||
@@ -329,7 +373,14 @@
|
|||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<div class="avatar-placeholder">
|
<div class="avatar-placeholder">
|
||||||
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
<svg
|
||||||
|
width="48"
|
||||||
|
height="48"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="1.5"
|
||||||
|
>
|
||||||
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2" />
|
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2" />
|
||||||
<circle cx="12" cy="7" r="4" />
|
<circle cx="12" cy="7" r="4" />
|
||||||
</svg>
|
</svg>
|
||||||
@@ -350,13 +401,26 @@
|
|||||||
bind:value={nameInput}
|
bind:value={nameInput}
|
||||||
placeholder="Enter your name"
|
placeholder="Enter your name"
|
||||||
class="name-input"
|
class="name-input"
|
||||||
on:keydown={(e) => e.key === 'Enter' && saveName()}
|
on:keydown={(e) => e.key === "Enter" && saveName()}
|
||||||
on:blur={saveName}
|
on:blur={saveName}
|
||||||
/>
|
/>
|
||||||
{:else}
|
{:else}
|
||||||
<button class="name-display" on:click={() => { editingName = true; nameInput = config.profile_name || ""; }}>
|
<button
|
||||||
|
class="name-display"
|
||||||
|
on:click={() => {
|
||||||
|
editingName = true;
|
||||||
|
nameInput = config.profile_name || "";
|
||||||
|
}}
|
||||||
|
>
|
||||||
<span class="name-text">{config.profile_name || "Click to add name"}</span>
|
<span class="name-text">{config.profile_name || "Click to add name"}</span>
|
||||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
<svg
|
||||||
|
width="16"
|
||||||
|
height="16"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
>
|
||||||
<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7" />
|
<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7" />
|
||||||
<path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z" />
|
<path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z" />
|
||||||
</svg>
|
</svg>
|
||||||
@@ -377,7 +441,13 @@
|
|||||||
></textarea>
|
></textarea>
|
||||||
<button class="save-bio-btn btn-trans-gradient" on:click={saveBio}>Save</button>
|
<button class="save-bio-btn btn-trans-gradient" on:click={saveBio}>Save</button>
|
||||||
{:else}
|
{:else}
|
||||||
<button class="bio-display" on:click={() => { editingBio = true; bioInput = config.profile_bio || ""; }}>
|
<button
|
||||||
|
class="bio-display"
|
||||||
|
on:click={() => {
|
||||||
|
editingBio = true;
|
||||||
|
bioInput = config.profile_bio || "";
|
||||||
|
}}
|
||||||
|
>
|
||||||
<span class="bio-text">{config.profile_bio || "Click to add a bio..."}</span>
|
<span class="bio-text">{config.profile_bio || "Click to add a bio..."}</span>
|
||||||
</button>
|
</button>
|
||||||
{/if}
|
{/if}
|
||||||
@@ -421,18 +491,31 @@
|
|||||||
<div class="progress-bar">
|
<div class="progress-bar">
|
||||||
<div class="progress-fill" style="width: {achievementPercentage}%"></div>
|
<div class="progress-fill" style="width: {achievementPercentage}%"></div>
|
||||||
</div>
|
</div>
|
||||||
<span class="progress-text">{unlockedCount} / {totalAchievements} ({achievementPercentage}%)</span>
|
<span class="progress-text"
|
||||||
|
>{unlockedCount} / {totalAchievements} ({achievementPercentage}%)</span
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Share Section -->
|
<!-- Share Section -->
|
||||||
<div class="share-section">
|
<div class="share-section">
|
||||||
<button class="share-btn btn-trans-gradient" on:click={shareProfile} disabled={isGeneratingImage}>
|
<button
|
||||||
|
class="share-btn btn-trans-gradient"
|
||||||
|
on:click={shareProfile}
|
||||||
|
disabled={isGeneratingImage}
|
||||||
|
>
|
||||||
{#if isGeneratingImage}
|
{#if isGeneratingImage}
|
||||||
<span class="spinner"></span>
|
<span class="spinner"></span>
|
||||||
Generating...
|
Generating...
|
||||||
{:else}
|
{:else}
|
||||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
<svg
|
||||||
|
width="18"
|
||||||
|
height="18"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
>
|
||||||
<path d="M4 12v8a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-8" />
|
<path d="M4 12v8a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-8" />
|
||||||
<polyline points="16 6 12 2 8 6" />
|
<polyline points="16 6 12 2 8 6" />
|
||||||
<line x1="12" y1="2" x2="12" y2="15" />
|
<line x1="12" y1="2" x2="12" y2="15" />
|
||||||
@@ -761,7 +844,14 @@
|
|||||||
|
|
||||||
.progress-fill {
|
.progress-fill {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
background: linear-gradient(90deg, var(--trans-blue), var(--trans-pink), var(--trans-white), var(--trans-pink), var(--trans-blue));
|
background: linear-gradient(
|
||||||
|
90deg,
|
||||||
|
var(--trans-blue),
|
||||||
|
var(--trans-pink),
|
||||||
|
var(--trans-white),
|
||||||
|
var(--trans-pink),
|
||||||
|
var(--trans-blue)
|
||||||
|
);
|
||||||
background-size: 200% 100%;
|
background-size: 200% 100%;
|
||||||
animation: shimmer 3s linear infinite;
|
animation: shimmer 3s linear infinite;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
@@ -769,8 +859,12 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
@keyframes shimmer {
|
@keyframes shimmer {
|
||||||
0% { background-position: 200% 0; }
|
0% {
|
||||||
100% { background-position: -200% 0; }
|
background-position: 200% 0;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
background-position: -200% 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.progress-text {
|
.progress-text {
|
||||||
@@ -828,6 +922,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
@keyframes spin {
|
@keyframes spin {
|
||||||
to { transform: rotate(360deg); }
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -449,6 +449,7 @@
|
|||||||
.line-clamp-2 {
|
.line-clamp-2 {
|
||||||
display: -webkit-box;
|
display: -webkit-box;
|
||||||
-webkit-line-clamp: 2;
|
-webkit-line-clamp: 2;
|
||||||
|
line-clamp: 2;
|
||||||
-webkit-box-orient: vertical;
|
-webkit-box-orient: vertical;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -469,6 +469,7 @@
|
|||||||
.line-clamp-2 {
|
.line-clamp-2 {
|
||||||
display: -webkit-box;
|
display: -webkit-box;
|
||||||
-webkit-line-clamp: 2;
|
-webkit-line-clamp: 2;
|
||||||
|
line-clamp: 2;
|
||||||
-webkit-box-orient: vertical;
|
-webkit-box-orient: vertical;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -460,6 +460,7 @@
|
|||||||
.line-clamp-2 {
|
.line-clamp-2 {
|
||||||
display: -webkit-box;
|
display: -webkit-box;
|
||||||
-webkit-line-clamp: 2;
|
-webkit-line-clamp: 2;
|
||||||
|
line-clamp: 2;
|
||||||
-webkit-box-orient: vertical;
|
-webkit-box-orient: vertical;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -119,14 +119,6 @@
|
|||||||
opacity: 0.8;
|
opacity: 0.8;
|
||||||
}
|
}
|
||||||
|
|
||||||
.stat-highlight {
|
|
||||||
font-weight: 600;
|
|
||||||
color: var(--accent-primary);
|
|
||||||
margin-top: 0.25rem;
|
|
||||||
padding-top: 0.25rem;
|
|
||||||
border-top: 1px solid var(--border-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.stat-label {
|
.stat-label {
|
||||||
color: var(--text-secondary, #9ca3af);
|
color: var(--text-secondary, #9ca3af);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
hasQuestionPending.subscribe((pending) => {
|
hasQuestionPending.subscribe((pending) => {
|
||||||
isVisible = pending;
|
isVisible = pending;
|
||||||
if (!pending) {
|
if (!pending) {
|
||||||
selectedOptions = new SvelteSet();
|
selectedOptions.clear();
|
||||||
customAnswer = "";
|
customAnswer = "";
|
||||||
showCustomInput = false;
|
showCustomInput = false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1502,12 +1502,25 @@ export const achievementCategories = [
|
|||||||
{
|
{
|
||||||
name: "Token Milestones",
|
name: "Token Milestones",
|
||||||
description: "Track your token generation progress",
|
description: "Track your token generation progress",
|
||||||
ids: ["FirstSteps", "GrowingStrong", "BlossomingCoder", "TokenMaster", "TokenBillionaire", "TokenTreasure"] as AchievementId[],
|
ids: [
|
||||||
|
"FirstSteps",
|
||||||
|
"GrowingStrong",
|
||||||
|
"BlossomingCoder",
|
||||||
|
"TokenMaster",
|
||||||
|
"TokenBillionaire",
|
||||||
|
"TokenTreasure",
|
||||||
|
] as AchievementId[],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Code Generation",
|
name: "Code Generation",
|
||||||
description: "Achievements for generating code",
|
description: "Achievements for generating code",
|
||||||
ids: ["HelloWorld", "CodeWizard", "ThousandBlocks", "CodeFactory", "CodeEmpire"] as AchievementId[],
|
ids: [
|
||||||
|
"HelloWorld",
|
||||||
|
"CodeWizard",
|
||||||
|
"ThousandBlocks",
|
||||||
|
"CodeFactory",
|
||||||
|
"CodeEmpire",
|
||||||
|
] as AchievementId[],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "File Operations",
|
name: "File Operations",
|
||||||
@@ -1517,7 +1530,13 @@ export const achievementCategories = [
|
|||||||
{
|
{
|
||||||
name: "Conversations",
|
name: "Conversations",
|
||||||
description: "Building our relationship through chat",
|
description: "Building our relationship through chat",
|
||||||
ids: ["ConversationStarter", "ChattyKathy", "Conversationalist", "ChatMarathon", "ChatLegend"] as AchievementId[],
|
ids: [
|
||||||
|
"ConversationStarter",
|
||||||
|
"ChattyKathy",
|
||||||
|
"Conversationalist",
|
||||||
|
"ChatMarathon",
|
||||||
|
"ChatLegend",
|
||||||
|
] as AchievementId[],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Tools & Skills",
|
name: "Tools & Skills",
|
||||||
@@ -1556,7 +1575,16 @@ export const achievementCategories = [
|
|||||||
{
|
{
|
||||||
name: "Relationship & Greetings",
|
name: "Relationship & Greetings",
|
||||||
description: "Our special moments together",
|
description: "Our special moments together",
|
||||||
ids: ["GoodMorning", "GoodNight", "ThankYou", "LoveYou", "HelloHikari", "HowAreYou", "MissedYou", "BackAgain"] as AchievementId[],
|
ids: [
|
||||||
|
"GoodMorning",
|
||||||
|
"GoodNight",
|
||||||
|
"ThankYou",
|
||||||
|
"LoveYou",
|
||||||
|
"HelloHikari",
|
||||||
|
"HowAreYou",
|
||||||
|
"MissedYou",
|
||||||
|
"BackAgain",
|
||||||
|
] as AchievementId[],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Personality & Fun",
|
name: "Personality & Fun",
|
||||||
@@ -1571,7 +1599,16 @@ export const achievementCategories = [
|
|||||||
{
|
{
|
||||||
name: "Git & Development",
|
name: "Git & Development",
|
||||||
description: "Version control mastery",
|
description: "Version control mastery",
|
||||||
ids: ["GitGuru", "TestWriter", "Debugger", "CommitKing", "CommitLegend", "BranchMaster", "MergeExpert", "ConflictResolver"] as AchievementId[],
|
ids: [
|
||||||
|
"GitGuru",
|
||||||
|
"TestWriter",
|
||||||
|
"Debugger",
|
||||||
|
"CommitKing",
|
||||||
|
"CommitLegend",
|
||||||
|
"BranchMaster",
|
||||||
|
"MergeExpert",
|
||||||
|
"ConflictResolver",
|
||||||
|
] as AchievementId[],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Tool Mastery",
|
name: "Tool Mastery",
|
||||||
@@ -1606,17 +1643,41 @@ export const achievementCategories = [
|
|||||||
{
|
{
|
||||||
name: "Seasonal",
|
name: "Seasonal",
|
||||||
description: "Holiday coding!",
|
description: "Holiday coding!",
|
||||||
ids: ["NewYearCoder", "ValentinesDev", "SpookyCode", "HolidayCoder", "LeapDayCoder"] as AchievementId[],
|
ids: [
|
||||||
|
"NewYearCoder",
|
||||||
|
"ValentinesDev",
|
||||||
|
"SpookyCode",
|
||||||
|
"HolidayCoder",
|
||||||
|
"LeapDayCoder",
|
||||||
|
] as AchievementId[],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Message Content",
|
name: "Message Content",
|
||||||
description: "How you communicate",
|
description: "How you communicate",
|
||||||
ids: ["LongMessage", "NovelWriter", "ShortAndSweet", "CodeInMessage", "MarkdownMaster"] as AchievementId[],
|
ids: [
|
||||||
|
"LongMessage",
|
||||||
|
"NovelWriter",
|
||||||
|
"ShortAndSweet",
|
||||||
|
"CodeInMessage",
|
||||||
|
"MarkdownMaster",
|
||||||
|
] as AchievementId[],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Programming Languages",
|
name: "Programming Languages",
|
||||||
description: "Languages you've used",
|
description: "Languages you've used",
|
||||||
ids: ["RustDeveloper", "PythonDeveloper", "JavaScriptDev", "TypeScriptDev", "GoDeveloper", "CppDeveloper", "JavaDeveloper", "HtmlCssDev", "SqlDeveloper", "ShellScripter", "FullStackDev"] as AchievementId[],
|
ids: [
|
||||||
|
"RustDeveloper",
|
||||||
|
"PythonDeveloper",
|
||||||
|
"JavaScriptDev",
|
||||||
|
"TypeScriptDev",
|
||||||
|
"GoDeveloper",
|
||||||
|
"CppDeveloper",
|
||||||
|
"JavaDeveloper",
|
||||||
|
"HtmlCssDev",
|
||||||
|
"SqlDeveloper",
|
||||||
|
"ShellScripter",
|
||||||
|
"FullStackDev",
|
||||||
|
] as AchievementId[],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Project Types",
|
name: "Project Types",
|
||||||
@@ -1661,12 +1722,23 @@ export const achievementCategories = [
|
|||||||
{
|
{
|
||||||
name: "UI Exploration",
|
name: "UI Exploration",
|
||||||
description: "Explore Hikari's features",
|
description: "Explore Hikari's features",
|
||||||
ids: ["MultiTasker", "Minimalist", "PrivacyFirst", "ThemeChanger", "SettingsTweaker"] as AchievementId[],
|
ids: [
|
||||||
|
"MultiTasker",
|
||||||
|
"Minimalist",
|
||||||
|
"PrivacyFirst",
|
||||||
|
"ThemeChanger",
|
||||||
|
"SettingsTweaker",
|
||||||
|
] as AchievementId[],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Achievement Meta",
|
name: "Achievement Meta",
|
||||||
description: "Achievements about achievements",
|
description: "Achievements about achievements",
|
||||||
ids: ["AchievementHunter", "Completionist", "MasterUnlocker", "PlatinumStatus"] as AchievementId[],
|
ids: [
|
||||||
|
"AchievementHunter",
|
||||||
|
"Completionist",
|
||||||
|
"MasterUnlocker",
|
||||||
|
"PlatinumStatus",
|
||||||
|
] as AchievementId[],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Clipboard & Snippets",
|
name: "Clipboard & Snippets",
|
||||||
@@ -1676,7 +1748,14 @@ export const achievementCategories = [
|
|||||||
{
|
{
|
||||||
name: "Other Features",
|
name: "Other Features",
|
||||||
description: "Miscellaneous features",
|
description: "Miscellaneous features",
|
||||||
ids: ["QuickActionUser", "HistoryBuff", "Archivist", "SessionExporter", "GitPanelUser", "FeatureExplorer"] as AchievementId[],
|
ids: [
|
||||||
|
"QuickActionUser",
|
||||||
|
"HistoryBuff",
|
||||||
|
"Archivist",
|
||||||
|
"SessionExporter",
|
||||||
|
"GitPanelUser",
|
||||||
|
"FeatureExplorer",
|
||||||
|
] as AchievementId[],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Special",
|
name: "Special",
|
||||||
|
|||||||
@@ -236,7 +236,8 @@ export function applyCustomThemeColors(colors: CustomThemeColors) {
|
|||||||
if (colors.bg_secondary) root.style.setProperty("--bg-secondary", colors.bg_secondary);
|
if (colors.bg_secondary) root.style.setProperty("--bg-secondary", colors.bg_secondary);
|
||||||
if (colors.bg_terminal) root.style.setProperty("--bg-terminal", colors.bg_terminal);
|
if (colors.bg_terminal) root.style.setProperty("--bg-terminal", colors.bg_terminal);
|
||||||
if (colors.accent_primary) root.style.setProperty("--accent-primary", colors.accent_primary);
|
if (colors.accent_primary) root.style.setProperty("--accent-primary", colors.accent_primary);
|
||||||
if (colors.accent_secondary) root.style.setProperty("--accent-secondary", colors.accent_secondary);
|
if (colors.accent_secondary)
|
||||||
|
root.style.setProperty("--accent-secondary", colors.accent_secondary);
|
||||||
if (colors.text_primary) root.style.setProperty("--text-primary", colors.text_primary);
|
if (colors.text_primary) root.style.setProperty("--text-primary", colors.text_primary);
|
||||||
if (colors.text_secondary) root.style.setProperty("--text-secondary", colors.text_secondary);
|
if (colors.text_secondary) root.style.setProperty("--text-secondary", colors.text_secondary);
|
||||||
if (colors.border_color) root.style.setProperty("--border-color", colors.border_color);
|
if (colors.border_color) root.style.setProperty("--border-color", colors.border_color);
|
||||||
|
|||||||
@@ -24,7 +24,7 @@
|
|||||||
import UpdateNotification from "$lib/components/UpdateNotification.svelte";
|
import UpdateNotification from "$lib/components/UpdateNotification.svelte";
|
||||||
|
|
||||||
let initialized = false;
|
let initialized = false;
|
||||||
let updateNotification: UpdateNotification;
|
let updateNotification: UpdateNotification | undefined = $state(undefined);
|
||||||
let achievementPanelOpen = $state(false);
|
let achievementPanelOpen = $state(false);
|
||||||
let currentCharacterState: CharacterState = $state("idle");
|
let currentCharacterState: CharacterState = $state("idle");
|
||||||
let compactModeActive = $state(false);
|
let compactModeActive = $state(false);
|
||||||
@@ -375,6 +375,9 @@
|
|||||||
-webkit-mask:
|
-webkit-mask:
|
||||||
linear-gradient(#fff 0 0) content-box,
|
linear-gradient(#fff 0 0) content-box,
|
||||||
linear-gradient(#fff 0 0);
|
linear-gradient(#fff 0 0);
|
||||||
|
mask:
|
||||||
|
linear-gradient(#fff 0 0) content-box,
|
||||||
|
linear-gradient(#fff 0 0);
|
||||||
-webkit-mask-composite: xor;
|
-webkit-mask-composite: xor;
|
||||||
mask-composite: exclude;
|
mask-composite: exclude;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
|
|||||||
Reference in New Issue
Block a user