diff --git a/package.json b/package.json
index 711f7e1..42721dc 100644
--- a/package.json
+++ b/package.json
@@ -19,6 +19,9 @@
},
"dependencies": {
"@ethersproject/sha2": "5.8.0",
+ "@fontsource-variable/inter": "^5.3.0",
+ "@fontsource-variable/jetbrains-mono": "^5.3.0",
+ "@lucide/svelte": "^1.51.0",
"@reown/appkit": "^1.8.24",
"@reown/appkit-adapter-ethers5": "^1.8.24",
"@sentry/sveltekit": "^11.4.0",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index e895a71..3413b6c 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -11,6 +11,15 @@ importers:
'@ethersproject/sha2':
specifier: 5.8.0
version: 5.8.0
+ '@fontsource-variable/inter':
+ specifier: ^5.3.0
+ version: 5.3.0
+ '@fontsource-variable/jetbrains-mono':
+ specifier: ^5.3.0
+ version: 5.3.0
+ '@lucide/svelte':
+ specifier: ^1.51.0
+ version: 1.51.0(svelte@5.57.1(@typescript-eslint/types@8.71.0))
'@reown/appkit':
specifier: ^1.8.24
version: 1.8.24(@types/react@19.3.0)(debug@4.4.3(supports-color@8.1.1))(supports-color@8.1.1)(typescript@6.0.3)(zod@3.25.76)
@@ -268,6 +277,12 @@ packages:
'@floating-ui/utils@0.2.12':
resolution: {integrity: sha512-HpCo8tmWzLVad5s2d19EhAz5zqrrQ6s69qd6moPMQvkOuSwDT1YgRfWSVuc4ennqrgv3OHppiOGMQ7oC13yIww==}
+ '@fontsource-variable/inter@5.3.0':
+ resolution: {integrity: sha512-OupL48va4JNofb97w6NYeF9S7W/kHNKM0Er8Dem5nqi4jeOLrVJDoE8tZEpnMJmtkvNbB1EIPPwHcdkF6b1oUA==}
+
+ '@fontsource-variable/jetbrains-mono@5.3.0':
+ resolution: {integrity: sha512-F32xpS2NsGYoQi2ADSkKTgpJj7ozajsGgDJ8woTnqjmIB+dxDIqImjl4pXZVEExu8UFZ2ndhmX18EBS/hdz3Lw==}
+
'@humanfs/core@0.19.2':
resolution: {integrity: sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==}
engines: {node: '>=18.18.0'}
@@ -322,6 +337,11 @@ packages:
'@lit/reactive-element@2.1.2':
resolution: {integrity: sha512-pbCDiVMnne1lYUIaYNN5wrwQXDtHaYtg7YEFPeW+hws6U47WeFvISGUWekPGKWOP1ygrs0ef0o1VJMk1exos5A==}
+ '@lucide/svelte@1.51.0':
+ resolution: {integrity: sha512-FNoBtfrnQS1LV7yRm8T8pF6OH72BzCyPOWrOmVcpqGGLFmQZndRB4h0qhzUKqPq2JNkSfF7f4tUaiCwdY4WFfg==}
+ peerDependencies:
+ svelte: ^5
+
'@mapbox/node-pre-gyp@2.0.3':
resolution: {integrity: sha512-uwPAhccfFJlsfCxMYTwOdVfOz3xqyj8xYL3zJj8f0pb30tLohnnFPhLuqp4/qoEz8sNxe4SESZedcBojRefIzg==}
engines: {node: '>=18'}
@@ -3651,6 +3671,10 @@ snapshots:
'@floating-ui/utils@0.2.12': {}
+ '@fontsource-variable/inter@5.3.0': {}
+
+ '@fontsource-variable/jetbrains-mono@5.3.0': {}
+
'@humanfs/core@0.19.2':
dependencies:
'@humanfs/types': 0.15.0
@@ -3705,6 +3729,10 @@ snapshots:
dependencies:
'@lit-labs/ssr-dom-shim': 1.6.0
+ '@lucide/svelte@1.51.0(svelte@5.57.1(@typescript-eslint/types@8.71.0))':
+ dependencies:
+ svelte: 5.57.1(@typescript-eslint/types@8.71.0)
+
'@mapbox/node-pre-gyp@2.0.3(supports-color@8.1.1)':
dependencies:
consola: 3.4.2
diff --git a/src/app.css b/src/app.css
index fad9c4d..766e659 100644
--- a/src/app.css
+++ b/src/app.css
@@ -1,11 +1,69 @@
@import 'tailwindcss';
@import '@skeletonlabs/skeleton';
-@import '@skeletonlabs/skeleton/themes/legacy';
@import '@skeletonlabs/skeleton-svelte';
+@import './theme.css';
@custom-variant dark (&:where(.dark, .dark *));
+@theme {
+ --font-mono: 'JetBrains Mono Variable', ui-monospace, SFMono-Regular, Menlo, monospace;
+}
+
html,
body {
@apply h-full;
}
+
+@layer base {
+ /* visible keyboard focus on every interactive element */
+ :where(
+ a,
+ button,
+ summary,
+ [role='button'],
+ [role='radio'],
+ input[type='checkbox']
+ ):focus-visible {
+ outline: 2px solid var(--color-primary-500);
+ outline-offset: 2px;
+ }
+}
+
+@media (prefers-reduced-motion: reduce) {
+ *,
+ ::before,
+ ::after {
+ animation-duration: 0.01ms !important;
+ transition-duration: 0.01ms !important;
+ }
+}
+
+body {
+ /* soft brand glow behind the content */
+ background-image:
+ radial-gradient(
+ 60rem 30rem at 90% -10%,
+ color-mix(in oklab, var(--color-primary-500) 12%, transparent),
+ transparent 70%
+ ),
+ radial-gradient(
+ 50rem 25rem at -10% 110%,
+ color-mix(in oklab, var(--color-secondary-500) 10%, transparent),
+ transparent 70%
+ );
+ background-attachment: fixed;
+
+ @variant dark {
+ background-image:
+ radial-gradient(
+ 60rem 30rem at 90% -10%,
+ color-mix(in oklab, var(--color-primary-500) 22%, transparent),
+ transparent 70%
+ ),
+ radial-gradient(
+ 50rem 25rem at -10% 110%,
+ color-mix(in oklab, var(--color-secondary-500) 10%, transparent),
+ transparent 70%
+ );
+ }
+}
diff --git a/src/app.html b/src/app.html
index 5008903..a7a1043 100644
--- a/src/app.html
+++ b/src/app.html
@@ -1,5 +1,5 @@
-
+
@@ -8,8 +8,8 @@
-
-
+
+
%sveltekit.head%
-
+
%sveltekit.body%
diff --git a/src/lib/badge.ts b/src/lib/badge.ts
new file mode 100644
index 0000000..a10d5a6
--- /dev/null
+++ b/src/lib/badge.ts
@@ -0,0 +1,44 @@
+import type { Badge, BadgeStyle } from '#lib/types.ts';
+
+const baseURL = 'https://img.shields.io/badge';
+
+export const badgeStyles: { value: BadgeStyle; label: string }[] = [
+ { value: 'flat', label: 'Flat' },
+ { value: 'flat-square', label: 'Flat square' },
+ { value: 'plastic', label: 'Plastic' },
+ { value: 'for-the-badge', label: 'Big' },
+ { value: 'social', label: 'Social' }
+];
+
+// shields.io static badge path segments use `-` as separator: escape `-` and `_` as `--` and `__`
+function escapeSegment(value: string) {
+ return encodeURIComponent(value.replaceAll('-', '--').replaceAll('_', '__'));
+}
+
+/** shields.io image URL of the badge, or undefined while the content or the color is missing */
+export function badgeImageURL(badge: Badge): string | undefined {
+ if (!badge.badgeContent || !badge.color) {
+ return undefined;
+ }
+ const u = new URL(
+ `${baseURL}/${escapeSegment(badge.badgeContent)}-${encodeURIComponent(badge.color)}`
+ );
+ Object.entries(badge).forEach(([key, value]) => {
+ if (key === 'badgeContent' || key === 'color') {
+ return;
+ }
+ if (value) {
+ u.searchParams.set(key, String(value));
+ }
+ });
+ return u.toString();
+}
+
+/** CommitKudos URL the badge links to, with the repository prefilled */
+export function badgeLink(repository: string, reward = 1): string {
+ const u = new URL('https://commitkudos.com');
+ u.searchParams.set('repository', repository);
+ u.searchParams.set('contributor', '5');
+ u.searchParams.set('reward', reward.toString());
+ return u.toString();
+}
diff --git a/src/lib/clipboard.ts b/src/lib/clipboard.ts
new file mode 100644
index 0000000..80684f4
--- /dev/null
+++ b/src/lib/clipboard.ts
@@ -0,0 +1,10 @@
+import { toaster } from '#lib/toaster.ts';
+
+export async function copyToClipboard(text: string, title = 'Copied to clipboard') {
+ try {
+ await navigator.clipboard.writeText(text);
+ toaster.success({ title, duration: 1500 });
+ } catch {
+ toaster.error({ title: 'Could not access the clipboard' });
+ }
+}
diff --git a/src/lib/components/BadgeCode.svelte b/src/lib/components/BadgeCode.svelte
index 618c6dc..251446a 100644
--- a/src/lib/components/BadgeCode.svelte
+++ b/src/lib/components/BadgeCode.svelte
@@ -1,39 +1,38 @@
(tab = e.value)}>
- HTML
Markdown
+ HTML
{#each Object.entries(snippets) as [name, code] (name)}
-
{code}
+
{code}
diff --git a/src/lib/components/BadgeForm.svelte b/src/lib/components/BadgeForm.svelte
index 67f60dc..190b53b 100644
--- a/src/lib/components/BadgeForm.svelte
+++ b/src/lib/components/BadgeForm.svelte
@@ -1,59 +1,126 @@
-
diff --git a/src/lib/components/BadgeGenerator.svelte b/src/lib/components/BadgeGenerator.svelte
index ca27c3f..8c5c1e2 100644
--- a/src/lib/components/BadgeGenerator.svelte
+++ b/src/lib/components/BadgeGenerator.svelte
@@ -1,66 +1,68 @@
-
-
+
+
-
-
+
+
+
+ {#each [{ bg: 'bg-white', theme: 'light' }, { bg: 'bg-[#0d1117]', theme: 'dark' }] as readme (readme.theme)}
+
+ {#if previewURL}
+

+ {/if}
+
+ {/each}
+
+ Preview on a light and a dark GitHub README
+
+
+
{#if imageURL}
-
-
-
+
+ {:else}
+
+ Enter your repository{badge.color ? '' : ' and a background color'} to get the code to paste in
+ your README.
+
{/if}
diff --git a/src/lib/components/Balance.svelte b/src/lib/components/Balance.svelte
index 3ec4a47..95dd2df 100644
--- a/src/lib/components/Balance.svelte
+++ b/src/lib/components/Balance.svelte
@@ -8,10 +8,18 @@
let {
token = $bindable(),
- amount = $bindable(0)
+ amount = $bindable(0),
+ id,
+ disabled = false,
+ recipients = 1
}: {
token?: Balance;
amount?: number;
+ /** id of the amount input, to attach a