- FontSelector: text-[#8a8a8a]→text-text-sec, border-[#222]→border-border - StatsView: Canvas fillStyle and inline trend color #8a8a8a→#a8a8a8 - ActivityManager: border-[#222]→border-border, #f85149→#ff6b6b danger color - Settings: #f85149→#ff6b6b danger color on reset/delete buttons
582 lines
21 KiB
Svelte
582 lines
21 KiB
Svelte
<script lang="ts">
|
|
import { config, autoSave } from "../stores/config";
|
|
import { breakActivities, getCategoryLabel } from "../utils/activities";
|
|
import ToggleSwitch from "./ToggleSwitch.svelte";
|
|
import { pressable } from "../utils/animate";
|
|
|
|
const categories = ["eyes", "stretch", "breathing", "movement"] as const;
|
|
let expandedCategory = $state<string | null>(null);
|
|
let newActivityText = $state("");
|
|
let newActivityCategory = $state<string>("eyes");
|
|
let dropdownOpen = $state(false);
|
|
let dropdownRef = $state<HTMLElement>(undefined!);
|
|
let dropdownTriggerRef = $state<HTMLElement>(undefined!);
|
|
let focusedOptionIndex = $state(-1);
|
|
|
|
// Close dropdown on outside click
|
|
function handleOutsideClick(e: MouseEvent) {
|
|
if (dropdownRef && !dropdownRef.contains(e.target as Node)) {
|
|
dropdownOpen = false;
|
|
}
|
|
}
|
|
|
|
$effect(() => {
|
|
if (dropdownOpen) {
|
|
document.addEventListener("mousedown", handleOutsideClick);
|
|
// Set initial focused option to current selection
|
|
focusedOptionIndex = categories.indexOf(newActivityCategory as typeof categories[number]);
|
|
return () => document.removeEventListener("mousedown", handleOutsideClick);
|
|
}
|
|
});
|
|
|
|
// Focus the highlighted option when index changes
|
|
$effect(() => {
|
|
if (dropdownOpen && focusedOptionIndex >= 0) {
|
|
const options = dropdownRef?.querySelectorAll<HTMLElement>('[role="option"]');
|
|
options?.[focusedOptionIndex]?.focus();
|
|
}
|
|
});
|
|
|
|
function handleDropdownKeydown(e: KeyboardEvent) {
|
|
if (!dropdownOpen) {
|
|
// Open on ArrowDown/Up/Enter/Space when closed
|
|
if (["ArrowDown", "ArrowUp", "Enter", " "].includes(e.key)) {
|
|
e.preventDefault();
|
|
dropdownOpen = true;
|
|
}
|
|
return;
|
|
}
|
|
|
|
switch (e.key) {
|
|
case "Escape":
|
|
e.preventDefault();
|
|
dropdownOpen = false;
|
|
dropdownTriggerRef?.focus();
|
|
break;
|
|
case "ArrowDown":
|
|
e.preventDefault();
|
|
focusedOptionIndex = Math.min(focusedOptionIndex + 1, categories.length - 1);
|
|
break;
|
|
case "ArrowUp":
|
|
e.preventDefault();
|
|
focusedOptionIndex = Math.max(focusedOptionIndex - 1, 0);
|
|
break;
|
|
case "Home":
|
|
e.preventDefault();
|
|
focusedOptionIndex = 0;
|
|
break;
|
|
case "End":
|
|
e.preventDefault();
|
|
focusedOptionIndex = categories.length - 1;
|
|
break;
|
|
case "Enter":
|
|
case " ":
|
|
e.preventDefault();
|
|
if (focusedOptionIndex >= 0) {
|
|
newActivityCategory = categories[focusedOptionIndex];
|
|
dropdownOpen = false;
|
|
dropdownTriggerRef?.focus();
|
|
}
|
|
break;
|
|
case "Tab":
|
|
dropdownOpen = false;
|
|
break;
|
|
}
|
|
}
|
|
|
|
function markChanged() {
|
|
autoSave();
|
|
}
|
|
|
|
function toggleCategory(cat: string) {
|
|
expandedCategory = expandedCategory === cat ? null : cat;
|
|
}
|
|
|
|
function isBuiltinDisabled(text: string): boolean {
|
|
return $config.disabled_builtin_activities.includes(text);
|
|
}
|
|
|
|
function isBuiltinFavorite(text: string): boolean {
|
|
return $config.favorite_builtin_activities.includes(text);
|
|
}
|
|
|
|
function toggleBuiltinEnabled(text: string) {
|
|
if (isBuiltinDisabled(text)) {
|
|
$config.disabled_builtin_activities = $config.disabled_builtin_activities.filter((t) => t !== text);
|
|
} else {
|
|
$config.disabled_builtin_activities = [...$config.disabled_builtin_activities, text];
|
|
}
|
|
markChanged();
|
|
}
|
|
|
|
function toggleBuiltinFavorite(text: string) {
|
|
if (isBuiltinFavorite(text)) {
|
|
$config.favorite_builtin_activities = $config.favorite_builtin_activities.filter((t) => t !== text);
|
|
} else {
|
|
$config.favorite_builtin_activities = [...$config.favorite_builtin_activities, text];
|
|
}
|
|
markChanged();
|
|
}
|
|
|
|
function addCustomActivity() {
|
|
const trimmed = newActivityText.trim();
|
|
if (!trimmed || trimmed.length > 500) return;
|
|
if ($config.custom_activities.length >= 100) return;
|
|
|
|
const id = `custom-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`;
|
|
$config.custom_activities = [
|
|
...$config.custom_activities,
|
|
{ id, category: newActivityCategory, text: trimmed, is_favorite: false, enabled: true },
|
|
];
|
|
newActivityText = "";
|
|
markChanged();
|
|
}
|
|
|
|
function removeCustomActivity(id: string) {
|
|
$config.custom_activities = $config.custom_activities.filter((a) => a.id !== id);
|
|
markChanged();
|
|
}
|
|
|
|
function toggleCustomEnabled(id: string) {
|
|
$config.custom_activities = $config.custom_activities.map((a) =>
|
|
a.id === id ? { ...a, enabled: !a.enabled } : a,
|
|
);
|
|
markChanged();
|
|
}
|
|
|
|
function toggleCustomFavorite(id: string) {
|
|
$config.custom_activities = $config.custom_activities.map((a) =>
|
|
a.id === id ? { ...a, is_favorite: !a.is_favorite } : a,
|
|
);
|
|
markChanged();
|
|
}
|
|
|
|
// Get counts per category
|
|
function builtinCount(cat: string): number {
|
|
return breakActivities.filter((a) => a.category === cat).length;
|
|
}
|
|
function customCount(cat: string): number {
|
|
return $config.custom_activities.filter((a) => a.category === cat).length;
|
|
}
|
|
function disabledCount(cat: string): number {
|
|
return breakActivities
|
|
.filter((a) => a.category === cat)
|
|
.filter((a) => isBuiltinDisabled(a.text)).length
|
|
+ $config.custom_activities
|
|
.filter((a) => a.category === cat)
|
|
.filter((a) => !a.enabled).length;
|
|
}
|
|
|
|
// ── Svelte action: blocks mousedown from reaching parent dragScroll ──
|
|
// Uses native addEventListener (not Svelte delegation) so stopPropagation
|
|
// fires BEFORE the parent's node-level mousedown handler in bubble phase.
|
|
function blockParentDrag(node: HTMLElement) {
|
|
function stop(e: MouseEvent) { e.stopPropagation(); }
|
|
node.addEventListener("mousedown", stop);
|
|
return { destroy() { node.removeEventListener("mousedown", stop); } };
|
|
}
|
|
|
|
// ── Svelte action: inner drag-scroll with momentum, overscroll, custom scrollbar ──
|
|
const prefersReducedMotion = typeof window !== "undefined"
|
|
? window.matchMedia("(prefers-reduced-motion: reduce)").matches
|
|
: false;
|
|
|
|
function innerDragScroll(node: HTMLElement) {
|
|
const scrollEl = node.querySelector(".activity-scroll") as HTMLElement;
|
|
const content = scrollEl?.querySelector(".scroll-content") as HTMLElement;
|
|
if (!scrollEl || !content) return { destroy() {} };
|
|
|
|
// Reduced motion: skip custom scroll physics, allow normal scroll
|
|
if (prefersReducedMotion) {
|
|
// Still block wheel propagation for isolation
|
|
function onWheel(e: WheelEvent) {
|
|
e.stopPropagation();
|
|
const { scrollTop, scrollHeight, clientHeight } = scrollEl;
|
|
const atTop = scrollTop <= 0 && e.deltaY < 0;
|
|
const atBottom = scrollTop + clientHeight >= scrollHeight && e.deltaY > 0;
|
|
if (!atTop && !atBottom) {
|
|
e.preventDefault();
|
|
scrollEl.scrollTop += e.deltaY;
|
|
}
|
|
}
|
|
node.addEventListener("wheel", onWheel, { passive: false });
|
|
return { destroy() { node.removeEventListener("wheel", onWheel); } };
|
|
}
|
|
|
|
// Create custom scrollbar thumb
|
|
const thumb = document.createElement("div");
|
|
thumb.style.cssText =
|
|
"position:absolute;right:2px;top:0;width:3px;border-radius:1.5px;" +
|
|
"background:rgba(255,255,255,0.15);opacity:0;transition:opacity 0.3s ease;" +
|
|
"z-index:10;pointer-events:none;";
|
|
node.appendChild(thumb);
|
|
|
|
let hideTimer: ReturnType<typeof setTimeout> | null = null;
|
|
let coastFrame = 0;
|
|
let overscroll = 0;
|
|
|
|
function getMaxScroll() {
|
|
return scrollEl.scrollHeight - scrollEl.clientHeight;
|
|
}
|
|
|
|
function showThumb() {
|
|
const { scrollTop, scrollHeight, clientHeight } = scrollEl;
|
|
if (scrollHeight <= clientHeight) { thumb.style.opacity = "0"; return; }
|
|
const h = Math.max(20, (clientHeight / scrollHeight) * clientHeight);
|
|
const t = (scrollTop / (scrollHeight - clientHeight)) * (clientHeight - h);
|
|
thumb.style.height = `${h}px`;
|
|
thumb.style.transform = `translateY(${t}px)`;
|
|
thumb.style.opacity = "1";
|
|
if (hideTimer) clearTimeout(hideTimer);
|
|
hideTimer = setTimeout(() => { thumb.style.opacity = "0"; }, 1000);
|
|
}
|
|
|
|
function setOverscroll(amount: number) {
|
|
overscroll = amount;
|
|
if (Math.abs(amount) < 0.5) {
|
|
content.style.transform = "";
|
|
overscroll = 0;
|
|
} else {
|
|
content.style.transform = `translateY(${-amount}px)`;
|
|
}
|
|
}
|
|
|
|
function springBack() {
|
|
const from = overscroll;
|
|
if (Math.abs(from) < 0.5) { setOverscroll(0); return; }
|
|
const start = performance.now();
|
|
function frame() {
|
|
const t = Math.min(1, (performance.now() - start) / 500);
|
|
const ease = 1 - Math.pow(1 - t, 3); // cubic ease-out
|
|
setOverscroll(from * (1 - ease));
|
|
showThumb();
|
|
if (t < 1) coastFrame = requestAnimationFrame(frame);
|
|
else { content.style.transform = ""; overscroll = 0; }
|
|
}
|
|
coastFrame = requestAnimationFrame(frame);
|
|
}
|
|
|
|
function forceReset() {
|
|
cancelAnimationFrame(coastFrame);
|
|
content.style.transform = "";
|
|
overscroll = 0;
|
|
}
|
|
|
|
function onDown(e: MouseEvent) {
|
|
e.stopPropagation();
|
|
if (e.button !== 0) return;
|
|
const tag = (e.target as HTMLElement).tagName;
|
|
if (["BUTTON", "INPUT", "LABEL", "SELECT"].includes(tag)) return;
|
|
|
|
forceReset();
|
|
const startY = e.clientY;
|
|
const startScroll = scrollEl.scrollTop;
|
|
let lastY = e.clientY;
|
|
let lastTime = Date.now();
|
|
const vSamples: number[] = [];
|
|
|
|
scrollEl.style.cursor = "grabbing";
|
|
showThumb();
|
|
|
|
function onMove(ev: MouseEvent) {
|
|
ev.preventDefault();
|
|
ev.stopPropagation();
|
|
const y = ev.clientY;
|
|
const now = Date.now();
|
|
const dt = now - lastTime;
|
|
if (dt > 0) {
|
|
vSamples.push((lastY - y) / dt);
|
|
if (vSamples.length > 5) vSamples.shift();
|
|
}
|
|
lastY = y;
|
|
lastTime = now;
|
|
|
|
const desired = startScroll - (y - startY);
|
|
const max = getMaxScroll();
|
|
|
|
if (desired < 0) {
|
|
scrollEl.scrollTop = 0;
|
|
setOverscroll(desired * 0.3);
|
|
} else if (desired > max) {
|
|
scrollEl.scrollTop = max;
|
|
setOverscroll((desired - max) * 0.3);
|
|
} else {
|
|
scrollEl.scrollTop = desired;
|
|
if (overscroll !== 0) setOverscroll(0);
|
|
}
|
|
showThumb();
|
|
}
|
|
|
|
function onUp(ev: MouseEvent) {
|
|
ev.stopPropagation();
|
|
scrollEl.style.cursor = "";
|
|
window.removeEventListener("mousemove", onMove, true);
|
|
window.removeEventListener("mouseup", onUp, true);
|
|
|
|
if (overscroll !== 0) { springBack(); return; }
|
|
|
|
// Momentum coast
|
|
const avgV = vSamples.length > 0
|
|
? vSamples.reduce((a, b) => a + b, 0) / vSamples.length : 0;
|
|
const v0 = Math.max(-4, Math.min(4, avgV));
|
|
if (Math.abs(v0) < 0.005) return;
|
|
|
|
const tau = 300;
|
|
const coastStart = performance.now();
|
|
const scrollStart2 = scrollEl.scrollTop;
|
|
const totalDist = v0 * tau;
|
|
|
|
function coast() {
|
|
const t = performance.now() - coastStart;
|
|
const decay = Math.exp(-t / tau);
|
|
const offset = totalDist * (1 - decay);
|
|
const target = scrollStart2 + offset;
|
|
const max = getMaxScroll();
|
|
|
|
if (target < 0) {
|
|
scrollEl.scrollTop = 0;
|
|
const bounce = Math.min(30, Math.abs(v0 * decay) * 40);
|
|
setOverscroll(-bounce);
|
|
springBack();
|
|
return;
|
|
}
|
|
if (target > max) {
|
|
scrollEl.scrollTop = max;
|
|
const bounce = Math.min(30, Math.abs(v0 * decay) * 40);
|
|
setOverscroll(bounce);
|
|
springBack();
|
|
return;
|
|
}
|
|
|
|
scrollEl.scrollTop = target;
|
|
showThumb();
|
|
if (Math.abs(v0 * decay) > 0.0005) {
|
|
coastFrame = requestAnimationFrame(coast);
|
|
}
|
|
}
|
|
coastFrame = requestAnimationFrame(coast);
|
|
}
|
|
|
|
window.addEventListener("mousemove", onMove, true);
|
|
window.addEventListener("mouseup", onUp, true);
|
|
}
|
|
|
|
function onWheel(e: WheelEvent) {
|
|
e.stopPropagation();
|
|
const { scrollTop, scrollHeight, clientHeight } = scrollEl;
|
|
const atTop = scrollTop <= 0 && e.deltaY < 0;
|
|
const atBottom = scrollTop + clientHeight >= scrollHeight && e.deltaY > 0;
|
|
if (!atTop && !atBottom) {
|
|
e.preventDefault();
|
|
scrollEl.scrollTop += e.deltaY;
|
|
showThumb();
|
|
}
|
|
}
|
|
|
|
node.addEventListener("mousedown", onDown);
|
|
node.addEventListener("wheel", onWheel, { passive: false });
|
|
|
|
return {
|
|
destroy() {
|
|
node.removeEventListener("mousedown", onDown);
|
|
node.removeEventListener("wheel", onWheel);
|
|
forceReset();
|
|
if (hideTimer) clearTimeout(hideTimer);
|
|
thumb.remove();
|
|
},
|
|
};
|
|
}
|
|
</script>
|
|
|
|
<div class="space-y-3">
|
|
<!-- Add custom activity -->
|
|
<div class="flex gap-2 items-center">
|
|
<div class="flex-1">
|
|
<input
|
|
type="text"
|
|
placeholder="Add custom activity..."
|
|
aria-label="New activity text"
|
|
maxlength={500}
|
|
class="w-full rounded-xl border border-[#161616] bg-black px-3 py-2.5 text-[13px]
|
|
text-white outline-none placeholder:text-[#2a2a2a] focus:border-[#333]"
|
|
bind:value={newActivityText}
|
|
onkeydown={(e) => { if (e.key === "Enter") addCustomActivity(); }}
|
|
/>
|
|
</div>
|
|
<!-- Custom category dropdown -->
|
|
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
<div class="relative" bind:this={dropdownRef} onkeydown={handleDropdownKeydown} role="presentation">
|
|
<button
|
|
bind:this={dropdownTriggerRef}
|
|
use:pressable
|
|
class="flex items-center gap-1.5 rounded-xl border border-[#161616] bg-black px-3 py-2.5 text-[13px] text-text-sec
|
|
hover:border-[#333] hover:text-white transition-colors"
|
|
onclick={() => { dropdownOpen = !dropdownOpen; }}
|
|
aria-haspopup="listbox"
|
|
aria-expanded={dropdownOpen}
|
|
aria-label="Category: {getCategoryLabel(newActivityCategory)}"
|
|
>
|
|
<span>{getCategoryLabel(newActivityCategory)}</span>
|
|
<svg aria-hidden="true" class="w-3 h-3 opacity-50 transition-transform duration-200 {dropdownOpen ? 'rotate-180' : ''}" viewBox="0 0 12 12" fill="none">
|
|
<path d="M3 4.5L6 7.5L9 4.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
|
|
</svg>
|
|
</button>
|
|
{#if dropdownOpen}
|
|
<div class="absolute top-full left-0 mt-1 z-50 min-w-[140px] rounded-xl border border-border bg-[#111] shadow-xl shadow-black/50 overflow-hidden"
|
|
role="listbox" aria-label="Activity category">
|
|
{#each categories as cat, i}
|
|
<button
|
|
class="w-full text-left px-3.5 py-2.5 text-[13px] transition-colors outline-none
|
|
{newActivityCategory === cat ? 'text-white bg-[#1a1a1a]' : 'text-text-sec hover:bg-[#1a1a1a] hover:text-white'}
|
|
{focusedOptionIndex === i ? 'bg-[#1a1a1a] text-white' : ''}"
|
|
role="option"
|
|
aria-selected={newActivityCategory === cat}
|
|
tabindex={focusedOptionIndex === i ? 0 : -1}
|
|
onclick={() => { newActivityCategory = cat; dropdownOpen = false; dropdownTriggerRef?.focus(); }}
|
|
>
|
|
{getCategoryLabel(cat)}
|
|
</button>
|
|
{/each}
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
<button
|
|
use:pressable
|
|
class="flex items-center justify-center w-10 h-10 rounded-xl border border-[#161616] text-[18px] text-text-sec
|
|
hover:border-[#333] hover:text-white transition-colors disabled:opacity-30"
|
|
onclick={addCustomActivity}
|
|
disabled={!newActivityText.trim()}
|
|
aria-label="Add custom activity"
|
|
>
|
|
+
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Category buttons -->
|
|
<div class="flex flex-wrap gap-1.5">
|
|
{#each categories as cat}
|
|
{@const total = builtinCount(cat) + customCount(cat)}
|
|
{@const disabled = disabledCount(cat)}
|
|
{@const isExpanded = expandedCategory === cat}
|
|
<button
|
|
use:pressable
|
|
class="rounded-xl px-3 py-2 text-[11px] tracking-wider transition-all duration-200
|
|
{isExpanded
|
|
? 'bg-[#1a1a1a] text-white border border-[#333]'
|
|
: 'bg-[#0a0a0a] text-text-sec border border-[#161616] hover:border-[#333] hover:text-white'}"
|
|
onclick={() => toggleCategory(cat)}
|
|
aria-expanded={isExpanded}
|
|
aria-controls="activity-panel-{cat}"
|
|
>
|
|
<span class="uppercase">{getCategoryLabel(cat)}</span>
|
|
<span class="ml-1.5 text-[10px] {isExpanded ? 'text-text-sec' : 'text-text-sec opacity-60'}">
|
|
{total - disabled}/{total}
|
|
</span>
|
|
</button>
|
|
{/each}
|
|
</div>
|
|
|
|
<!-- Animated accordion for activity list -->
|
|
{#each categories as cat}
|
|
{@const isExpanded = expandedCategory === cat}
|
|
{@const catBuiltins = breakActivities.filter((a) => a.category === cat)}
|
|
{@const catCustoms = $config.custom_activities.filter((a) => a.category === cat)}
|
|
<div class="accordion-wrapper" class:accordion-open={isExpanded} id="activity-panel-{cat}" role="region" aria-label="{getCategoryLabel(cat)} activities">
|
|
<div class="accordion-inner">
|
|
<div class="rounded-xl border border-[#161616] bg-[#0a0a0a] overflow-hidden" use:blockParentDrag>
|
|
{#if catCustoms.length > 0}
|
|
<div class="px-3 pt-2.5 pb-1">
|
|
<div class="text-[9px] font-medium tracking-[0.15em] text-text-sec uppercase">Custom</div>
|
|
</div>
|
|
{/if}
|
|
|
|
<div class="relative" use:innerDragScroll>
|
|
<!-- svelte-ignore a11y_no_noninteractive_tabindex -->
|
|
<div class="activity-scroll max-h-[200px] overflow-y-auto overflow-x-hidden" tabindex="0" role="group" aria-label="{getCategoryLabel(cat)} activity list">
|
|
<div class="scroll-content">
|
|
{#each catCustoms as activity (activity.id)}
|
|
<div class="flex items-center gap-1 px-1.5 py-0.5 group hover:bg-[#111]">
|
|
<button
|
|
class="flex items-center justify-center w-9 h-9 min-w-[44px] min-h-[44px] text-[13px] transition-opacity {activity.is_favorite ? 'opacity-100' : 'opacity-30 hover:opacity-60'}"
|
|
onclick={() => toggleCustomFavorite(activity.id)}
|
|
aria-label="{activity.is_favorite ? 'Remove from favorites' : 'Add to favorites'}"
|
|
>
|
|
★
|
|
</button>
|
|
<span class="flex-1 text-[12px] {activity.enabled ? 'text-white' : 'text-text-sec line-through'}">
|
|
{activity.text}
|
|
</span>
|
|
<button
|
|
class="flex items-center justify-center w-9 h-9 min-w-[44px] min-h-[44px] text-text-sec hover:text-[#ff6b6b] opacity-0 group-hover:opacity-100 focus-visible:opacity-100 transition-all text-[11px]"
|
|
onclick={() => removeCustomActivity(activity.id)}
|
|
aria-label="Remove {activity.text}"
|
|
>
|
|
✕
|
|
</button>
|
|
<ToggleSwitch
|
|
checked={activity.enabled}
|
|
label="Enable {activity.text}"
|
|
onchange={() => toggleCustomEnabled(activity.id)}
|
|
/>
|
|
</div>
|
|
{/each}
|
|
|
|
{#if catCustoms.length > 0 && catBuiltins.length > 0}
|
|
<div class="px-3 pt-2 pb-1">
|
|
<div class="text-[9px] font-medium tracking-[0.15em] text-text-sec uppercase">Built-in</div>
|
|
</div>
|
|
{/if}
|
|
|
|
{#each catBuiltins as activity (activity.text)}
|
|
<div class="flex items-center gap-1 px-1.5 py-0.5 hover:bg-[#111]">
|
|
<button
|
|
class="flex items-center justify-center w-9 h-9 min-w-[44px] min-h-[44px] text-[13px] transition-opacity {isBuiltinFavorite(activity.text) ? 'opacity-100' : 'opacity-30 hover:opacity-60'}"
|
|
onclick={() => toggleBuiltinFavorite(activity.text)}
|
|
aria-label="{isBuiltinFavorite(activity.text) ? 'Remove from favorites' : 'Add to favorites'}"
|
|
>
|
|
★
|
|
</button>
|
|
<span class="flex-1 text-[12px] {!isBuiltinDisabled(activity.text) ? 'text-text-sec' : 'text-text-sec line-through opacity-60'}">
|
|
{activity.text}
|
|
</span>
|
|
<ToggleSwitch
|
|
checked={!isBuiltinDisabled(activity.text)}
|
|
label="Enable {activity.text}"
|
|
onchange={() => toggleBuiltinEnabled(activity.text)}
|
|
/>
|
|
</div>
|
|
{/each}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{/each}
|
|
</div>
|
|
|
|
<style>
|
|
/* Accordion: CSS grid row transition for height animation */
|
|
.accordion-wrapper {
|
|
display: grid;
|
|
grid-template-rows: 0fr;
|
|
transition: grid-template-rows 0.3s cubic-bezier(0.22, 0.03, 0.26, 1);
|
|
}
|
|
.accordion-wrapper.accordion-open {
|
|
grid-template-rows: 1fr;
|
|
}
|
|
.accordion-inner {
|
|
overflow: hidden;
|
|
min-height: 0;
|
|
}
|
|
|
|
/* Hide native scrollbar completely */
|
|
.activity-scroll {
|
|
overscroll-behavior: contain;
|
|
scrollbar-width: none; /* Firefox */
|
|
}
|
|
.activity-scroll::-webkit-scrollbar {
|
|
display: none; /* Chrome/Edge */
|
|
}
|
|
</style>
|