Docs/Composables

Composables

useFlash ()

Returns the flash notification store. Must be called within a component's setup context.

TypeScript
const flash = useFlash ();
MethodSignatureDescription
success(message, options?) → idGreen toast
error(message, options?) → idRed toast, 10s default
warning(message, options?) → idAmber toast
info(message, options?) → idBlue toast
dismiss(id) → voidDismiss a specific message
dismissAll() → voidClear 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.

TypeScript
import { useApi } from '@katforge/spark';

const api = useApi ();
await api.auth.login (email, password);

useSpark () is a deprecated alias of useApi () 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.

Vue
<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.

Vue
<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.

Vue
<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.

Vue
<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.

Vue
<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.

TypeScript
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) }
);
Vue
<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

TypeScript
useAsync<T> (
   loader:   () => Promise<T>,
   options?: UseAsyncOptions<T>
): UseAsyncReturn<T>

Options

OptionTypeDefaultDescription
immediatebooleantrueRun the loader on mount
watchWatchSource | WatchSource[]Re-run the loader when any of these change
keepDatabooleantrueKeep data set while a refresh is in flight
initialTnullInitial value for data
minDurationnumber0Minimum 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) => voidCalled when the loader rejects, after minDuration has elapsed
onSuccess(data: T) => voidCalled when the loader resolves, after minDuration has elapsed

Returns

FieldTypeDescription
dataShallowRef<T | null>Latest resolved value. null until the first successful load (or initial if provided)
loadingRef<boolean>true from call start until the loader settles (and at least minDuration ms have elapsed)
errorShallowRef<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).

TypeScript
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,
});
Vue
<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.

Vue
<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

TypeScript
usePagedAsync<T> (
   options: UsePagedAsyncOptions<T>
): UsePagedAsyncReturn<T>

Options

OptionTypeDefaultDescription
load(page: number) => Promise<PagedResult<T>>Required. Fetch one page (1-based). Return hasMore: false to stop future loadMore calls
immediatebooleantrueFetch page 1 on mount
watchWatchSource | WatchSource[]Reset pagination and refetch page 1 when any source changes
initialT[][]Seed items before the first fetch resolves
minDurationnumber0Minimum ms the initial loading state is held. Does not apply to loadMore appends. (SparkAsyncPaged defaults this to 750; the bare composable defaults to 0.)
sentinelMarginstring'0px 0px 120px 0px'rootMargin passed to the observer. Pre-load earlier by widening the bottom margin
onError(err: Error) => voidCalled after any failed fetch
onSuccess(result: PagedResult<T>, page: number) => voidCalled after any successful fetch. page lets you distinguish initial load from appends

Returns

FieldTypeDescription
itemsShallowRef<T[]>Accumulator. Replaced on refresh (), extended on loadMore ()
totalRef<number>Mirrors the last non-undefined total from a PagedResult
pageRef<number>Highest page number loaded so far (1-based; 0 before any fetch)
hasMoreRef<boolean>Drawn from the last PagedResult. Gates loadMore
loadingRef<boolean>true during the initial load / explicit refresh. Drives the main spinner
loadingMoreRef<boolean>true during an append. Drives the in-list "loading more" row
errorShallowRef<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) => voidFunction 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.

TypeScript
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);
ExportDescription
getRealm (name)Case-insensitive lookup; resolves aliases (geargoblinsgoblins); 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-*: … }.