Composables
useFlash ()
Returns the flash notification store. Must be called within a component's setup context.
const flash = useFlash ();
| Method | Signature | Description |
|---|---|---|
success | (message, options?) → id | Green toast |
error | (message, options?) → id | Red toast, 10s default |
warning | (message, options?) → id | Amber toast |
info | (message, options?) → id | Blue toast |
dismiss | (id) → void | Dismiss a specific message |
dismissAll | () → void | Clear all messages |
Options: { title?: string, duration?: number }
useApi ()
Access the
KATFORGE @katforge/api instance from any component. Returns the exact KATforge object registered via createSpark ({ katforge }). Despite living in the Spark package, this is the SDK — not a UI wrapper.
import { useApi } from '@katforge/spark';
const api = useApi ();
await api.auth.login (email, password);
useSpark ()is a deprecated alias ofuseApi ()kept for backward compatibility. It will be removed in a future release.
usePlayer ()
Returns a Vue Ref<Player | null> that tracks the authenticated player. Subscribes to the SDK's auth:change event internally and tears down on component unmount — the common "show the current user in the header" pattern without the boilerplate.
<script setup>
import { usePlayer } from '@katforge/spark';
const player = usePlayer ();
</script>
<template>
<span v-if="player">{{ player.display_name }}</span>
<span v-else>Sign in</span>
</template>
For non-Vue consumers or lower-level control, subscribe to api.on ('auth:change', handler) directly.
useFeedback ()
Returns Ref<FeedbackHandle | null> — a reactive handle on the currently-mounted SparkFeedback bubble. Use it to open the feedback modal from anywhere in the app: a toolbar button, a keyboard shortcut, an error boundary's "Report this" link.
<script setup>
import { useFeedback } from '@katforge/spark';
const feedback = useFeedback ();
function reportBug () {
feedback.value?.open ('bug');
}
</script>
<template>
<button v-if="feedback" @click="reportBug">Report a bug</button>
</template>
The ref is null until a <SparkFeedback> mounts somewhere in the app. Hide your trigger when null, or fall back to a /contact link. SparkUserMenu already uses this internally — its built-in Feedback menu item opens the modal when one is mounted, otherwise navigates to the contact page.
useEmbers ()
Reactive Embers balance, level, and progress. Returns { state, refresh } where state is a Ref<EmberBalance | null>. Auto-fetches on sign-in, refetches on every embers:change event the SDK emits after grants, clears to null on sign-out.
<script setup>
import { useEmbers, SparkEmberBalance } from '@katforge/spark';
const { state: embers } = useEmbers ();
</script>
<template>
<SparkEmberBalance v-if="embers" :balance="embers" :show-level="true" />
</template>
Pair with SparkEmberBalance for the rendered chip.
useIngots ()
Reactive Ingots balance for the paid currency. Same shape as useEmbers (), listens for ingots:change events.
<script setup>
import { useIngots, SparkIngotBalance } from '@katforge/spark';
const { state: ingots } = useIngots ();
</script>
<template>
<SparkIngotBalance v-if="ingots" :balance="ingots" />
</template>
useWallet ()
Combined snapshot — both currencies in one ref. Single fetch backs both Ember and Ingot displays. Use this in components that show the whole wallet (account dashboard card, SparkUserMenu dropdown). Components that only need one currency should prefer useEmbers / useIngots so a single change event doesn't refetch the other side.
<script setup>
import { useWallet, SparkWallet } from '@katforge/spark';
const { state: wallet } = useWallet ();
</script>
<template>
<SparkWallet v-if="wallet" :snapshot="wallet" variant="full" :show-progress="true" />
</template>
useAsync ()
The engine behind SparkAsync. Use it when you need reactive data, loading, and error refs without a wrapper component: for layouts the slot model doesn't fit, or when data-dependent UI must render outside the loading boundary.
import { useAsync, useApi, useFlash } from '@katforge/spark';
const api = useApi ();
const flash = useFlash ();
const { data, loading, error, refresh } = useAsync (
() => api.users.list (),
{ onError: (e) => flash.error (e.message) }
);
<template>
<header class="flex items-center justify-between">
<h1>Users</h1>
<SparkButton variant="ghost" :loading @click="refresh">Refresh</SparkButton>
</header>
<span v-if="loading" class="kf-spinner w-6 h-6" />
<ul v-else-if="data">
<li v-for="u in data" :key="u.id">{{ u.name }}</li>
</ul>
</template>
Signature
useAsync<T> (
loader: () => Promise<T>,
options?: UseAsyncOptions<T>
): UseAsyncReturn<T>
Options
| Option | Type | Default | Description |
|---|---|---|---|
immediate | boolean | true | Run the loader on mount |
watch | WatchSource | WatchSource[] | — | Re-run the loader when any of these change |
keepData | boolean | true | Keep data set while a refresh is in flight |
initial | T | null | Initial value for data |
minDuration | number | 0 | Minimum milliseconds the loading ref stays true, even if the loader resolves sooner. Applies to both success and error paths. (SparkAsync defaults this to 750; the bare composable defaults to 0.) |
onError | (err: Error) => void | — | Called when the loader rejects, after minDuration has elapsed |
onSuccess | (data: T) => void | — | Called when the loader resolves, after minDuration has elapsed |
Returns
| Field | Type | Description |
|---|---|---|
data | ShallowRef<T | null> | Latest resolved value. null until the first successful load (or initial if provided) |
loading | Ref<boolean> | true from call start until the loader settles (and at least minDuration ms have elapsed) |
error | ShallowRef<Error | null> | Latest error. Cleared on successful load. null initially |
refresh | () => Promise<T | null> | Re-run the loader. Cancels any in-flight call so racing refreshes never set stale data |
usePagedAsync ()
The engine behind SparkAsyncPaged. Drop to the composable when you want the accumulator + pagination + observer plumbing but need custom rendering (e.g. a table with sticky headers, a virtual scroller, or a grid layout the slot model doesn't fit).
import { usePagedAsync, useApi } from '@katforge/spark';
const api = useApi ();
const {
items, total, page, hasMore,
loading, loadingMore, error,
refresh, loadMore, sentinelRef,
} = usePagedAsync <Entry> ({
load: async (page) => {
const { entries, has_more, total } = await api.leaderboard.list ({ page });
return { items: entries, hasMore: has_more, total };
},
minDuration: 1000,
});
<template>
<table>
<thead><tr>…</tr></thead>
<tbody>
<tr v-for="e in items" :key="e.rank">…</tr>
</tbody>
</table>
<!-- Sentinel fires loadMore when it scrolls into view -->
<div :ref="sentinelRef" />
<span v-if="loadingMore" class="kf-spinner w-4 h-4" />
</template>
End-to-end leaderboard example
A full leaderboard page wired to the API and IntersectionObserver. Each chunk of the response appends to items; the sentinel near the bottom of the table fetches the next page when it scrolls into view, with no scroll-handler glue.
<script setup lang="ts">
import { usePagedAsync, useApi, useFlash } from '@katforge/spark';
type Entry = { rank: number; name: string; score: number };
const api = useApi ();
const flash = useFlash ();
const {
items, total, hasMore,
loading, loadingMore, error,
refresh, sentinelRef
} = usePagedAsync <Entry> ({
load: async (page) => {
const { entries, has_more, total } = await api.games.lextris.leaderboard ({ page, mode: 'daily' });
return { items: entries, hasMore: has_more, total };
},
onError: (e) => flash.error (e.message)
});
</script>
<template>
<header>
<h1>Daily leaderboard <small>({{ total }} entries)</small></h1>
<SparkButton variant="ghost" :loading @click="refresh">Refresh</SparkButton>
</header>
<SparkSpinner v-if="loading" />
<ol v-else>
<li v-for="e in items" :key="e.rank">
#{{ e.rank }} {{ e.name }} — {{ e.score }}
</li>
</ol>
<div :ref="sentinelRef" />
<SparkSpinner v-if="loadingMore" size="sm" />
<p v-if="!hasMore && !loading">— end of leaderboard —</p>
</template>
Signature
usePagedAsync<T> (
options: UsePagedAsyncOptions<T>
): UsePagedAsyncReturn<T>
Options
| Option | Type | Default | Description |
|---|---|---|---|
load | (page: number) => Promise<PagedResult<T>> | — | Required. Fetch one page (1-based). Return hasMore: false to stop future loadMore calls |
immediate | boolean | true | Fetch page 1 on mount |
watch | WatchSource | WatchSource[] | — | Reset pagination and refetch page 1 when any source changes |
initial | T[] | [] | Seed items before the first fetch resolves |
minDuration | number | 0 | Minimum ms the initial loading state is held. Does not apply to loadMore appends. (SparkAsyncPaged defaults this to 750; the bare composable defaults to 0.) |
sentinelMargin | string | '0px 0px 120px 0px' | rootMargin passed to the observer. Pre-load earlier by widening the bottom margin |
onError | (err: Error) => void | — | Called after any failed fetch |
onSuccess | (result: PagedResult<T>, page: number) => void | — | Called after any successful fetch. page lets you distinguish initial load from appends |
Returns
| Field | Type | Description |
|---|---|---|
items | ShallowRef<T[]> | Accumulator. Replaced on refresh (), extended on loadMore () |
total | Ref<number> | Mirrors the last non-undefined total from a PagedResult |
page | Ref<number> | Highest page number loaded so far (1-based; 0 before any fetch) |
hasMore | Ref<boolean> | Drawn from the last PagedResult. Gates loadMore |
loading | Ref<boolean> | true during the initial load / explicit refresh. Drives the main spinner |
loadingMore | Ref<boolean> | true during an append. Drives the in-list "loading more" row |
error | ShallowRef<Error | null> | Latest error. Cleared by a successful fetch |
refresh | () => Promise<void> | Reset pagination and refetch page 1 |
loadMore | () => Promise<void> | Fetch page + 1 and append. No-op while loading or when exhausted |
sentinelRef | (el: Element | null) => void | Function template-ref. Attach to your end-of-list element; intersection with the viewport triggers loadMore |
Realm Data API
The realm registry exposes palette and logo metadata as a plain data API, not a component. Use it to build your own UI, generate theme tokens, or feed into tooling. The registry itself is intentionally not exported; use getRealm for lookup and listRealms for enumeration.
import { getRealm, listRealms } from '@katforge/spark';
import type { Realm, RealmPalette, Swatch } from '@katforge/spark';
const stumper = getRealm ('stumper');
// → {
// label: 'Stumper', style: 'stumper',
// typography: { fontClass: 'font-bungee', color: '#c4b5fd', glow: '…', weight: 700, tracking: '0.02em' },
// suffix: { text: '.gg', style: { color: '#7dd3fc', glow: '…', fontClass: 'font-bungee' } },
// palette: {
// primary: [{ hex: '#8B5CF6', label: 'Violet' }, ...],
// secondary: [{ hex: '#0C0C14', label: 'Surface' }, ...]
// }
// }
// Iterate all realms with a palette (packages return palette: null)
const branded = listRealms ().filter (r => r.palette);
| Export | Description |
|---|---|
getRealm (name) | Case-insensitive lookup; resolves aliases (geargoblins → goblins); returns null for unknown slugs |
listRealms () | Returns every canonical realm as Array<Realm & { slug }> — aliases collapse to their canonical entry |
Realm | { label, style, typography, suffix, palette } |
RealmTypography | { fontClass, color, glow, weight?, tracking?, transform?, size? } |
RealmSuffix | { text, style } where style is { color, glow, fontClass?, weight? } |
RealmPalette | { primary: Swatch[], secondary: Swatch[] } |
Swatch | { hex, label } |
Palette colors are exposed only as data. There are no auto-generated CSS variables or Tailwind utilities per realm. If a consumer app wants stumper-violet or lextris-cyan as first-class Tailwind tokens, it should feed listRealms() output into its own theme config at build time.
To add a new realm, edit packages/spark/src/realms.ts and add a key to the internal REALMS record with label, style, typography, optional suffix, and optional palette. Font families referenced by typography.fontClass must be declared in packages/spark/src/styles/katforge.css under @theme { --font-*: … }.