Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
441 changes: 441 additions & 0 deletions src/app.css

Large diffs are not rendered by default.

38 changes: 38 additions & 0 deletions src/lib/actions/reveal.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/**
* `use:reveal` — fades an element up as it scrolls into view.
*
* The initial hidden state is added by the action (not in markup), so the
* content is fully visible without JavaScript and only animates when the script
* runs. Honours prefers-reduced-motion by leaving the element untouched.
*
* Usage: `<section use:reveal>` or `<div use:reveal={{ delay: 120 }}>`
*/
export function reveal(node: HTMLElement, params: { delay?: number } = {}) {
if (typeof window === 'undefined') return {};

const reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
if (reduce || !('IntersectionObserver' in window)) return {};

if (params.delay) node.style.setProperty('--reveal-delay', `${params.delay}ms`);
node.classList.add('reveal');

const io = new IntersectionObserver(
(entries) => {
for (const entry of entries) {
if (entry.isIntersecting) {
node.classList.add('is-visible');
io.unobserve(entry.target);
}
}
},
{ threshold: 0.12, rootMargin: '0px 0px -8% 0px' }
);

io.observe(node);

return {
destroy() {
io.disconnect();
}
};
}
99 changes: 40 additions & 59 deletions src/lib/components/AttentionBanner.svelte
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
<script>
import { onMount } from 'svelte';

import Icon from '$lib/components/Icon.svelte';

let showBanner = $state(true);
let autoHide = $state(false);

onMount(() => {
// Check if banner should be permanently hidden
const autoHideEnabled = localStorage.getItem('attentionBannerAutoHide');
Expand All @@ -12,7 +13,7 @@
showBanner = false;
}
});

// Reactive effect to handle autoHide changes
$effect(() => {
if (autoHide) {
Expand All @@ -23,75 +24,55 @@
showBanner = true;
}
});

function closeBanner() {
// Temporary close - just hide for this session
showBanner = false;
}

const points = [
'Always read the code before executing',
'Watch for scams selling "premium" scripts',
'Official resources only via Bruce App Store',
'Report suspicious activity to our moderators'
];
</script>

{#if showBanner}
<div class="mx-2 sm:mx-4 my-2 rounded-lg border border-purple-600 bg-purple-400/20 p-3 sm:p-4 relative">
<div class="flex flex-col sm:flex-row items-center sm:items-start gap-3">
<!-- Close Button -->
<button
class="absolute top-2 right-2 text-gray-400 hover:text-white text-xl font-bold p-1 z-10"
<!-- Severity is carried by a left rule and the icon colour rather than a
filled alert block, so the notice sits inside the page instead of
shouting over it. -->
<aside class="relative border border-l-2 border-[var(--rule)] border-l-yellow-500/80 bg-yellow-500/[0.04] p-5 text-left">
<button
class="absolute top-4 right-4 inline-flex h-7 w-7 items-center justify-center rounded-[3px] text-[var(--text-faint)] transition-colors hover:bg-white/5 hover:text-white"
onclick={closeBanner}
title="Close banner"
aria-label="Close banner"
>
×
<Icon name="close" size={15} />
</button>

<!-- Warning Icon -->
<div class="flex-shrink-0 self-center sm:self-start sm:mt-0.5">
<svg class="h-8 w-8 sm:h-10 sm:w-10 text-yellow-400" fill="currentColor" viewBox="0 0 20 20">
<path
fill-rule="evenodd"
d="M8.485 2.495c.673-1.167 2.357-1.167 3.03 0l6.28 10.875c.673 1.167-.17 2.625-1.516 2.625H3.72c-1.347 0-2.19-1.458-1.515-2.625L8.485 2.495zM10 5a.75.75 0 01.75.75v3.5a.75.75 0 01-1.5 0v-3.5A.75.75 0 0110 5zm0 9a1 1 0 100-2 1 1 0 000 2z"
clip-rule="evenodd"
/>
</svg>
</div>

<!-- Content -->
<div class="flex-grow pr-0 sm:pr-8">
<h3 class="mb-2 text-xl sm:text-2xl font-bold tracking-wide text-yellow-400 text-center sm:text-left">ATTENTION</h3>
<div class="flex items-center gap-2.5 pr-10 text-yellow-500">
<Icon name="warning" size={16} />
<h3 class="eyebrow" style="color:inherit">Attention</h3>
</div>

<p class="mb-3 text-base sm:text-lg text-slate-300 text-center sm:text-left leading-relaxed">
Only trust open-source scripts you can verify! Bruce is open-source under AGPL License - never pay for scripts, firmware forks or themes.
</p>
<p class="mt-3 max-w-3xl text-sm leading-relaxed text-white">
Only trust open-source scripts you can verify! Bruce is open-source under AGPL License - never pay for scripts, firmware forks or themes.
</p>

<ul class="text-sm sm:text-md space-y-2 text-slate-300 mb-3 text-left">
<li class="flex items-start gap-2 leading-relaxed">
<span class="h-1.5 w-1.5 rounded-full bg-slate-400 mt-2 flex-shrink-0"></span>
<span>Always read the code before executing</span>
</li>
<li class="flex items-start gap-2 leading-relaxed">
<span class="h-1.5 w-1.5 rounded-full bg-slate-400 mt-2 flex-shrink-0"></span>
<span>Watch for scams selling "premium" scripts</span>
</li>
<li class="flex items-start gap-2 leading-relaxed">
<span class="h-1.5 w-1.5 rounded-full bg-slate-400 mt-2 flex-shrink-0"></span>
<span>Official resources only via Bruce App Store</span>
<ul class="mt-4 grid gap-2 sm:grid-cols-2">
{#each points as point}
<li class="flex items-start gap-2.5 text-sm leading-relaxed text-[var(--text-dim)]">
<span class="mt-[0.45rem] h-px w-3 shrink-0 bg-[var(--rule-strong)]"></span>
<span>{point}</span>
</li>
<li class="flex items-start gap-2 leading-relaxed">
<span class="h-1.5 w-1.5 rounded-full bg-slate-400 mt-2 flex-shrink-0"></span>
<span>Report suspicious activity to our moderators</span>
</li>
</ul>

<!-- Auto-hide option -->
<div class="border-t border-purple-500/30 pt-3 mt-3">
<label class="flex items-center gap-2 text-xs sm:text-sm text-slate-300 cursor-pointer">
<input
type="checkbox"
bind:checked={autoHide}
class="w-4 h-4 text-purple-600 bg-gray-700 border-gray-600 rounded focus:ring-purple-500 focus:ring-2 cursor-pointer"
>
<span>Don't show this banner again</span>
</label>
</div>
</div>
</div>
</div>
{/each}
</ul>

<label class="mt-5 flex w-fit cursor-pointer items-center gap-2 border-t border-[var(--rule)] pt-4 text-xs text-[var(--text-faint)]">
<input type="checkbox" bind:checked={autoHide} class="h-3.5 w-3.5 accent-[var(--color-brand)]" />
<span>Don't show this banner again</span>
</label>
</aside>
{/if}
92 changes: 50 additions & 42 deletions src/lib/components/BoardCard.svelte
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
<script>
import { resolve } from '$app/paths';
import { base } from '$app/paths';
import Icon from '$lib/components/Icon.svelte';

let { images = [], title = '', description = '', className, children } = $props();

Expand Down Expand Up @@ -44,52 +45,59 @@
});
</script>

<!-- Board Card -->
<div class="mx-auto">
<div class="overflow-hidden rounded-lg border border-white/10 bg-white/5 {className}">
<!-- Board Image Section -->
<div class="relative mx-auto max-w-2xl">
<div class="group relative">
<img
src={resolve('/') + String(images[currentImageIndex]).replace(/^\//, '')}
alt={title}
class="h-72 w-full rounded-lg object-cover max-sm:p-5"
onmouseenter={pauseInterval}
onmouseleave={resumeInterval}
/>
<article class="panel overflow-hidden {className}">
<!-- Image plate -->
<div class="group relative border-b border-[var(--rule)] bg-black/40">
<img
src={base + images[currentImageIndex]}
alt={title}
class="h-72 w-full object-contain p-6"
onmouseenter={pauseInterval}
onmouseleave={resumeInterval}
/>

<!-- Navigation Buttons -->
{#if images.length > 1}
<div class="absolute inset-0 flex items-center justify-between px-5 opacity-0 transition-opacity duration-300 group-hover:opacity-100">
<button
onclick={prevImage}
class="flex h-12 w-12 cursor-pointer items-center justify-center rounded-full border-none bg-black/50 text-lg text-white transition-all duration-300 hover:bg-black/80"
>
&lt;
</button>
<button
onclick={nextImage}
class="flex h-12 w-12 cursor-pointer items-center justify-center rounded-full border-none bg-black/50 text-lg text-white transition-all duration-300 hover:bg-black/80"
>
&gt;
</button>
</div>
{/if}
{#if images.length > 1}
<div
class="absolute inset-x-0 top-1/2 flex -translate-y-1/2 items-center justify-between px-4 opacity-0 transition-opacity duration-200 group-hover:opacity-100"
>
<button
onclick={prevImage}
aria-label="Previous image"
class="inline-flex h-9 w-9 items-center justify-center rounded-[3px] border border-[var(--rule-strong)] bg-black/70 text-white transition-colors hover:border-[var(--color-brand)]"
>
<Icon name="chevron-left" size={16} />
</button>
<button
onclick={nextImage}
aria-label="Next image"
class="inline-flex h-9 w-9 items-center justify-center rounded-[3px] border border-[var(--rule-strong)] bg-black/70 text-white transition-colors hover:border-[var(--color-brand)]"
>
<Icon name="chevron-right" size={16} />
</button>
</div>
</div>

<!-- Board Content -->
<div class="p-8 text-center">
<h2 class="mb-6 text-3xl font-bold">{title}</h2>

<div class="mb-8">
<p class="leading-relaxed text-gray-300">
{description}
</p>
<!-- Position marker: ticks, not dots, matching the hero carousel. -->
<div class="absolute bottom-4 left-1/2 flex -translate-x-1/2 gap-1.5">
{#each images as _, i}
<button
onclick={() => (currentImageIndex = i)}
aria-label="Image {i + 1}"
aria-current={i === currentImageIndex}
class="h-[3px] w-6 transition-colors {i === currentImageIndex ? 'bg-[var(--color-brand)]' : 'bg-white/25 hover:bg-white/45'}"
></button>
{/each}
</div>
{/if}
</div>

<div class="p-8">
<h2 class="text-2xl font-semibold">{title}</h2>
<p class="mt-3 max-w-3xl text-sm leading-relaxed text-[var(--text-dim)]">
{description}
</p>

<!-- Slot for technical specifications and action buttons -->
<div class="mt-8">
{@render children()}
</div>
</div>
</div>
</article>
20 changes: 6 additions & 14 deletions src/lib/components/Btn.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,14 @@
export let outline: boolean = false;
</script>

{#if !outline}
<a
{href}
class="inline-block rounded bg-[#9B51E0] px-6 py-3 font-semibold text-white transition-all duration-300 ease-in-out hover:scale-105 hover:bg-[#8033C7] {className}"
{...$$restProps}
style="color:white"
>
<!-- Renders as a link when href is given, otherwise as a real button, so the
onclick handlers in Bruce Lab get proper keyboard/AT semantics. -->
{#if href}
<a {href} class="btn {outline ? 'btn-outline' : 'btn-primary'} {className}" {...$$restProps} style="color:#fff">
<slot />
</a>
{:else}
<a
{href}
class="inline-block rounded-lg border-2 border-[#9B51E0] bg-transparent px-6 py-3 font-semibold text-[#9B51E0] transition-all duration-300 ease-in-out hover:scale-105 hover:bg-[#9B51E0] hover:text-white {className}"
{...$$restProps}
style="color:white"
>
<button type="button" class="btn {outline ? 'btn-outline' : 'btn-primary'} {className}" {...$$restProps}>
<slot />
</a>
</button>
{/if}
22 changes: 12 additions & 10 deletions src/lib/components/Card.svelte
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
<div
class="flex w-full max-w-xs items-center justify-center rounded-lg border-none bg-[#222] text-center transition-transform duration-300 ease-in-out hover:scale-105 hover:shadow-2xl"
style="transform-style: preserve-3d;"
>
<div class="p-1">
<div class="p-4">
<slot />
</div>
</div>
</div>
<script lang="ts">
// `index` prints a monospaced ordinal in the corner — a structural marker
// that gives the grid rhythm without resorting to decorative icon badges.
export let index: string | number | undefined = undefined;
</script>

<article class="panel panel-interactive relative flex h-full flex-col p-6 text-left">
{#if index !== undefined}
<span class="meta absolute top-5 right-5 tabular-nums">{index}</span>
{/if}
<slot />
</article>
Loading