Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
7b09ac7
redirect old url's
hhvrc May 17, 2026
3af6481
welcome screen
hhvrc May 17, 2026
2bd90ba
Update WelcomeScreen.svelte
hhvrc May 17, 2026
69933f1
Update WelcomeScreen.svelte
hhvrc May 17, 2026
95e2483
add tutorial n stuff
hhvrc May 17, 2026
f037443
Potential fix for pull request finding 'CodeQL / Client-side URL redi…
hhvrc May 17, 2026
cbe180d
bleh
hhvrc May 17, 2026
51a4471
a
hhvrc May 18, 2026
d825a54
fix: handleKeydown skips interactive targets; tour next button labell…
hhvrc May 19, 2026
c9bf739
Merge branch 'develop' into feat/onboarding-flow
hhvrc May 19, 2026
3dbe5b2
merge: resolve conflicts from develop into feat/onboarding-flow
hhvrc May 19, 2026
58ecf42
Merge branch 'develop' into feat/onboarding-flow
hhvrc May 19, 2026
14e1e9d
good stuff
hhvrc May 19, 2026
4e84fad
format
hhvrc May 19, 2026
f2d3461
yeah
hhvrc May 19, 2026
6d0bd4d
Merge remote-tracking branch 'origin/develop' into feat/onboarding-flow
hhvrc May 19, 2026
d666a30
prepend base path to legacy hash redirects and test safety guard
hhvrc May 19, 2026
806e8d9
only attach window listeners while welcome screen is open, focus dial…
hhvrc May 19, 2026
cb757e5
extract dot grid into shared DotGrid component
hhvrc May 19, 2026
7a7e303
prompt for tour on login if welcome was seen but tour skipped
hhvrc May 19, 2026
85d3203
split welcome-tour.ts: extract onboarding state into onboarding-state.ts
hhvrc May 19, 2026
4dec2e9
fix DotGrid style expressions; add browser guard to startWelcomeTour
hhvrc May 19, 2026
c6f7b69
mobile tour: auto open/close sidebar; navigate home on tour complete;…
hhvrc May 19, 2026
bc7e536
add onPrevClick sidebar management; mark tour done on skip/dismiss
hhvrc May 19, 2026
dc7d396
hide driver.js close button; it's unclickable due to title z-index
hhvrc May 19, 2026
6f71258
fix driver.js button styles being overridden by default CSS
hhvrc May 19, 2026
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
4 changes: 4 additions & 0 deletions .env
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ PUBLIC_TURNSTILE_DEV_BYPASS_VALUE=INVALID

PUBLIC_DEVELOPMENT_BANNER=false

PUBLIC_DISABLE_ONBOARDING=false

PUBLIC_DISABLE_SHOCKER_MAP=true
Comment thread
hhvrc marked this conversation as resolved.

PUBLIC_SITE_URL=https://openshock.app
PUBLIC_SITE_SHORT_URL=https://openshock.app
PUBLIC_BACKEND_API_URL=https://api.openshock.app
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@
"vitest": "^4.1.6"
},
"dependencies": {
"driver.js": "^1.4.0",
"temporal-polyfill": "^0.3.2",
"vite": "^8.0.13"
},
Expand Down
8 changes: 8 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

51 changes: 51 additions & 0 deletions src/app.css
Original file line number Diff line number Diff line change
Expand Up @@ -250,3 +250,54 @@
display: none;
}
}

/* driver.js theme override — match the dark UI */
.driver-popover {
background: #13131a;
color: rgba(255, 255, 255, 0.85);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 0.75rem;
}
.driver-popover-title {
color: #fff;
font-weight: 600;
}
.driver-popover-description {
color: rgba(255, 255, 255, 0.7);
}
.driver-popover-arrow-side-left.driver-popover-arrow,
.driver-popover-arrow-side-right.driver-popover-arrow,
.driver-popover-arrow-side-top.driver-popover-arrow,
.driver-popover-arrow-side-bottom.driver-popover-arrow {
border-color: #13131a;
}
.driver-popover-progress-text {
color: rgba(255, 255, 255, 0.5);
font-size: 0.75rem;
}
.driver-popover-footer button {
background: rgba(255, 255, 255, 0.08) !important;
color: #fff !important;
border: 1px solid rgba(255, 255, 255, 0.12);
border-radius: 0.5rem;
padding: 0.375rem 0.875rem;
font-size: 0.875rem;
text-shadow: none !important;
}
.driver-popover-footer button:hover {
background: rgba(255, 255, 255, 0.14) !important;
}
.driver-popover-footer .driver-popover-next-btn {
background: #fff !important;
color: #000 !important;
border-color: transparent;
}
.driver-popover-footer .driver-popover-next-btn:hover {
background: rgba(255, 255, 255, 0.9) !important;
}
.driver-popover-close-btn {
color: rgba(255, 255, 255, 0.5);
}
.driver-popover-close-btn:hover {
color: #fff;
}
3 changes: 3 additions & 0 deletions src/hooks.client.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { base } from '$app/paths';
import { versionGetBackendInfo } from '$lib/api';
import { handleApiError } from '$lib/errorhandling/apiErrorHandling';
import { authState, startAuthLifecycle } from '$lib/state/auth-state.svelte';
import { backendMetadata } from '$lib/state/backend-metadata-state.svelte';
import { initializeColorScheme } from '$lib/state/color-scheme-state.svelte';
import { userState } from '$lib/state/user-state.svelte';
import { redirectLegacyHashRoute } from '$lib/utils/legacy-hash-redirect';

async function ensureTemporal(): Promise<void> {
if (typeof (globalThis as { Temporal?: unknown }).Temporal === 'undefined') {
Expand All @@ -26,6 +28,7 @@ async function clientInit(): Promise<void> {
}

export async function init() {
redirectLegacyHashRoute(base);
await ensureTemporal();
await clientInit().catch(handleApiError);
initializeColorScheme();
Expand Down
50 changes: 50 additions & 0 deletions src/lib/components/DotGrid.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
<script lang="ts">
let mouseX = $state(-9999);
let mouseY = $state(-9999);
let rafPending = false;
let containerEl: HTMLDivElement | undefined = $state();

export function handlePointerMove(e: PointerEvent) {
if (rafPending) return;
rafPending = true;
requestAnimationFrame(() => {
const rect = containerEl?.getBoundingClientRect();
mouseX = rect ? e.clientX - rect.left : e.clientX;
mouseY = rect ? e.clientY - rect.top : e.clientY;
rafPending = false;
});
}
</script>

<div bind:this={containerEl} class="pointer-events-none absolute inset-0">
<div class="bg-grid absolute inset-0" aria-hidden="true"></div>
<div
class="bg-grid-spotlight absolute inset-0"
style="--mouse-x={mouseX}px --mouse-y={mouseY}px"
aria-hidden="true"
></div>
</div>

<style>
.bg-grid {
background-image: radial-gradient(circle, rgba(255, 255, 255, 0.07) 1px, transparent 1px);
background-size: 28px 28px;
background-position: center center;
}

.bg-grid-spotlight {
background-image: radial-gradient(circle, rgba(225, 74, 109, 0.9) 1px, transparent 1px);
background-size: 28px 28px;
background-position: center center;
mask-image: radial-gradient(
circle 220px at var(--mouse-x, -9999px) var(--mouse-y, -9999px),
black 0%,
transparent 75%
);
-webkit-mask-image: radial-gradient(
circle 220px at var(--mouse-x, -9999px) var(--mouse-y, -9999px),
black 0%,
transparent 75%
);
}
</style>
4 changes: 2 additions & 2 deletions src/lib/components/ui/sidebar/sidebar-menu-button.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@
import { tv, type VariantProps } from "tailwind-variants";

export const sidebarMenuButtonVariants = tv({
base: "ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground active:bg-sidebar-accent active:text-sidebar-accent-foreground data-active:bg-sidebar-accent data-active:text-sidebar-accent-foreground data-open:hover:bg-sidebar-accent data-open:hover:text-sidebar-accent-foreground gap-2 rounded-md p-2 text-left text-sm transition-[width,height,padding] group-has-data-[sidebar=menu-action]/menu-item:pr-8 group-data-[collapsible=icon]:size-8! group-data-[collapsible=icon]:p-2! focus-visible:ring-2 data-active:font-medium peer/menu-button group/menu-button flex w-full items-center overflow-hidden outline-hidden disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&_svg]:size-4 [&_svg]:shrink-0 [&>span:last-child]:truncate",
base: "ring-sidebar-ring hover:bg-sidebar-accent/50 hover:text-sidebar-accent-foreground active:bg-sidebar-accent active:text-sidebar-accent-foreground data-active:bg-sidebar-accent data-active:text-sidebar-accent-foreground data-open:hover:bg-sidebar-accent/50 data-open:hover:text-sidebar-accent-foreground gap-2 rounded-md p-2 text-left text-sm transition-[width,height,padding] group-has-data-[sidebar=menu-action]/menu-item:pr-8 group-data-[collapsible=icon]:size-8! group-data-[collapsible=icon]:p-2! focus-visible:ring-2 data-active:font-medium peer/menu-button group/menu-button flex w-full items-center overflow-hidden outline-hidden disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&_svg]:size-4 [&_svg]:shrink-0 [&>span:last-child]:truncate",
variants: {
variant: {
default: "hover:bg-sidebar-accent hover:text-sidebar-accent-foreground",
default: "hover:bg-sidebar-accent/50 hover:text-sidebar-accent-foreground",
outline: "bg-background hover:bg-sidebar-accent hover:text-sidebar-accent-foreground shadow-[0_0_0_1px_hsl(var(--sidebar-border))] hover:shadow-[0_0_0_1px_hsl(var(--sidebar-accent))]",
},
size: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@

const mergedProps = $derived({
class: cn(
"text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground active:bg-sidebar-accent active:text-sidebar-accent-foreground [&>svg]:text-sidebar-accent-foreground data-active:bg-sidebar-accent data-active:text-sidebar-accent-foreground h-7 gap-2 rounded-md px-2 focus-visible:ring-2 data-[size=md]:text-sm data-[size=sm]:text-xs [&>svg]:size-4 flex min-w-0 -translate-x-px items-center overflow-hidden outline-hidden group-data-[collapsible=icon]:hidden disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&>span:last-child]:truncate [&>svg]:shrink-0",
"text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent/50 hover:text-sidebar-accent-foreground active:bg-sidebar-accent active:text-sidebar-accent-foreground [&>svg]:text-sidebar-accent-foreground data-active:bg-sidebar-accent data-active:text-sidebar-accent-foreground h-7 gap-2 rounded-md px-2 focus-visible:ring-2 data-[size=md]:text-sm data-[size=sm]:text-xs [&>svg]:size-4 flex min-w-0 -translate-x-px items-center overflow-hidden outline-hidden group-data-[collapsible=icon]:hidden disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&>span:last-child]:truncate [&>svg]:shrink-0",
className
),
"data-slot": "sidebar-menu-sub-button",
Expand Down
28 changes: 26 additions & 2 deletions src/lib/state/auth-state.svelte.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
import { dialog } from '$lib/components/dialog-manager/dialog-store.svelte';
import { registerOnUnauthorized } from '$lib/errorhandling/apiErrorHandling';
import { destroySignalR, initializeSignalR } from '$lib/signalr/user.svelte';
import {
hasCompletedTour,
hasSeenWelcome,
isOnboardingDisabled,
markTourCompleted,
startWelcomeTour,
} from '$lib/tour/welcome-tour';
import { userState } from './user-state.svelte';

export const AuthStatus = {
Expand All @@ -25,6 +33,18 @@ export const authState = {
},
};

async function maybeTourPrompt() {
if (isOnboardingDisabled() || hasCompletedTour() || !hasSeenWelcome()) return;
const result = await dialog.confirm({
title: 'Take the quick tour?',
desc: "You skipped it earlier. It only takes a minute and shows you what's new.",
confirmButtonText: 'Sure, show me',
cancelButtonText: 'No thanks',
});
if (result.confirmed) await startWelcomeTour();
else markTourCompleted();
}

/**
* Wires up the reactive side-effects of auth state:
* - SignalR connects/disconnects as the user logs in/out.
Expand All @@ -37,12 +57,16 @@ export function startAuthLifecycle() {
registerOnUnauthorized(() => userState.reset());

_stopLifecycle = $effect.root(() => {
let prevSelf: typeof userState.self = null;
$effect(() => {
if (userState.self) {
const self = userState.self;
if (self && !prevSelf) {
void maybeTourPrompt();
void initializeSignalR();
} else {
} else if (!self) {
void destroySignalR();
}
prevSelf = self;
});
});
}
60 changes: 60 additions & 0 deletions src/lib/tour/onboarding-state.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { PUBLIC_DISABLE_ONBOARDING } from '$env/static/public';
import { isTruthy } from '$lib/utils/parse';

const CURRENT_WELCOME_VERSION = 1;
const WELCOME_VERSION_KEY = 'os.welcomeVersion';

const CURRENT_TOUR_VERSION = 1;
const TOUR_VERSION_KEY = 'os.tourCompletedVersion';

export function isOnboardingDisabled(): boolean {
return isTruthy(PUBLIC_DISABLE_ONBOARDING);
}

export function hasSeenWelcome(): boolean {
try {
const raw = localStorage.getItem(WELCOME_VERSION_KEY);
const seen = raw ? parseInt(raw, 10) : 0;
return Number.isFinite(seen) && seen >= CURRENT_WELCOME_VERSION;
} catch {
return false;
}
}

export function markWelcomed(): void {
try {
localStorage.setItem(WELCOME_VERSION_KEY, String(CURRENT_WELCOME_VERSION));
} catch {
// ignore (private mode, quota, etc.)
}
}

export function shouldShowWelcome(): boolean {
if (isOnboardingDisabled()) return false;
try {
const raw = localStorage.getItem(WELCOME_VERSION_KEY);
const seen = raw ? parseInt(raw, 10) : 0;
return !Number.isFinite(seen) || seen < CURRENT_WELCOME_VERSION;
} catch {
return false;
}
}

export function hasCompletedTour(): boolean {
if (isOnboardingDisabled()) return true;
try {
const raw = localStorage.getItem(TOUR_VERSION_KEY);
const n = raw ? parseInt(raw, 10) : 0;
return Number.isFinite(n) && n >= CURRENT_TOUR_VERSION;
} catch {
return false;
}
}

export function markTourCompleted(): void {
try {
localStorage.setItem(TOUR_VERSION_KEY, String(CURRENT_TOUR_VERSION));
} catch {
// ignore
}
}
Loading
Loading