(default_state)
17 |
18 | function toggle_new_move() {
19 | update((state) => {
20 | state.new_move.status = state.new_move.status === 'OPEN' ? 'CLOSED' : 'OPEN'
21 | return state
22 | })
23 | }
24 |
25 | return { subscribe, update, set, toggle_new_move }
26 | }
27 |
28 | export const state = new_state()
29 |
--------------------------------------------------------------------------------
/src/routes/(app)/profile/+page.svelte:
--------------------------------------------------------------------------------
1 |
13 |
14 | {pb.authStore.model.name}
15 |
16 |
17 | Email: {pb.authStore.model.email}
18 |
19 |
20 | More coming soon
21 |
22 | Logout
23 |
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/src/lib/state/battle_moves.svelte.ts:
--------------------------------------------------------------------------------
1 | const createBattleMoves = () => {
2 | const initial_moves = localStorage?.getItem('battle_moves')
3 | let battle_moves: string[] = $state(initial_moves ? JSON.parse(initial_moves) : [])
4 |
5 | function use(move_id: string) {
6 | // If there are no moves, create an empty array
7 | if (!battle_moves.includes(move_id)) {
8 | battle_moves.push(move_id)
9 | sync_with_local_storage()
10 | }
11 | }
12 |
13 | function reset() {
14 | battle_moves = []
15 | sync_with_local_storage()
16 | }
17 |
18 | function sync_with_local_storage() {
19 | localStorage.setItem('battle_moves', JSON.stringify(battle_moves))
20 | }
21 |
22 | return {
23 | reset,
24 | use,
25 | get moves() {
26 | return battle_moves
27 | }
28 | }
29 | }
30 |
31 | export const battle_moves = createBattleMoves()
32 |
--------------------------------------------------------------------------------
/src/lib/auth/Verify.svelte:
--------------------------------------------------------------------------------
1 |
13 |
14 |
15 | {#if !verified_sent}
16 |
17 | Your email is not verified.
18 | Send Verification
19 |
20 | {:else}
21 |
Verification email sent to {user.email}. Please check your email.
22 | {/if}
23 |
24 |
25 |
37 |
--------------------------------------------------------------------------------
/src/lib/auth/auth_form.svelte.ts:
--------------------------------------------------------------------------------
1 | import { settings } from '$/settings'
2 | import { goto } from '$app/navigation'
3 |
4 | export function auth_form_state() {
5 | let status: 'LOADING' | 'SUCCESS' | 'ERROR' | 'INITIAL' = $state('INITIAL')
6 | let error_message: string | undefined = $state()
7 |
8 | function loading() {
9 | status = 'LOADING'
10 | }
11 | function error(e_message: string) {
12 | // toast.error(e_message)
13 | status = 'ERROR'
14 | error_message = e_message
15 | }
16 |
17 | function success(route: string | boolean = settings.app_route) {
18 | error_message = undefined
19 | status = 'SUCCESS'
20 | if (route && typeof route === 'string') goto(route)
21 | }
22 |
23 | return {
24 | get status() {
25 | return status
26 | },
27 | get error_message() {
28 | return error_message
29 | },
30 | loading,
31 | error,
32 | success
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/src/lib/MoveType.svelte:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 | {type_name.charAt(0)}
7 | {type_name}
8 |
9 |
10 |
43 |
--------------------------------------------------------------------------------
/src/lib/YouSureAboutThat.svelte:
--------------------------------------------------------------------------------
1 |
30 |
31 | Reset
32 |
33 |
34 | {#if attempt_count !== 0}
35 |
36 | Are you sure? Press {action_text}
37 | {remaining} more time{one_more_left ? '' : 's'}.
38 |
39 | {/if}
40 |
41 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # The Break
2 |
3 | https://thebreak.app
4 |
5 | An app for the bboys and bgirls.
6 |
7 | It allows for you to have a move book with classifications/names/values. This way you can organize your moves and combos. There are also several tools built into the site based off of practice games I've picked up. Right now there are 3 tools.
8 |
9 | 30/30 - This is essentially a stop watch that plays an airhorn every 30 seconds. It's useful for developing endurance. Practice hard for 30 seconds, then rest for 30 seconds.
10 |
11 | Battle Mode - This shows all of your moves in their different categories. Swipe a move and select it as 'used'. It's then hidden. If you select all of the moves you did in a given round, the ones remaining are the moves you have left. By the time you get to the finals of a jam, you have a list of the moves you have left.
12 |
13 | ## About The Codebase
14 |
15 | - Svelte 5
16 | - Svelte Kit 2
17 | - Pocketbase hosted on Coolify
18 | - Hosted on Cloudflare Pages
19 |
20 | syntax.fm
21 |
--------------------------------------------------------------------------------
/src/app.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | %sveltekit.head%
8 |
9 |
10 |
14 |
15 |
16 |
17 |
18 |
23 |
24 |
25 | %sveltekit.body%
26 |
27 |
28 |
--------------------------------------------------------------------------------
/src/routes/(app)/moves/+page.svelte:
--------------------------------------------------------------------------------
1 |
9 |
10 |
11 |
24 |
25 |
29 |
30 |
31 | Add Move
32 |
33 |
34 |
35 |
36 |
--------------------------------------------------------------------------------
/src/routes/(app)/+layout.svelte:
--------------------------------------------------------------------------------
1 |
25 |
26 | {#if data.user && !data.user.verified}
27 |
28 | {/if}
29 |
30 | {#if $skpage.url.pathname !== '/time'}
31 |
34 | {/if}
35 |
36 |
37 |
38 | {@render children()}
39 |
40 |
--------------------------------------------------------------------------------
/src/lib/BattleMove.svelte:
--------------------------------------------------------------------------------
1 |
9 |
10 |
11 |
{
15 | x = e.detail.offsetX
16 | }}
17 | on:neodrag:end={(e) => {
18 | if (Math.abs(e.detail.offsetX) > 150) {
19 | battle_moves.use(move.id)
20 | }
21 | x = 0
22 | }}
23 | >
24 | {move.name}
25 |
26 |
Use Move Use Move
27 |
28 |
29 |
61 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "bboy-tools",
3 | "version": "0.0.1",
4 | "scripts": {
5 | "dev": "vite dev",
6 | "build": "vite build",
7 | "package": "svelte-kit package",
8 | "preview": "svelte-kit preview",
9 | "prepare": "svelte-kit sync",
10 | "check": "svelte-check --tsconfig ./tsconfig.json",
11 | "check:watch": "svelte-check --tsconfig ./tsconfig.json --watch",
12 | "lint": "prettier --check --plugin-search-dir=. . && eslint .",
13 | "format": "prettier --write --plugin-search-dir=. .",
14 | "typegen": "pocketbase-typegen --env --out ./src/pocket-types.ts"
15 | },
16 | "devDependencies": {
17 | "@sveltejs/adapter-cloudflare": "^4.7.2",
18 | "@sveltejs/kit": "^2.5.26",
19 | "@sveltejs/vite-plugin-svelte": "^3.1.2",
20 | "@typescript-eslint/eslint-plugin": "^7.18.0",
21 | "@typescript-eslint/parser": "^7.18.0",
22 | "pocketbase-typegen": "^1.2.1",
23 | "prettier": "^3.3.3",
24 | "prettier-plugin-svelte": "^3.2.6",
25 | "svelte": "5.0.0-next.175",
26 | "svelte-check": "^3.8.6",
27 | "svelte-preprocess": "^6.0.2",
28 | "tslib": "^2.7.0",
29 | "typescript": "^5.6.2",
30 | "vite": "^5.4.4"
31 | },
32 | "type": "module",
33 | "dependencies": {
34 | "@atlaskit/pragmatic-drag-and-drop": "^1.3.0",
35 | "@neodrag/svelte": "^2.0.6",
36 | "@rodrigodagostino/svelte-sortable-list": "^0.9.6",
37 | "dexie": "^4.0.8",
38 | "just-kebab-case": "^4.2.0",
39 | "pocketbase": "^0.21.5",
40 | "sk-form-data": "^2.0.2",
41 | "svelte-gesture": "^0.1.4"
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/src/lib/auth/Signup.svelte:
--------------------------------------------------------------------------------
1 |
30 |
31 | Sign up
32 |
47 |
48 | {#if auth.error_message}
49 | {auth.error_message}
50 | {/if}
51 |
52 | Already have an account?
53 | Sign in
54 |
--------------------------------------------------------------------------------
/static/safari-pinned-tab.svg:
--------------------------------------------------------------------------------
1 |
2 |
4 |
7 |
8 | Created by potrace 1.14, written by Peter Selinger 2001-2017
9 |
10 |
12 |
21 |
23 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/src/lib/auth/Login.svelte:
--------------------------------------------------------------------------------
1 |
30 |
31 | Login
32 |
47 |
48 | {#if auth.error_message}
49 | {auth.error_message}
50 | {/if}
51 |
52 |
53 | Need an account?
54 | Sign Up
55 |
56 |
57 |
58 | Forgot your password?
59 |
60 |
--------------------------------------------------------------------------------
/src/routes/(site)/auth/confirm-password-reset/[token]/+page.svelte:
--------------------------------------------------------------------------------
1 |
28 |
29 | Reset Password
30 |
45 |
46 | {#if auth.error_message}
47 | {auth.error_message}
48 | {/if}
49 |
50 |
51 | Need an account?
52 | Sign Up
53 |
54 |
55 |
56 | Forgot your password?
57 |
58 |
--------------------------------------------------------------------------------
/src/routes/(app)/tools/battle-mode/+page.svelte:
--------------------------------------------------------------------------------
1 |
13 |
14 |
15 |
16 | Drag to use move
17 |
18 | {#if moves?.moves}
19 | {#each TYPES as type (type)}
20 | {@const filtered = moves?.moves
21 | ?.filter((move) => move.type === type)
22 | .filter((move) => !battle_moves.moves.includes(move.id))}
23 | {@const type_name = type[0].toUpperCase() + type.slice(1)}
24 |
25 |
26 | {#if filtered?.length === 0}
27 |
All {type_name}s used
28 | {:else}
29 | {#each filtered as move (move.id)}
30 |
31 |
32 |
33 | {/each}
34 | {/if}
35 |
36 | {/each}
37 | {/if}
38 |
39 |
59 |
--------------------------------------------------------------------------------
/src/routes/(app)/tools/+page.svelte:
--------------------------------------------------------------------------------
1 |
42 |
43 |
53 |
54 |
73 |
--------------------------------------------------------------------------------
/src/lib/auth/ForgotPassword.svelte:
--------------------------------------------------------------------------------
1 |
30 |
31 | Forgot Password
32 | {#if auth.status === 'SUCCESS'}
33 | Please check your email for password reset instructions
34 | {:else}
35 |
46 |
47 | {#if auth.error_message}
48 | {auth.error_message}
49 | {/if}
50 | {/if}
51 |
52 |
53 | Need an account?
54 | Sign Up
55 |
56 |
57 | Know your account?
58 | Login
59 |
60 |
--------------------------------------------------------------------------------
/src/lib/Moves.svelte:
--------------------------------------------------------------------------------
1 |
7 |
8 | {#if moves?.moves}
9 | {#each TYPES as type}
10 | {@const filtered = moves?.moves.filter((move) => move.type === type)}
11 | {@const type_name = type[0].toUpperCase() + type.slice(1)}
12 |
13 |
14 |
15 |
16 | {#if filtered.length === 0}
17 |
No {type_name} Moves added yet.
18 | {:else}
19 | {#each filtered as move (move.id)}
20 |
21 | {move.name}{move.value}
22 |
23 | {/each}
24 | {/if}
25 |
26 |
27 | {/each}
28 | {/if}
29 |
30 |
88 |
--------------------------------------------------------------------------------
/src/pocket-types.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * This file was @generated using pocketbase-typegen
3 | */
4 |
5 | import type PocketBase from 'pocketbase'
6 | import type { RecordService } from 'pocketbase'
7 |
8 | export enum Collections {
9 | Moves = 'moves',
10 | Users = 'users'
11 | }
12 |
13 | // Alias types for improved usability
14 | export type IsoDateString = string
15 | export type RecordIdString = string
16 | export type HTMLString = string
17 |
18 | // System fields
19 | export type BaseSystemFields = {
20 | id: RecordIdString
21 | created: IsoDateString
22 | updated: IsoDateString
23 | collectionId: string
24 | collectionName: Collections
25 | expand?: T
26 | }
27 |
28 | export type AuthSystemFields = {
29 | email: string
30 | emailVisibility: boolean
31 | username: string
32 | verified: boolean
33 | } & BaseSystemFields
34 |
35 | // Record types for each collection
36 |
37 | export enum MovesTypeOptions {
38 | 'toprock' = 'toprock',
39 | 'footwork' = 'footwork',
40 | 'go-down' = 'go-down',
41 | 'freeze' = 'freeze',
42 | 'power' = 'power',
43 | 'burner' = 'burner'
44 | }
45 | export type MovesRecord = {
46 | name?: string
47 | props?: number
48 | type?: MovesTypeOptions
49 | user?: RecordIdString
50 | value?: number
51 | }
52 |
53 | export type UsersRecord = {
54 | avatar?: string
55 | name?: string
56 | }
57 |
58 | // Response types include system fields and match responses from the PocketBase API
59 | export type MovesResponse = Required & BaseSystemFields
60 | export type UsersResponse = Required & AuthSystemFields
61 |
62 | // Types containing all Records and Responses, useful for creating typing helper functions
63 |
64 | export type CollectionRecords = {
65 | moves: MovesRecord
66 | users: UsersRecord
67 | }
68 |
69 | export type CollectionResponses = {
70 | moves: MovesResponse
71 | users: UsersResponse
72 | }
73 |
74 | // Type for usage with type asserted PocketBase instance
75 | // https://github.com/pocketbase/js-sdk#specify-typescript-definitions
76 |
77 | export type TypedPocketBase = PocketBase & {
78 | collection(idOrName: 'moves'): RecordService
79 | collection(idOrName: 'users'): RecordService
80 | }
81 |
--------------------------------------------------------------------------------
/src/routes/(app)/tools/auto-set-builder/+page.svelte:
--------------------------------------------------------------------------------
1 |
52 |
53 | CAUTION: This is not complete and just a basic implementation
54 |
55 | {#if status === 'INITIAL'}
56 |
57 |
58 |
59 | Generate
60 | {:else if status === 'DONE'}
61 | Reset
62 | {/if}
63 |
64 | {#each sets as set, i}
65 | Set #{i + 1}
66 |
67 | {#each set as move}
68 |
69 | {move.name} - {move.type}
70 |
71 | {/each}
72 |
73 | {/each}
74 |
--------------------------------------------------------------------------------
/src/service-worker.js:
--------------------------------------------------------------------------------
1 | ///
2 | import { build, files, version } from '$service-worker'
3 |
4 | // Create a unique cache name for this deployment
5 | const CACHE = `cache-${version}`
6 |
7 | const ASSETS = [
8 | ...build, // the app itself
9 | ...files // everything in `static`
10 | ]
11 |
12 | self.addEventListener('install', (event) => {
13 | // Create a new cache and add all files to it
14 | async function addFilesToCache() {
15 | const cache = await caches.open(CACHE)
16 | await cache.addAll(ASSETS)
17 | }
18 |
19 | event.waitUntil(addFilesToCache())
20 | })
21 |
22 | self.addEventListener('activate', (event) => {
23 | // Remove previous cached data from disk
24 | async function deleteOldCaches() {
25 | for (const key of await caches.keys()) {
26 | if (key !== CACHE) await caches.delete(key)
27 | }
28 | }
29 |
30 | event.waitUntil(deleteOldCaches())
31 | })
32 |
33 | self.addEventListener('fetch', (event) => {
34 | // ignore POST requests etc
35 | if (event.request.method !== 'GET') return
36 |
37 | async function respond() {
38 | const url = new URL(event.request.url)
39 | const cache = await caches.open(CACHE)
40 |
41 | // `build`/`files` can always be served from the cache
42 | if (ASSETS.includes(url.pathname)) {
43 | const response = await cache.match(url.pathname)
44 |
45 | if (response) {
46 | return response
47 | }
48 | }
49 |
50 | // for everything else, try the network first, but
51 | // fall back to the cache if we're offline
52 | try {
53 | const response = await fetch(event.request)
54 |
55 | // if we're offline, fetch can return a value that is not a Response
56 | // instead of throwing - and we can't pass this non-Response to respondWith
57 | if (!(response instanceof Response)) {
58 | throw new Error('invalid response from fetch')
59 | }
60 |
61 | if (response.status === 200) {
62 | cache.put(event.request, response.clone())
63 | }
64 |
65 | return response
66 | } catch (err) {
67 | const response = await cache.match(event.request)
68 |
69 | if (response) {
70 | return response
71 | }
72 |
73 | // if there's no cache, then just error out
74 | // as there is nothing we can do to respond to this request
75 | throw err
76 | }
77 | }
78 |
79 | event.respondWith(respond())
80 | })
81 |
--------------------------------------------------------------------------------
/src/lib/MoveForm.svelte:
--------------------------------------------------------------------------------
1 |
24 |
25 | {#if $state.new_move.status === 'OPEN'}
26 |
61 | {/if}
62 |
63 |
104 |
--------------------------------------------------------------------------------
/src/routes/(app)/time/+page.svelte:
--------------------------------------------------------------------------------
1 |
50 |
51 |
52 | Total Time
53 | {Math.floor(total / 60)}:{(total % 60).toFixed(0).toString().padStart(2, '0')}
54 |
55 |
56 |
57 | Current Round
58 | {time / 100}
59 |
60 |
61 |
62 | Total Rounds
63 | {laps}
64 |
65 |
66 |
67 | {#if timer_status === 'STOPPED' || timer_status === 'PAUSED'}
68 | Start
69 | {/if}
70 | {#if timer_status === 'STOPPED' || timer_status === 'PAUSED'}
71 | Reset
72 | {/if}
73 | {#if timer_status === 'RUNNING'}
74 | Pause
75 | {/if}
76 |
77 |
78 |
115 |
--------------------------------------------------------------------------------
/src/routes/(app)/tools/30s/+page.svelte:
--------------------------------------------------------------------------------
1 |
48 |
49 |
50 | Current Round
51 | {time / 100}
52 |
53 |
54 |
55 | Percentage of Current Round
56 | {(100 * (time / 100 / 30)).toFixed(0)}%
57 |
58 |
59 |
60 | Total Rounds
61 | {laps}
62 |
63 |
64 | Total Time
65 | {total}
66 |
67 |
68 |
69 | Status
70 | {rest_status}
71 |
72 |
73 |
74 | {#if timer_status === 'STOPPED' || timer_status === 'PAUSED'}
75 | Start
76 | {/if}
77 | {#if timer_status === 'STOPPED' || timer_status === 'PAUSED'}
78 | Reset
79 | {/if}
80 | {#if timer_status === 'RUNNING'}
81 | Pause
82 | {/if}
83 |
84 |
85 |
122 |
--------------------------------------------------------------------------------
/src/lib/state/moves.svelte.ts:
--------------------------------------------------------------------------------
1 | import type { MovesResponse } from '../../pocket-types'
2 | import { pb } from '../../pocketbase'
3 | import { localDB } from '../local_db'
4 |
5 | export interface Move {
6 | id: string
7 | collectionId: string
8 | collectionName: string
9 | created: string
10 | updated: string
11 | name: string
12 | props?: number
13 | type?: 'toprock' | 'footwork' | 'go-down' | 'freeze' | 'power' | 'burner'
14 | user?: string
15 | value?: number
16 | needsSync?: boolean
17 | }
18 |
19 | const typeOrder = {
20 | toprock: 1,
21 | 'go-down': 2,
22 | footwork: 3,
23 | power: 4,
24 | freeze: 5
25 | } as const
26 |
27 | function createMoves() {
28 | let moves = $state()
29 | // Set up a more frequent sync interval
30 | setInterval(sync, 30 * 1000)
31 | // Listen for online/offline events
32 | window.addEventListener('online', sync)
33 |
34 | // Set up a real-time subscription if PocketBase supports it
35 | pb.collection('moves').subscribe('*', function (e) {
36 | if (e.action === 'create' || e.action === 'update') {
37 | localDB.moves.put({ ...e.record, needsSync: false })
38 | moves?.push(e.record)
39 | } else if (e.action === 'delete') {
40 | localDB.moves.delete(e.record.id)
41 | }
42 | })
43 |
44 | function remove(move_id: string) {
45 | moves = moves?.filter((move) => move.id !== move_id)
46 | }
47 |
48 | // SYNC METHODS
49 | async function load() {
50 | const userId = pb.authStore.model?.id
51 | if (!userId) {
52 | console.error('User not authenticated')
53 | return []
54 | }
55 |
56 | const localMoves = await localDB.moves.toArray()
57 |
58 | // If there is anything in the database
59 | if (localMoves.length > 0) {
60 | sync() // Trigger sync in background
61 | // Set local moves into state
62 | moves = localMoves
63 | return
64 | }
65 |
66 | try {
67 | const serverMoves = await pb.collection('moves').getFullList({
68 | sort: '-created'
69 | })
70 |
71 | await localDB.moves.bulkAdd(serverMoves.map((move) => ({ ...move, needsSync: false })))
72 | moves = serverMoves
73 | return moves
74 | } catch (error) {
75 | console.error('Failed to fetch moves from server:', error)
76 | return []
77 | }
78 | }
79 |
80 | async function sync() {
81 | console.log('syncing moves...')
82 | if (!navigator.onLine) return
83 |
84 | const unsyncedMoves = await localDB.moves.filter((move) => move.needsSync === true).toArray()
85 |
86 | for (const move of unsyncedMoves) {
87 | try {
88 | if (move.id.startsWith('local_')) {
89 | const { needsSync, id, ...moveData } = move
90 |
91 | const server_move = await pb.collection('moves').create(moveData)
92 | await localDB.moves.delete(move.id)
93 | await localDB.moves.add({
94 | ...server_move,
95 | needsSync: false
96 | })
97 | } else {
98 | const { needsSync, ...moveData } = move
99 | await pb.collection('moves').update(move.id, moveData)
100 | await localDB.moves.update(move.id, { needsSync: false })
101 | }
102 | } catch (error) {
103 | console.error(`Failed to sync move ${move.id}:`, error)
104 | }
105 | }
106 | }
107 |
108 | async function save(move: Partial) {
109 | console.log('saving...', move.name)
110 | const now = new Date()
111 | const updatedMove: Move = {
112 | ...move,
113 | updated: now.toISOString(),
114 | needsSync: true,
115 | id: move.id || `local_${Date.now()}`
116 | } as Move // Type assertion to satisfy TypeScript
117 |
118 | await localDB.moves.put(updatedMove)
119 | sync() // Attempt to sync immediately
120 | }
121 |
122 | return {
123 | load,
124 | get moves() {
125 | return moves
126 | },
127 | remove,
128 | // SYNC METHODS
129 | save,
130 | sync
131 | }
132 | }
133 |
134 | export const moves = createMoves()
135 |
--------------------------------------------------------------------------------
/src/lib/Nav.svelte:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
7 |
8 |
15 |
16 | Move Book
17 |
18 |
19 |
20 |
27 |
28 | Tools
29 |
30 |
31 |
32 |
39 |
40 | Profile
41 |
42 |
43 |
44 |
71 |
--------------------------------------------------------------------------------
/src/routes/(app)/tools/set-builder/+page.svelte:
--------------------------------------------------------------------------------
1 |
148 |
149 |
150 | Add Set
151 |
152 | {#each sets as set, index ('set-builder-set-' + index)}
153 |
154 |
{set.name}
155 |
toggle(index)}>Add Moves
156 |
157 | {#if set?.expand?.moves}
158 |
159 |
handleSort(e, index)}>
160 |
161 | {item.type.charAt(0)} {item.name}
162 | removeMoveFromSet(item, index)}
163 | >Remove
165 |
166 |
167 |
168 | {/if}
169 |
170 | {/each}
171 |
172 |
173 |
174 |
Add Moves to Set
175 |
×
176 |
188 |
189 |
190 |
191 |
280 |
--------------------------------------------------------------------------------
/.github/copilot-instructions.md:
--------------------------------------------------------------------------------
1 | # The Break - Breakdancing Move Management App
2 |
3 | The Break is a Svelte 5 + SvelteKit 2 web application for breakdancers to organize moves, practice with timing tools, and manage battle sets. It uses PocketBase for backend services and is hosted on Cloudflare Pages.
4 |
5 | Always reference these instructions first and fallback to search or bash commands only when you encounter unexpected information that does not match the info here.
6 |
7 | ## Working Effectively
8 |
9 | ### Prerequisites and Installation
10 |
11 | - Install Node.js 17.0.1 exactly (required by .node-version): `wget https://nodejs.org/dist/v17.0.1/node-v17.0.1-linux-x64.tar.xz && tar -xJf node-v17.0.1-linux-x64.tar.xz && export PATH=$PWD/node-v17.0.1-linux-x64/bin:$PATH`
12 | - Install pnpm globally: `npm install -g pnpm`
13 | - Install dependencies: `pnpm install` -- takes 2-17 seconds on first run, ~2 seconds on subsequent runs. NEVER CANCEL.
14 |
15 | ### Core Development Commands
16 |
17 | - Check types: `pnpm run check` -- takes ~5 seconds. EXPECT TypeScript errors (25 errors, 4 warnings). The project builds successfully despite these errors.
18 | - Format code: `pnpm run format` -- takes ~1 second. ALWAYS run this before linting or committing.
19 | - Lint code: `pnpm run lint` -- **WARNING**: ESLint currently fails with "eslint: not found". The Prettier check portion works. Focus on formatting only.
20 | - Build: `pnpm run build` -- takes ~7 seconds. NEVER CANCEL. Set timeout to 30+ seconds.
21 | - Dev server: `pnpm run dev` -- starts in ~2 seconds on http://localhost:5173
22 | - Preview build: `pnpm dlx vite preview --outDir .svelte-kit/output/client` -- serves production build on http://localhost:4173
23 |
24 | ### CRITICAL Build and Timing Information
25 |
26 | - **NEVER CANCEL** any build or install command
27 | - `pnpm install`: 2-17 seconds (first time), 2 seconds (subsequent)
28 | - `pnpm run build`: ~7 seconds - NEVER CANCEL, set timeout to 30+ seconds minimum
29 | - `pnpm run format`: ~1 second
30 | - `pnpm run check`: ~5 seconds (will show TypeScript errors - this is expected)
31 | - All commands complete quickly. If any command hangs for more than 30 seconds, investigate.
32 |
33 | ## Validation Requirements
34 |
35 | ### Manual Testing Scenarios
36 |
37 | After making any changes, ALWAYS test these core user flows:
38 |
39 | 1. **Application Startup**:
40 |
41 | - Run `pnpm run dev`
42 | - Visit http://localhost:5173
43 | - Verify login page loads correctly with dark theme
44 | - Take screenshot to confirm UI rendering
45 |
46 | 2. **Navigation Flow**:
47 |
48 | - Navigate to signup page (/auth/signup)
49 | - Verify form renders correctly with email/password fields
50 | - Test form interaction (typing in fields works)
51 | - Note: External API calls to api.thebreak.app will fail in sandboxed environments (expected)
52 |
53 | 3. **Build Validation**:
54 |
55 | - Run `pnpm run build` and verify it completes successfully
56 | - Run preview server and test that static build works
57 |
58 | 4. **Core Application Pages** (requires authentication in real environment):
59 | - /moves - Move management interface
60 | - /tools - Practice tools overview
61 | - /tools/30s - 30-second timer tool
62 | - /tools/battle-mode - Battle move tracking
63 | - /tools/set-builder - Set creation tool
64 | - /time - Timer interface
65 | - /profile - User profile
66 |
67 | ### Pre-commit Validation
68 |
69 | ALWAYS run these commands before committing changes:
70 |
71 | ```bash
72 | pnpm run format # Fix code formatting
73 | pnpm run build # Ensure build succeeds
74 | ```
75 |
76 | Note: Skip `pnpm run lint` due to ESLint configuration issue. The build command will catch most syntax errors.
77 |
78 | ## Known Issues and Workarounds
79 |
80 | ### Expected Errors
81 |
82 | - **TypeScript Errors**: The project has 25 TypeScript errors and 4 warnings. These are existing issues and do not prevent building or running the application.
83 | - **ESLint Configuration Issue**: The `pnpm run lint` command fails with "eslint: not found". The Prettier portion works correctly for code formatting.
84 | - **API Connection Failures**: In sandboxed environments, calls to `https://api.thebreak.app` will fail. This is expected and does not indicate a problem with your changes.
85 | - **Svelte Version Warning**: The project uses Svelte 5.0.0-next.175 with vite-plugin-svelte@3. The warning about upgrading to vite-plugin-svelte@4 can be ignored.
86 | - **Preview Command**: The `pnpm run preview` script is broken. Use `pnpm dlx vite preview --outDir .svelte-kit/output/client` instead.
87 |
88 | ### Build System Notes
89 |
90 | - Uses pnpm (not npm) as package manager
91 | - Requires Node.js 17.0.1 exactly (check .node-version)
92 | - Build warnings about deprecated `on:click` usage are expected
93 | - Prettier warns about `--plugin-search-dir` flag being ignored (harmless)
94 |
95 | ## Application Architecture
96 |
97 | ### Key Technologies
98 |
99 | - **Frontend**: Svelte 5 + SvelteKit 2
100 | - **Build Tool**: Vite 5.4.4
101 | - **Backend**: PocketBase (https://api.thebreak.app)
102 | - **Deployment**: Cloudflare Pages (using @sveltejs/adapter-cloudflare)
103 | - **Styling**: Custom CSS with CSS variables
104 | - **State Management**: Svelte 5 runes and stores
105 |
106 | ### Important Files and Directories
107 |
108 | ```
109 | src/
110 | ├── routes/
111 | │ ├── (app)/ # Authenticated app routes
112 | │ │ ├── moves/ # Move management
113 | │ │ ├── tools/ # Practice tools
114 | │ │ ├── time/ # Timer functionality
115 | │ │ └── profile/ # User profile
116 | │ └── (site)/ # Public routes (auth)
117 | ├── lib/
118 | │ ├── auth/ # Authentication components
119 | │ ├── state/ # Svelte stores and state
120 | │ └── *.svelte # Reusable components
121 | ├── pocketbase.ts # PocketBase configuration
122 | ├── pocket-types.ts # Generated TypeScript types
123 | └── settings.ts # App configuration
124 | ```
125 |
126 | ### State Management
127 |
128 | - `src/lib/state/moves.svelte.ts` - Move data management
129 | - `src/lib/state/battle_moves.svelte.ts` - Battle mode state
130 | - `src/lib/state/page.ts` - Page title management
131 | - `src/lib/state/session.ts` - User session state
132 |
133 | ### External Dependencies
134 |
135 | - **UI Libraries**: @neodrag/svelte, @rodrigodagostino/svelte-sortable-list
136 | - **Data**: dexie (IndexedDB), pocketbase (backend client)
137 | - **Utilities**: just-kebab-case, sk-form-data
138 |
139 | ## Common Development Tasks
140 |
141 | ### Adding New Features
142 |
143 | - Create new routes in `src/routes/(app)/` for authenticated features
144 | - Add reusable components to `src/lib/`
145 | - Update types in `src/pocket-types.ts` if adding new data models
146 | - Always test with `pnpm run dev` and take screenshots of UI changes
147 |
148 | ### Debugging
149 |
150 | - Check browser console for client-side errors
151 | - Server logs appear in the terminal running `pnpm run dev`
152 | - TypeScript errors can be checked with `pnpm run check`
153 | - Use browser dev tools for styling and responsive issues
154 |
155 | ### Code Style
156 |
157 | - Use Prettier for formatting: `pnpm run format`
158 | - Follow existing patterns for Svelte 5 runes (`$state`, `$derived`, `$effect`)
159 | - Prefer `onclick` over deprecated `on:click` in new code
160 | - Use TypeScript for type safety where possible
161 |
162 | ## Quick Reference Commands
163 |
164 | ```bash
165 | # Fresh setup
166 | pnpm install
167 |
168 | # Development
169 | pnpm run dev # Start dev server
170 | pnpm run format # Format code (always run first)
171 | pnpm run check # Type check (expect errors)
172 | # Note: Skip `pnpm run lint` due to ESLint issue
173 |
174 | # Build and deploy
175 | pnpm run build # Build for production (~7 sec)
176 | pnpm dlx vite preview --outDir .svelte-kit/output/client # Preview build
177 |
178 | # Utilities
179 | pnpm run typegen # Regenerate PocketBase types
180 | ```
181 |
182 | Remember: This project builds and runs successfully despite TypeScript errors. Focus on functionality and user experience rather than fixing existing type issues unless they directly impact your changes.
183 |
--------------------------------------------------------------------------------
/src/lib/style.css:
--------------------------------------------------------------------------------
1 | .space-mono-regular {
2 | font-family: 'Space Mono', monospace;
3 | font-weight: 400;
4 | font-style: normal;
5 | }
6 |
7 | .space-mono-bold {
8 | font-family: 'Space Mono', monospace;
9 | font-weight: 700;
10 | font-style: normal;
11 | }
12 |
13 | .space-mono-regular-italic {
14 | font-family: 'Space Mono', monospace;
15 | font-weight: 400;
16 | font-style: italic;
17 | }
18 |
19 | .space-mono-bold-italic {
20 | font-family: 'Space Mono', monospace;
21 | font-weight: 700;
22 | font-style: italic;
23 | }
24 |
25 | html {
26 | box-sizing: border-box;
27 | font-family: var(--font-sans);
28 | }
29 |
30 | *,
31 | *:before,
32 | *:after {
33 | box-sizing: inherit;
34 | }
35 |
36 | :root {
37 | --black: #111;
38 | --grey: #394851;
39 | --red: #ee352e;
40 | --yellow: #fccc0a;
41 | --green: #6cbe44;
42 |
43 | --white: #fbf0df;
44 | --bg: var(--black);
45 | --color: var(--white);
46 | --power_color: var(--red);
47 | --freeze_color: #02add0;
48 | --footwork_color: var(--yellow);
49 | --toprock_color: var(--green);
50 | --nav_height: 65px;
51 |
52 | --fs-xxs: clamp(0.64rem, 0.752941vi + 0.45rem, 0.96rem);
53 | --fs-xs: clamp(0.8rem, 0.941176vi + 0.56rem, 1.2rem);
54 | --fs-base: clamp(1rem, 1.176471vi + 0.71rem, 1.5rem);
55 | --fs-s: clamp(1.25rem, 1.482353vi + 0.88rem, 1.88rem);
56 | --fs-m: clamp(1.56rem, 1.835294vi + 1.1rem, 2.34rem);
57 | --fs-l: clamp(1.95rem, 2.305882vi + 1.37rem, 2.93rem);
58 | --fs-xl: clamp(2.44rem, 2.870588vi + 1.72rem, 3.66rem);
59 | --fs-xxl: clamp(3.05rem, 3.6vi + 2.15rem, 4.58rem);
60 | --fs-xxxl: clamp(3.81rem, 4.494118vi + 2.69rem, 5.72rem);
61 | }
62 |
63 | :where(html) {
64 | --font-sans: system-ui, -apple-system, Segoe UI, Roboto, Ubuntu, Cantarell, Noto Sans, sans-serif,
65 | Segoe UI, Roboto, Ubuntu, Cantarell, Noto Sans, sans-serif;
66 | --font-serif: ui-serif, serif;
67 | --font-mono: Dank Mono, Operator Mono, Inconsolata, Fira Mono, ui-monospace, SF Mono, Monaco,
68 | Droid Sans Mono, Source Code Pro, monospace;
69 | --font-weight-1: 100;
70 | --font-weight-2: 200;
71 | --font-weight-3: 300;
72 | --font-weight-4: 400;
73 | --font-weight-5: 500;
74 | --font-weight-6: 600;
75 | --font-weight-7: 700;
76 | --font-weight-8: 800;
77 | --font-weight-9: 900;
78 | --font-lineheight-00: 0.95;
79 | --font-lineheight-0: 1.1;
80 | --font-lineheight-1: 1.25;
81 | --font-lineheight-2: 1.375;
82 | --font-lineheight-3: 1.5;
83 | --font-lineheight-4: 1.75;
84 | --font-lineheight-5: 2;
85 | --font-letterspacing-0: -0.05em;
86 | --font-letterspacing-1: 0.025em;
87 | --font-letterspacing-2: 0.05em;
88 | --font-letterspacing-3: 0.075em;
89 | --font-letterspacing-4: 0.15em;
90 | --font-letterspacing-5: 0.5em;
91 | --font-letterspacing-6: 0.75em;
92 | --font-letterspacing-7: 1em;
93 | --font-size-00: 0.5rem;
94 | --font-size-0: 0.75rem;
95 | --font-size-1: 1rem;
96 | --font-size-2: 1.1rem;
97 | --font-size-3: 1.25rem;
98 | --font-size-4: 1.5rem;
99 | --font-size-5: 2rem;
100 | --font-size-6: 2.5rem;
101 | --font-size-7: 3rem;
102 | --font-size-8: 3.5rem;
103 | --font-size-fluid-0: clamp(0.75rem, 2vw, 1rem);
104 | --font-size-fluid-1: clamp(1rem, 4vw, 1.5rem);
105 | --font-size-fluid-2: clamp(1.5rem, 6vw, 2.5rem);
106 | --font-size-fluid-3: clamp(2rem, 9vw, 3.5rem);
107 | --size-000: -0.5rem;
108 | --size-00: -0.25rem;
109 | --size-1: 0.25rem;
110 | --size-2: 0.5rem;
111 | --size-3: 1rem;
112 | --size-4: 1.25rem;
113 | --size-5: 1.5rem;
114 | --size-6: 1.75rem;
115 | --size-7: 2rem;
116 | --size-8: 3rem;
117 | --size-9: 4rem;
118 | --size-10: 5rem;
119 | --size-11: 7.5rem;
120 | --size-12: 10rem;
121 | --size-13: 15rem;
122 | --size-14: 20rem;
123 | --size-15: 30rem;
124 | --size-fluid-1: clamp(0.5rem, 1vw, 1rem);
125 | --size-fluid-2: clamp(1rem, 2vw, 1.5rem);
126 | --size-fluid-3: clamp(1.5rem, 3vw, 2rem);
127 | --size-fluid-4: clamp(2rem, 4vw, 3rem);
128 | --size-fluid-5: clamp(4rem, 5vw, 5rem);
129 | --size-fluid-6: clamp(5rem, 7vw, 7.5rem);
130 | --size-fluid-7: clamp(7.5rem, 10vw, 10rem);
131 | --size-fluid-8: clamp(10rem, 20vw, 15rem);
132 | --size-fluid-9: clamp(15rem, 30vw, 20rem);
133 | --size-fluid-10: clamp(20rem, 40vw, 30rem);
134 | --size-content-1: 20ch;
135 | --size-content-2: 45ch;
136 | --size-content-3: 60ch;
137 | --size-header-1: 20ch;
138 | --size-header-2: 25ch;
139 | --size-header-3: 35ch;
140 | --size-xxs: 240px;
141 | --size-xs: 360px;
142 | --size-sm: 480px;
143 | --size-md: 768px;
144 | --size-lg: 1024px;
145 | --size-xl: 1440px;
146 | --size-xxl: 1920px;
147 | --ease-1: cubic-bezier(0.25, 0, 0.5, 1);
148 | --ease-2: cubic-bezier(0.25, 0, 0.4, 1);
149 | --ease-3: cubic-bezier(0.25, 0, 0.3, 1);
150 | --ease-4: cubic-bezier(0.25, 0, 0.2, 1);
151 | --ease-5: cubic-bezier(0.25, 0, 0.1, 1);
152 | --ease-in-1: cubic-bezier(0.25, 0, 1, 1);
153 | --ease-in-2: cubic-bezier(0.5, 0, 1, 1);
154 | --ease-in-3: cubic-bezier(0.7, 0, 1, 1);
155 | --ease-in-4: cubic-bezier(0.9, 0, 1, 1);
156 | --ease-in-5: cubic-bezier(1, 0, 1, 1);
157 | --ease-out-1: cubic-bezier(0, 0, 0.75, 1);
158 | --ease-out-2: cubic-bezier(0, 0, 0.5, 1);
159 | --ease-out-3: cubic-bezier(0, 0, 0.3, 1);
160 | --ease-out-4: cubic-bezier(0, 0, 0.1, 1);
161 | --ease-out-5: cubic-bezier(0, 0, 0, 1);
162 | --ease-in-out-1: cubic-bezier(0.1, 0, 0.9, 1);
163 | --ease-in-out-2: cubic-bezier(0.3, 0, 0.7, 1);
164 | --ease-in-out-3: cubic-bezier(0.5, 0, 0.5, 1);
165 | --ease-in-out-4: cubic-bezier(0.7, 0, 0.3, 1);
166 | --ease-in-out-5: cubic-bezier(0.9, 0, 0.1, 1);
167 | --ease-elastic-1: cubic-bezier(0.5, 0.75, 0.75, 1.25);
168 | --ease-elastic-2: cubic-bezier(0.5, 1, 0.75, 1.25);
169 | --ease-elastic-3: cubic-bezier(0.5, 1.25, 0.75, 1.25);
170 | --ease-elastic-4: cubic-bezier(0.5, 1.5, 0.75, 1.25);
171 | --ease-elastic-5: cubic-bezier(0.5, 1.75, 0.75, 1.25);
172 | --ease-squish-1: cubic-bezier(0.5, -0.1, 0.1, 1.5);
173 | --ease-squish-2: cubic-bezier(0.5, -0.3, 0.1, 1.5);
174 | --ease-squish-3: cubic-bezier(0.5, -0.5, 0.1, 1.5);
175 | --ease-squish-4: cubic-bezier(0.5, -0.7, 0.1, 1.5);
176 | --ease-squish-5: cubic-bezier(0.5, -0.9, 0.1, 1.5);
177 | --ease-step-1: steps(2);
178 | --ease-step-2: steps(3);
179 | --ease-step-3: steps(4);
180 | --ease-step-4: steps(7);
181 | --ease-step-5: steps(10);
182 | --layer-1: 1;
183 | --layer-2: 2;
184 | --layer-3: 3;
185 | --layer-4: 4;
186 | --layer-5: 5;
187 | --layer-important: 2147483647;
188 | --shadow-color: 220 3% 15%;
189 | --shadow-strength: 1%;
190 | --shadow-1: 0 1px 2px -1px hsl(var(--shadow-color) / calc(var(--shadow-strength) + 9%));
191 | --shadow-2: 0 3px 5px -2px hsl(var(--shadow-color) / calc(var(--shadow-strength) + 3%)),
192 | 0 7px 14px -5px hsl(var(--shadow-color) / calc(var(--shadow-strength) + 5%));
193 | --shadow-3: 0 -1px 3px 0 hsl(var(--shadow-color) / calc(var(--shadow-strength) + 2%)),
194 | 0 1px 2px -5px hsl(var(--shadow-color) / calc(var(--shadow-strength) + 2%)),
195 | 0 2px 5px -5px hsl(var(--shadow-color) / calc(var(--shadow-strength) + 4%)),
196 | 0 4px 12px -5px hsl(var(--shadow-color) / calc(var(--shadow-strength) + 5%)),
197 | 0 12px 15px -5px hsl(var(--shadow-color) / calc(var(--shadow-strength) + 7%));
198 | --shadow-4: 0 -2px 5px 0 hsl(var(--shadow-color) / calc(var(--shadow-strength) + 2%)),
199 | 0 1px 1px -2px hsl(var(--shadow-color) / calc(var(--shadow-strength) + 3%)),
200 | 0 2px 2px -2px hsl(var(--shadow-color) / calc(var(--shadow-strength) + 3%)),
201 | 0 5px 5px -2px hsl(var(--shadow-color) / calc(var(--shadow-strength) + 4%)),
202 | 0 9px 9px -2px hsl(var(--shadow-color) / calc(var(--shadow-strength) + 5%)),
203 | 0 16px 16px -2px hsl(var(--shadow-color) / calc(var(--shadow-strength) + 6%));
204 | --shadow-5: 0 -1px 2px 0 hsl(var(--shadow-color) / calc(var(--shadow-strength) + 2%)),
205 | 0 2px 1px -2px hsl(var(--shadow-color) / calc(var(--shadow-strength) + 3%)),
206 | 0 5px 5px -2px hsl(var(--shadow-color) / calc(var(--shadow-strength) + 3%)),
207 | 0 10px 10px -2px hsl(var(--shadow-color) / calc(var(--shadow-strength) + 4%)),
208 | 0 20px 20px -2px hsl(var(--shadow-color) / calc(var(--shadow-strength) + 5%)),
209 | 0 40px 40px -2px hsl(var(--shadow-color) / calc(var(--shadow-strength) + 7%));
210 | --shadow-6: 0 -1px 2px 0 hsl(var(--shadow-color) / calc(var(--shadow-strength) + 2%)),
211 | 0 3px 2px -2px hsl(var(--shadow-color) / calc(var(--shadow-strength) + 3%)),
212 | 0 7px 5px -2px hsl(var(--shadow-color) / calc(var(--shadow-strength) + 3%)),
213 | 0 12px 10px -2px hsl(var(--shadow-color) / calc(var(--shadow-strength) + 4%)),
214 | 0 22px 18px -2px hsl(var(--shadow-color) / calc(var(--shadow-strength) + 5%)),
215 | 0 41px 33px -2px hsl(var(--shadow-color) / calc(var(--shadow-strength) + 6%)),
216 | 0 100px 80px -2px hsl(var(--shadow-color) / calc(var(--shadow-strength) + 7%));
217 | --inner-shadow-0: inset 0 0 0 1px hsl(var(--shadow-color) / calc(var(--shadow-strength) + 9%));
218 | --inner-shadow-1: inset 0 1px 2px 0 hsl(var(--shadow-color) / calc(var(--shadow-strength) + 9%));
219 | --inner-shadow-2: inset 0 1px 4px 0 hsl(var(--shadow-color) / calc(var(--shadow-strength) + 9%));
220 | --inner-shadow-3: inset 0 2px 8px 0 hsl(var(--shadow-color) / calc(var(--shadow-strength) + 9%));
221 | --inner-shadow-4: inset 0 2px 14px 0 hsl(var(--shadow-color) / calc(var(--shadow-strength) + 9%));
222 | --ratio-square: 1;
223 | --ratio-landscape: 4/3;
224 | --ratio-portrait: 3/4;
225 | --ratio-widescreen: 16/9;
226 | --ratio-ultrawide: 18/5;
227 | --ratio-golden: 1.618/1;
228 | --gray-0: #f8f9fa;
229 | --gray-1: #f1f3f5;
230 | --gray-2: #e9ecef;
231 | --gray-3: #dee2e6;
232 | --gray-4: #ced4da;
233 | --gray-5: #adb5bd;
234 | --gray-6: #868e96;
235 | --gray-7: #495057;
236 | --gray-8: #343a40;
237 | --gray-9: #212529;
238 | --red-0: #fff5f5;
239 | --red-1: #ffe3e3;
240 | --red-2: #ffc9c9;
241 | --red-3: #ffa8a8;
242 | --red-4: #ff8787;
243 | --red-5: #ff6b6b;
244 | --red-6: #fa5252;
245 | --red-7: #f03e3e;
246 | --red-8: #e03131;
247 | --red-9: #c92a2a;
248 | --pink-0: #fff0f6;
249 | --pink-1: #ffdeeb;
250 | --pink-2: #fcc2d7;
251 | --pink-3: #faa2c1;
252 | --pink-4: #f783ac;
253 | --pink-5: #f06595;
254 | --pink-6: #e64980;
255 | --pink-7: #d6336c;
256 | --pink-8: #c2255c;
257 | --pink-9: #a61e4d;
258 | --grape-0: #f8f0fc;
259 | --grape-1: #f3d9fa;
260 | --grape-2: #eebefa;
261 | --grape-3: #e599f7;
262 | --grape-4: #da77f2;
263 | --grape-5: #cc5de8;
264 | --grape-6: #be4bdb;
265 | --grape-7: #ae3ec9;
266 | --grape-8: #9c36b5;
267 | --grape-9: #862e9c;
268 | --violet-0: #f3f0ff;
269 | --violet-1: #e5dbff;
270 | --violet-2: #d0bfff;
271 | --violet-3: #b197fc;
272 | --violet-4: #9775fa;
273 | --violet-5: #845ef7;
274 | --violet-6: #7950f2;
275 | --violet-7: #7048e8;
276 | --violet-8: #6741d9;
277 | --violet-9: #5f3dc4;
278 | --indigo-0: #edf2ff;
279 | --indigo-1: #dbe4ff;
280 | --indigo-2: #bac8ff;
281 | --indigo-3: #91a7ff;
282 | --indigo-4: #748ffc;
283 | --indigo-5: #5c7cfa;
284 | --indigo-6: #4c6ef5;
285 | --indigo-7: #4263eb;
286 | --indigo-8: #3b5bdb;
287 | --indigo-9: #364fc7;
288 | --blue-0: #e7f5ff;
289 | --blue-1: #d0ebff;
290 | --blue-2: #a5d8ff;
291 | --blue-3: #74c0fc;
292 | --blue-4: #4dabf7;
293 | --blue-5: #339af0;
294 | --blue-6: #228be6;
295 | --blue-7: #1c7ed6;
296 | --blue-8: #1971c2;
297 | --blue-9: #1864ab;
298 | --cyan-0: #e3fafc;
299 | --cyan-1: #c5f6fa;
300 | --cyan-2: #99e9f2;
301 | --cyan-3: #66d9e8;
302 | --cyan-4: #3bc9db;
303 | --cyan-5: #22b8cf;
304 | --cyan-6: #15aabf;
305 | --cyan-7: #1098ad;
306 | --cyan-8: #0c8599;
307 | --cyan-9: #0b7285;
308 | --teal-0: #e6fcf5;
309 | --teal-1: #c3fae8;
310 | --teal-2: #96f2d7;
311 | --teal-3: #63e6be;
312 | --teal-4: #38d9a9;
313 | --teal-5: #20c997;
314 | --teal-6: #12b886;
315 | --teal-7: #0ca678;
316 | --teal-8: #099268;
317 | --teal-9: #087f5b;
318 | --green-0: #ebfbee;
319 | --green-1: #d3f9d8;
320 | --green-2: #b2f2bb;
321 | --green-3: #8ce99a;
322 | --green-4: #69db7c;
323 | --green-5: #51cf66;
324 | --green-6: #40c057;
325 | --green-7: #37b24d;
326 | --green-8: #2f9e44;
327 | --green-9: #2b8a3e;
328 | --lime-0: #f4fce3;
329 | --lime-1: #e9fac8;
330 | --lime-2: #d8f5a2;
331 | --lime-3: #c0eb75;
332 | --lime-4: #a9e34b;
333 | --lime-5: #94d82d;
334 | --lime-6: #82c91e;
335 | --lime-7: #74b816;
336 | --lime-8: #66a80f;
337 | --lime-9: #5c940d;
338 | --yellow-0: #fff9db;
339 | --yellow-1: #fff3bf;
340 | --yellow-2: #ffec99;
341 | --yellow-3: #ffe066;
342 | --yellow-4: #ffd43b;
343 | --yellow-5: #fcc419;
344 | --yellow-6: #fab005;
345 | --yellow-7: #f59f00;
346 | --yellow-8: #f08c00;
347 | --yellow-9: #e67700;
348 | --orange-0: #fff4e6;
349 | --orange-1: #ffe8cc;
350 | --orange-2: #ffd8a8;
351 | --orange-3: #ffc078;
352 | --orange-4: #ffa94d;
353 | --orange-5: #ff922b;
354 | --orange-6: #fd7e14;
355 | --orange-7: #f76707;
356 | --orange-8: #e8590c;
357 | --orange-9: #d9480f;
358 | --gradient-1: linear-gradient(
359 | to bottom right,
360 | #1f005c,
361 | #5b0060,
362 | #870160,
363 | #ac255e,
364 | #ca485c,
365 | #e16b5c,
366 | #f39060,
367 | #ffb56b
368 | );
369 | --gradient-2: linear-gradient(to bottom right, #48005c, #8300e2, #a269ff);
370 | --gradient-3: radial-gradient(circle at top right, #0ff, rgba(0, 255, 255, 0)),
371 | radial-gradient(circle at bottom left, #ff1492, rgba(255, 20, 146, 0));
372 | --gradient-4: linear-gradient(to bottom right, #00f5a0, #00d9f5);
373 | --gradient-5: conic-gradient(from -270deg at 75%, at 110%, #f0f, #fffaf0);
374 | --gradient-5: conic-gradient(from -270deg at 75% 110%, #f0f, #fffaf0);
375 | --gradient-6: conic-gradient(from -90deg at top left, #000, #fff);
376 | --gradient-7: linear-gradient(to bottom right, #72c6ef, #004e8f);
377 | --gradient-8: conic-gradient(from 90deg at 50%, at 0%, #111, 50%, #222, #111);
378 | --gradient-8: conic-gradient(from 90deg at 50% 0%, #111, 50%, #222, #111);
379 | --gradient-9: conic-gradient(from 0.5turn at bottom center, #add8e6, #fff);
380 | --gradient-10: conic-gradient(
381 | from 90deg at 40%,
382 | at -25%,
383 | gold,
384 | #f79d03,
385 | #ee6907,
386 | #e6390a,
387 | #de0d0d,
388 | #d61039,
389 | #cf1261,
390 | #c71585,
391 | #cf1261,
392 | #d61039,
393 | #de0d0d,
394 | #ee6907,
395 | #f79d03,
396 | gold,
397 | gold,
398 | gold
399 | );
400 | --gradient-10: conic-gradient(
401 | from 90deg at 40% -25%,
402 | gold,
403 | #f79d03,
404 | #ee6907,
405 | #e6390a,
406 | #de0d0d,
407 | #d61039,
408 | #cf1261,
409 | #c71585,
410 | #cf1261,
411 | #d61039,
412 | #de0d0d,
413 | #ee6907,
414 | #f79d03,
415 | gold,
416 | gold,
417 | gold
418 | );
419 | --gradient-11: conic-gradient(at bottom left, #ff1493, cyan);
420 | --gradient-12: conic-gradient(
421 | from 90deg at 25%,
422 | at -10%,
423 | #ff4500,
424 | #d3f340,
425 | #7bee85,
426 | #afeeee,
427 | #7bee85
428 | );
429 | --gradient-12: conic-gradient(
430 | from 90deg at 25% -10%,
431 | #ff4500,
432 | #d3f340,
433 | #7bee85,
434 | #afeeee,
435 | #7bee85
436 | );
437 | --gradient-13: radial-gradient(
438 | circle at 50%,
439 | at 200%,
440 | #000142,
441 | #3b0083,
442 | #b300c3,
443 | #ff059f,
444 | #ff4661,
445 | #ffad86,
446 | #fff3c7
447 | );
448 | --gradient-13: radial-gradient(
449 | circle at 50% 200%,
450 | #000142,
451 | #3b0083,
452 | #b300c3,
453 | #ff059f,
454 | #ff4661,
455 | #ffad86,
456 | #fff3c7
457 | );
458 | --gradient-14: conic-gradient(at top right, lime, cyan);
459 | --gradient-15: linear-gradient(to bottom right, #c7d2fe, #fecaca, #fef3c7);
460 | --gradient-16: radial-gradient(circle at 50%, at -250%, #374151, #111827, #000);
461 | --gradient-16: radial-gradient(circle at 50% -250%, #374151, #111827, #000);
462 | --gradient-17: conic-gradient(from -90deg at 50%, at -25%, blue, #8a2be2);
463 | --gradient-17: conic-gradient(from -90deg at 50% -25%, blue, #8a2be2);
464 | --gradient-18: linear-gradient(0deg, rgba(255, 0, 0, 0.8), rgba(255, 0, 0, 0) 75%),
465 | linear-gradient(60deg, rgba(255, 255, 0, 0.8), rgba(255, 255, 0, 0) 75%),
466 | linear-gradient(120deg, rgba(0, 255, 0, 0.8), rgba(0, 255, 0, 0) 75%),
467 | linear-gradient(180deg, rgba(0, 255, 255, 0.8), rgba(0, 255, 255, 0) 75%),
468 | linear-gradient(240deg, rgba(0, 0, 255, 0.8), rgba(0, 0, 255, 0) 75%),
469 | linear-gradient(300deg, rgba(255, 0, 255, 0.8), rgba(255, 0, 255, 0) 75%);
470 | --gradient-19: linear-gradient(to bottom right, #ffe259, #ffa751);
471 | --gradient-20: conic-gradient(
472 | from -135deg at -10% center,
473 | orange,
474 | #ff7715,
475 | #ff522a,
476 | #ff3f47,
477 | #ff5482,
478 | #ff69b4
479 | );
480 | --gradient-21: conic-gradient(
481 | from -90deg at 25%,
482 | at 115%,
483 | red,
484 | #f06,
485 | #f0c,
486 | #c0f,
487 | #60f,
488 | #00f,
489 | #00f,
490 | #00f,
491 | #00f
492 | );
493 | --gradient-21: conic-gradient(
494 | from -90deg at 25% 115%,
495 | red,
496 | #f06,
497 | #f0c,
498 | #c0f,
499 | #60f,
500 | #00f,
501 | #00f,
502 | #00f,
503 | #00f
504 | );
505 | --gradient-22: linear-gradient(to bottom right, #acb6e5, #86fde8);
506 | --gradient-23: linear-gradient(to bottom right, #536976, #292e49);
507 | --gradient-24: conic-gradient(from 0.5turn at 0%, at 0%, #00c476, 10%, #82b0ff, 90%, #00c476);
508 | --gradient-24: conic-gradient(from 0.5turn at 0% 0%, #00c476, 10%, #82b0ff, 90%, #00c476);
509 | --gradient-25: conic-gradient(at 125%, at 50%, #b78cf7, #ff7c94, #ffcf0d, #ff7c94, #b78cf7);
510 | --gradient-25: conic-gradient(at 125% 50%, #b78cf7, #ff7c94, #ffcf0d, #ff7c94, #b78cf7);
511 | --gradient-26: linear-gradient(to bottom right, #9796f0, #fbc7d4);
512 | --gradient-27: conic-gradient(from 0.5turn at bottom left, #ff1493, #639);
513 | --gradient-28: conic-gradient(from -90deg at 50%, at 105%, #fff, orchid);
514 | --gradient-28: conic-gradient(from -90deg at 50% 105%, #fff, orchid);
515 | --gradient-29: radial-gradient(circle at top right, #bfb3ff, rgba(191, 179, 255, 0)),
516 | radial-gradient(circle at bottom left, #86acf9, rgba(134, 172, 249, 0));
517 | --gradient-30: radial-gradient(circle at top right, #00ff80, rgba(0, 255, 128, 0)),
518 | radial-gradient(circle at bottom left, #adffd6, rgba(173, 255, 214, 0));
519 | --noise-1: url("data:image/svg+xml;charset=utf-8,%3Csvg viewBox='0 0 200 200' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='a'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.005' numOctaves='2' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23a)'/%3E%3C/svg%3E");
520 | --noise-2: url("data:image/svg+xml;charset=utf-8,%3Csvg viewBox='0 0 300 300' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='a'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.05' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23a)'/%3E%3C/svg%3E");
521 | --noise-3: url("data:image/svg+xml;charset=utf-8,%3Csvg viewBox='0 0 1024 1024' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='a'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.25' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23a)'/%3E%3C/svg%3E");
522 | --noise-4: url("data:image/svg+xml;charset=utf-8,%3Csvg viewBox='0 0 2056 2056' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='a'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.5' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23a)'/%3E%3C/svg%3E");
523 | --noise-5: url("data:image/svg+xml;charset=utf-8,%3Csvg viewBox='0 0 2056 2056' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='a'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.75' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23a)'/%3E%3C/svg%3E");
524 | --noise-filter-1: contrast(300%) brightness(100%);
525 | --noise-filter-2: contrast(200%) brightness(150%);
526 | --noise-filter-3: contrast(200%) brightness(250%);
527 | --noise-filter-4: contrast(200%) brightness(500%);
528 | --noise-filter-5: contrast(200%) brightness(1000%);
529 | --animation-fade-in: fade-in 0.5s var(--ease-3);
530 | --animation-fade-out: fade-out 0.5s var(--ease-3);
531 | --animation-scale-up: scale-up 0.5s var(--ease-3);
532 | --animation-scale-down: scale-down 0.5s var(--ease-3);
533 | --animation-slide-out-up: slide-out-up 0.5s var(--ease-3);
534 | --animation-slide-out-down: slide-out-down 0.5s var(--ease-3);
535 | --animation-slide-out-right: slide-out-right 0.5s var(--ease-3);
536 | --animation-slide-out-left: slide-out-left 0.5s var(--ease-3);
537 | --animation-slide-in-up: slide-in-up 0.5s var(--ease-3);
538 | --animation-slide-in-down: slide-in-down 0.5s var(--ease-3);
539 | --animation-slide-in-right: slide-in-right 0.5s var(--ease-3);
540 | --animation-slide-in-left: slide-in-left 0.5s var(--ease-3);
541 | --animation-shake-x: shake-x 0.75s var(--ease-out-5);
542 | --animation-shake-y: shake-y 0.75s var(--ease-out-5);
543 | --animation-spin: spin 2s linear infinite;
544 | --animation-ping: ping 5s var(--ease-out-3) infinite;
545 | --animation-blink: blink 1s var(--ease-out-3) infinite;
546 | --animation-float: float 3s var(--ease-in-out-3) infinite;
547 | --animation-bounce: bounce 2s var(--ease-squish-2) infinite;
548 | --animation-pulse: pulse 2s var(--ease-out-3) infinite;
549 | --border-size-1: 1px;
550 | --border-size-2: 2px;
551 | --border-size-3: 5px;
552 | --border-size-4: 10px;
553 | --border-size-5: 25px;
554 | --radius-1: 2px;
555 | --radius-2: 5px;
556 | --radius-3: 1rem;
557 | --radius-4: 2rem;
558 | --radius-5: 4rem;
559 | --radius-6: 8rem;
560 | --radius-round: 1e5px;
561 | --radius-blob-1: 30% 70% 70% 30%/53% 30% 70% 47%;
562 | --radius-blob-2: 53% 47% 34% 66%/63% 46% 54% 37%;
563 | --radius-blob-3: 37% 63% 56% 44%/49% 56% 44% 51%;
564 | --radius-blob-4: 63% 37% 37% 63%/43% 37% 63% 57%;
565 | --radius-blob-5: 49% 51% 48% 52%/57% 44% 56% 43%;
566 | --radius-conditional-1: clamp(0px, calc(100vw - 100%) * 1e5, var(--radius-1));
567 | --radius-conditional-2: clamp(0px, calc(100vw - 100%) * 1e5, var(--radius-2));
568 | --radius-conditional-3: clamp(0px, calc(100vw - 100%) * 1e5, var(--radius-3));
569 | --radius-conditional-4: clamp(0px, calc(100vw - 100%) * 1e5, var(--radius-4));
570 | --radius-conditional-5: clamp(0px, calc(100vw - 100%) * 1e5, var(--radius-5));
571 | --radius-conditional-6: clamp(0px, calc(100vw - 100%) * 1e5, var(--radius-6));
572 | }
573 |
574 | @media (prefers-color-scheme: dark) {
575 | :where(html) {
576 | --shadow-color: 220 40% 2%;
577 | --shadow-strength: 25%;
578 | }
579 | }
580 | @keyframes fade-in {
581 | to {
582 | opacity: 1;
583 | }
584 | }
585 | @keyframes fade-out {
586 | to {
587 | opacity: 0;
588 | }
589 | }
590 | @keyframes scale-up {
591 | to {
592 | transform: scale(1.25);
593 | }
594 | }
595 | @keyframes scale-down {
596 | to {
597 | transform: scale(0.75);
598 | }
599 | }
600 | @keyframes slide-out-up {
601 | to {
602 | transform: translateY(-100%);
603 | }
604 | }
605 | @keyframes slide-out-down {
606 | to {
607 | transform: translateY(100%);
608 | }
609 | }
610 | @keyframes slide-out-right {
611 | to {
612 | transform: translateX(100%);
613 | }
614 | }
615 | @keyframes slide-out-left {
616 | to {
617 | transform: translateX(-100%);
618 | }
619 | }
620 | @keyframes slide-in-up {
621 | 0% {
622 | transform: translateY(100%);
623 | }
624 | }
625 | @keyframes slide-in-down {
626 | 0% {
627 | transform: translateY(-100%);
628 | }
629 | }
630 | @keyframes slide-in-right {
631 | 0% {
632 | transform: translateX(-100%);
633 | }
634 | }
635 | @keyframes slide-in-left {
636 | 0% {
637 | transform: translateX(100%);
638 | }
639 | }
640 | @keyframes shake-x {
641 | 0%,
642 | to {
643 | transform: translateX(0);
644 | }
645 | 20% {
646 | transform: translateX(-5%);
647 | }
648 | 40% {
649 | transform: translateX(5%);
650 | }
651 | 60% {
652 | transform: translateX(-5%);
653 | }
654 | 80% {
655 | transform: translateX(5%);
656 | }
657 | }
658 | @keyframes shake-y {
659 | 0%,
660 | to {
661 | transform: translateY(0);
662 | }
663 | 20% {
664 | transform: translateY(-5%);
665 | }
666 | 40% {
667 | transform: translateY(5%);
668 | }
669 | 60% {
670 | transform: translateY(-5%);
671 | }
672 | 80% {
673 | transform: translateY(5%);
674 | }
675 | }
676 | @keyframes spin {
677 | to {
678 | transform: rotate(1turn);
679 | }
680 | }
681 | @keyframes ping {
682 | 90%,
683 | to {
684 | opacity: 0;
685 | transform: scale(2);
686 | }
687 | }
688 | @keyframes blink {
689 | 0%,
690 | to {
691 | opacity: 1;
692 | }
693 | 50% {
694 | opacity: 0.5;
695 | }
696 | }
697 | @keyframes float {
698 | 50% {
699 | transform: translateY(-25%);
700 | }
701 | }
702 | @keyframes bounce {
703 | 25% {
704 | transform: translateY(-20%);
705 | }
706 | 40% {
707 | transform: translateY(-3%);
708 | }
709 | 0%,
710 | 60%,
711 | to {
712 | transform: translateY(0);
713 | }
714 | }
715 | @keyframes pulse {
716 | 50% {
717 | transform: scale(0.9);
718 | }
719 | }
720 |
721 | /* MY CSS */
722 |
723 | body {
724 | background: var(--black);
725 | min-height: 100vh;
726 | }
727 |
728 | .content {
729 | margin: 1rem 0;
730 | }
731 |
732 | .readable {
733 | max-width: 900px;
734 | width: 100%;
735 | }
736 |
737 | .flex {
738 | display: flex;
739 | gap: 10px;
740 | }
741 |
742 | .row {
743 | margin-bottom: 1rem;
744 | }
745 |
746 | .error {
747 | color: var(--red-6);
748 | }
749 |
750 | .form {
751 | border-radius: 30px;
752 | padding: 40px;
753 | background-color: rgba(255, 255, 255, 0.02);
754 | box-shadow: 0 1px 10px 1px rgb(0 0 0 / 0.1);
755 | margin-bottom: 2rem;
756 | }
757 |
758 | label {
759 | font-size: 12px;
760 | font-style: italic;
761 | display: block;
762 | }
763 |
764 | input[type='submit'],
765 | button {
766 | color: var(--bg);
767 | background: var(--footwork_color);
768 | border: solid 2px var(--footwork_color);
769 | box-shadow: var(--shadow-3);
770 | font-weight: bold;
771 | border-radius: 8px;
772 | font-size: 18px;
773 | padding: 10px;
774 | cursor: pointer;
775 | display: flex;
776 | justify-content: center;
777 | align-items: center;
778 | gap: 10px;
779 | }
780 |
781 | .ghost {
782 | color: var(--footwork_color);
783 | background-color: transparent;
784 | }
785 |
786 | .warning-btn {
787 | border-color: var(--red);
788 | color: var(--fg);
789 | background-color: transparent;
790 | padding: 4px 8px;
791 | }
792 | .go-btn {
793 | border-color: var(--green);
794 | color: var(--fg);
795 | background-color: transparent;
796 |
797 | padding: 4px 8px;
798 | }
799 |
800 | .small {
801 | font-size: 14px;
802 | }
803 |
804 | body {
805 | font-family: Helvetica, sans-serif;
806 | padding: 0;
807 | margin: 0;
808 | color: var(--color);
809 | }
810 | a {
811 | color: var(--white);
812 | text-decoration: none;
813 | }
814 | input,
815 | select {
816 | color: var(--white);
817 | background: transparent;
818 | border: solid 1px var(--white);
819 | border-radius: 3px;
820 | font-size: var(--fs-base);
821 | width: 100%;
822 | padding: 8px;
823 | }
824 |
825 | label {
826 | font-size: 14px;
827 | margin-bottom: 5px;
828 | color: var(--white);
829 | opacity: 0.5;
830 | display: block;
831 | }
832 | .center {
833 | text-align: center;
834 | }
835 |
836 | .btn-small {
837 | font-size: var(--fs-xxs);
838 | }
839 |
840 | .row {
841 | margin-block: 2rem;
842 | }
843 |
--------------------------------------------------------------------------------