Fraser commited on
Commit
6710512
·
1 Parent(s): 3983bc5
index.html CHANGED
@@ -2,9 +2,24 @@
2
  <html lang="en">
3
  <head>
4
  <meta charset="UTF-8" />
5
- <link rel="icon" type="image/svg+xml" href="/vite.svg" />
6
- <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
- <title>HF Multi-Space Demo | FLUX + Joy Caption + RWKV</title>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8
  </head>
9
  <body>
10
  <div id="app"></div>
 
2
  <html lang="en">
3
  <head>
4
  <meta charset="UTF-8" />
5
+ <link rel="icon" type="image/png" href="/assets/pictuary_logo.png" />
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover" />
7
+
8
+ <!-- PWA Meta Tags -->
9
+ <meta name="mobile-web-app-capable" content="yes">
10
+ <meta name="apple-mobile-web-app-capable" content="yes">
11
+ <meta name="apple-mobile-web-app-status-bar-style" content="default">
12
+ <meta name="apple-mobile-web-app-title" content="Piclets">
13
+ <meta name="application-name" content="Piclets">
14
+ <meta name="theme-color" content="#007bff">
15
+
16
+ <!-- Apple Touch Icon -->
17
+ <link rel="apple-touch-icon" href="/assets/pictuary_logo.png">
18
+
19
+ <!-- PWA Manifest -->
20
+ <link rel="manifest" href="/manifest.json">
21
+
22
+ <title>Piclets - Monster Collection Game</title>
23
  </head>
24
  <body>
25
  <div id="app"></div>
public/manifest.json ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "Piclets - Monster Collection Game",
3
+ "short_name": "Piclets",
4
+ "description": "Transform photos into unique monsters and build your collection",
5
+ "start_url": "/",
6
+ "display": "standalone",
7
+ "background_color": "#f5f5f5",
8
+ "theme_color": "#007bff",
9
+ "orientation": "portrait",
10
+ "icons": [
11
+ {
12
+ "src": "/assets/pictuary_logo.png",
13
+ "sizes": "192x192",
14
+ "type": "image/png"
15
+ },
16
+ {
17
+ "src": "/assets/pictuary_logo.png",
18
+ "sizes": "512x512",
19
+ "type": "image/png"
20
+ }
21
+ ]
22
+ }
src/App.svelte CHANGED
@@ -1,10 +1,12 @@
1
  <script lang="ts">
2
  import { onMount } from 'svelte';
3
  import { authStore } from './lib/stores/auth';
4
- import SignInButton from './lib/components/Auth/SignInButton.svelte';
5
- import AuthBanner from './lib/components/Auth/AuthBanner.svelte';
6
- import MonsterGenerator from './lib/components/MonsterGenerator/MonsterGenerator.svelte';
7
- import type { HuggingFaceLibs, GradioLibs, GradioClient, FluxGenerationResult } from './lib/types';
 
 
8
 
9
  // These will be loaded from window after HF libs are loaded
10
  let hfAuth: HuggingFaceLibs | null = $state(null);
@@ -15,6 +17,15 @@
15
  let joyCaptionClient: GradioClient | null = $state(null);
16
  let rwkvClient: GradioClient | null = $state(null);
17
 
 
 
 
 
 
 
 
 
 
18
 
19
  // Auth state from store
20
  const auth = $derived(authStore);
@@ -65,7 +76,7 @@
65
  async function initializeClients(hfToken: string | null) {
66
  if (!gradioClient) return;
67
 
68
- authStore.setBannerMessage("Connecting to FLUX.1-schnell, Joy Caption, and RWKV…");
69
 
70
  try {
71
  const opts = hfToken ? { hf_token: hfToken } : {};
@@ -92,116 +103,51 @@
92
  authStore.setBannerMessage(`❌ Failed to connect: ${err}`);
93
  }
94
  }
 
 
 
 
95
  </script>
96
 
97
  <div class="app">
98
- <div class="card">
99
- <h1>👾 Monster Generator</h1>
100
- <p>
101
- Transform your photos into unique monster creations using AI
102
- </p>
103
-
104
- {#if $auth.showSignIn}
105
- <SignInButton {hfAuth} />
106
- {/if}
107
-
108
- <AuthBanner
109
- message={$auth.bannerMessage}
110
- visible={!!$auth.bannerMessage}
111
- />
112
-
113
- {#if $auth.userInfo}
114
- <p class="user-greeting">Hello, {$auth.userInfo.name || $auth.userInfo.preferred_username}!</p>
115
- {/if}
116
-
117
- <!-- Monster Generator Content -->
118
- {#if fluxClient && joyCaptionClient && rwkvClient}
119
- <MonsterGenerator
120
  {fluxClient}
121
  {joyCaptionClient}
122
  {rwkvClient}
123
  />
124
- {:else}
125
- <div class="loading-message">
126
- <div class="spinner"></div>
127
- <p>Connecting to AI services...</p>
128
- </div>
129
  {/if}
130
-
131
- <hr />
132
- <p class="footer">
133
- Source & docs:
134
- <a href="https://huggingface.co/docs/hub/spaces-oauth" target="_blank">Spaces OAuth</a>,
135
- <a href="https://github.com/huggingface/huggingface.js" target="_blank">huggingface.js</a>,
136
- <a href="https://js.gradio.app" target="_blank">@gradio/client</a>
137
- </p>
138
- </div>
139
  </div>
140
 
141
  <style>
142
  .app {
143
- min-height: 100vh;
 
 
 
144
  background: #f5f5f5;
145
- padding: 2rem;
146
- }
147
-
148
- .card {
149
- max-width: 900px;
150
- margin: 0 auto;
151
- background: #ffffff;
152
- padding: 2rem;
153
- border-radius: 10px;
154
- box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
155
- }
156
-
157
- h1 {
158
- margin-top: 0;
159
- }
160
-
161
- hr {
162
- margin: 3rem 0 2rem;
163
- border: none;
164
- border-top: 1px solid #eee;
165
- }
166
-
167
- .footer {
168
- color: #666;
169
- font-size: 0.9rem;
170
- }
171
-
172
- .footer a {
173
- color: #007bff;
174
- text-decoration: none;
175
- }
176
-
177
- .footer a:hover {
178
- text-decoration: underline;
179
- }
180
-
181
- .user-greeting {
182
- text-align: center;
183
- color: #666;
184
- margin-bottom: 2rem;
185
- }
186
-
187
- .loading-message {
188
- text-align: center;
189
- padding: 3rem;
190
- color: #666;
191
  }
192
 
193
- .spinner {
194
- width: 60px;
195
- height: 60px;
196
- border: 3px solid #f3f3f3;
197
- border-top: 3px solid #007bff;
198
- border-radius: 50%;
199
- animation: spin 1s linear infinite;
200
- margin: 0 auto 2rem;
201
  }
202
 
203
- @keyframes spin {
204
- 0% { transform: rotate(0deg); }
205
- 100% { transform: rotate(360deg); }
206
  }
207
  </style>
 
1
  <script lang="ts">
2
  import { onMount } from 'svelte';
3
  import { authStore } from './lib/stores/auth';
4
+ import AppHeader from './lib/components/Layout/AppHeader.svelte';
5
+ import TabBar, { type TabId } from './lib/components/Layout/TabBar.svelte';
6
+ import Scanner from './lib/components/Pages/Scanner.svelte';
7
+ import Encounters from './lib/components/Pages/Encounters.svelte';
8
+ import Pictuary from './lib/components/Pages/Pictuary.svelte';
9
+ import type { HuggingFaceLibs, GradioLibs, GradioClient } from './lib/types';
10
 
11
  // These will be loaded from window after HF libs are loaded
12
  let hfAuth: HuggingFaceLibs | null = $state(null);
 
17
  let joyCaptionClient: GradioClient | null = $state(null);
18
  let rwkvClient: GradioClient | null = $state(null);
19
 
20
+ // Navigation state
21
+ let activeTab: TabId = $state('scanner');
22
+
23
+ // Tab names mapping
24
+ const tabNames: Record<TabId, string> = {
25
+ scanner: 'Scanner',
26
+ encounters: 'Encounters',
27
+ pictuary: 'Pictuary'
28
+ };
29
 
30
  // Auth state from store
31
  const auth = $derived(authStore);
 
76
  async function initializeClients(hfToken: string | null) {
77
  if (!gradioClient) return;
78
 
79
+ authStore.setBannerMessage("Connecting to AI services...");
80
 
81
  try {
82
  const opts = hfToken ? { hf_token: hfToken } : {};
 
103
  authStore.setBannerMessage(`❌ Failed to connect: ${err}`);
104
  }
105
  }
106
+
107
+ function handleTabChange(tab: TabId) {
108
+ activeTab = tab;
109
+ }
110
  </script>
111
 
112
  <div class="app">
113
+ <AppHeader {hfAuth} currentTab={tabNames[activeTab]} />
114
+
115
+ <main class="app-content">
116
+ {#if activeTab === 'scanner'}
117
+ <Scanner
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
118
  {fluxClient}
119
  {joyCaptionClient}
120
  {rwkvClient}
121
  />
122
+ {:else if activeTab === 'encounters'}
123
+ <Encounters />
124
+ {:else if activeTab === 'pictuary'}
125
+ <Pictuary />
 
126
  {/if}
127
+ </main>
128
+
129
+ <TabBar {activeTab} onTabChange={handleTabChange} />
 
 
 
 
 
 
130
  </div>
131
 
132
  <style>
133
  .app {
134
+ display: flex;
135
+ flex-direction: column;
136
+ height: 100vh;
137
+ height: 100dvh; /* Dynamic viewport height for mobile */
138
  background: #f5f5f5;
139
+ overflow: hidden;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
140
  }
141
 
142
+ .app-content {
143
+ flex: 1;
144
+ overflow: hidden;
145
+ position: relative;
146
+ padding-bottom: calc(50px + env(safe-area-inset-bottom, 0));
 
 
 
147
  }
148
 
149
+ /* Ensure content scrolls properly on iOS */
150
+ :global(.app-content > *) {
151
+ height: 100%;
152
  }
153
  </style>
src/app.css CHANGED
@@ -11,6 +11,13 @@
11
  text-rendering: optimizeLegibility;
12
  -webkit-font-smoothing: antialiased;
13
  -moz-osx-font-smoothing: grayscale;
 
 
 
 
 
 
 
14
  }
15
 
16
  * {
@@ -22,10 +29,18 @@ body {
22
  display: flex;
23
  min-width: 320px;
24
  min-height: 100vh;
 
 
 
 
 
25
  }
26
 
27
  #app {
28
  width: 100%;
 
 
 
29
  }
30
 
31
  a {
@@ -41,6 +56,9 @@ a:hover {
41
  button {
42
  font-family: inherit;
43
  font-size: inherit;
 
 
 
44
  }
45
 
46
  h1, h2, h3, h4, h5, h6 {
@@ -63,6 +81,59 @@ h1, h2, h3, h4, h5, h6 {
63
  .mb-3 { margin-bottom: 1.5rem; }
64
  .mb-4 { margin-bottom: 2rem; }
65
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
66
  /* Hide scrollbar for Chrome, Safari and Opera */
67
  ::-webkit-scrollbar {
68
  width: 8px;
@@ -80,3 +151,10 @@ h1, h2, h3, h4, h5, h6 {
80
  ::-webkit-scrollbar-thumb:hover {
81
  background: #555;
82
  }
 
 
 
 
 
 
 
 
11
  text-rendering: optimizeLegibility;
12
  -webkit-font-smoothing: antialiased;
13
  -moz-osx-font-smoothing: grayscale;
14
+
15
+ /* Mobile-specific variables */
16
+ --safe-area-inset-top: env(safe-area-inset-top, 0);
17
+ --safe-area-inset-bottom: env(safe-area-inset-bottom, 0);
18
+ --safe-area-inset-left: env(safe-area-inset-left, 0);
19
+ --safe-area-inset-right: env(safe-area-inset-right, 0);
20
+ --tab-bar-height: 60px;
21
  }
22
 
23
  * {
 
29
  display: flex;
30
  min-width: 320px;
31
  min-height: 100vh;
32
+ overscroll-behavior: none;
33
+ -webkit-overflow-scrolling: touch;
34
+ touch-action: pan-y;
35
+ -webkit-text-size-adjust: 100%;
36
+ text-size-adjust: 100%;
37
  }
38
 
39
  #app {
40
  width: 100%;
41
+ height: 100vh;
42
+ height: 100dvh;
43
+ position: relative;
44
  }
45
 
46
  a {
 
56
  button {
57
  font-family: inherit;
58
  font-size: inherit;
59
+ -webkit-appearance: none;
60
+ appearance: none;
61
+ touch-action: manipulation;
62
  }
63
 
64
  h1, h2, h3, h4, h5, h6 {
 
81
  .mb-3 { margin-bottom: 1.5rem; }
82
  .mb-4 { margin-bottom: 2rem; }
83
 
84
+ .p-1 { padding: 0.5rem; }
85
+ .p-2 { padding: 1rem; }
86
+ .p-3 { padding: 1.5rem; }
87
+ .p-4 { padding: 2rem; }
88
+
89
+ /* Flex utilities */
90
+ .flex { display: flex; }
91
+ .flex-col { flex-direction: column; }
92
+ .flex-1 { flex: 1; }
93
+ .items-center { align-items: center; }
94
+ .justify-center { justify-content: center; }
95
+ .justify-between { justify-content: space-between; }
96
+ .gap-1 { gap: 0.5rem; }
97
+ .gap-2 { gap: 1rem; }
98
+ .gap-3 { gap: 1.5rem; }
99
+
100
+ /* Mobile-first responsive styles */
101
+ @media (max-width: 768px) {
102
+ .hide-mobile {
103
+ display: none !important;
104
+ }
105
+ }
106
+
107
+ @media (min-width: 769px) {
108
+ .hide-desktop {
109
+ display: none !important;
110
+ }
111
+ }
112
+
113
+ /* Prevent text selection on UI elements */
114
+ .no-select {
115
+ -webkit-user-select: none;
116
+ -moz-user-select: none;
117
+ -ms-user-select: none;
118
+ user-select: none;
119
+ }
120
+
121
+ /* iOS-style tap highlight */
122
+ .tappable {
123
+ -webkit-tap-highlight-color: rgba(0, 0, 0, 0.1);
124
+ cursor: pointer;
125
+ }
126
+
127
+ /* Smooth transitions for interactive elements */
128
+ .interactive {
129
+ transition: transform 0.1s ease-out, opacity 0.1s ease-out;
130
+ }
131
+
132
+ .interactive:active {
133
+ transform: scale(0.95);
134
+ opacity: 0.8;
135
+ }
136
+
137
  /* Hide scrollbar for Chrome, Safari and Opera */
138
  ::-webkit-scrollbar {
139
  width: 8px;
 
151
  ::-webkit-scrollbar-thumb:hover {
152
  background: #555;
153
  }
154
+
155
+ /* Mobile scrollbar styles */
156
+ @media (max-width: 768px) {
157
+ ::-webkit-scrollbar {
158
+ width: 4px;
159
+ }
160
+ }
src/lib/components/Layout/AppHeader.svelte ADDED
@@ -0,0 +1,97 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import { authStore } from '$lib/stores/auth';
3
+ import type { HuggingFaceLibs } from '$lib/types';
4
+
5
+ interface Props {
6
+ hfAuth: HuggingFaceLibs | null;
7
+ currentTab?: string;
8
+ }
9
+
10
+ let { hfAuth, currentTab = 'Piclets' }: Props = $props();
11
+ const auth = $derived(authStore);
12
+
13
+ async function handleSignIn() {
14
+ if (!hfAuth) return;
15
+
16
+ const url = await hfAuth.oauthLoginUrl({ scopes: ["inference-api"] });
17
+ window.location.href = url;
18
+ }
19
+ </script>
20
+
21
+ <header class="app-header">
22
+ <h1 class="app-title">{currentTab}</h1>
23
+
24
+ {#if $auth.showSignIn && !$auth.userInfo}
25
+ <button class="sign-in-btn" onclick={handleSignIn}>
26
+ Sign In
27
+ </button>
28
+ {:else if $auth.userInfo}
29
+ <div class="user-info">
30
+ <span class="username">{$auth.userInfo.preferred_username || 'User'}</span>
31
+ </div>
32
+ {/if}
33
+ </header>
34
+
35
+ {#if $auth.bannerMessage}
36
+ <div class="auth-banner">
37
+ {$auth.bannerMessage}
38
+ </div>
39
+ {/if}
40
+
41
+ <style>
42
+ .app-header {
43
+ display: flex;
44
+ justify-content: space-between;
45
+ align-items: center;
46
+ padding: 1rem;
47
+ background: white;
48
+ border-bottom: 1px solid #eee;
49
+ position: sticky;
50
+ top: 0;
51
+ z-index: 100;
52
+ }
53
+
54
+ .app-title {
55
+ margin: 0;
56
+ font-size: 1.25rem;
57
+ font-weight: 700;
58
+ color: #333;
59
+ }
60
+
61
+ .sign-in-btn {
62
+ padding: 0.5rem 1rem;
63
+ background: #007bff;
64
+ color: white;
65
+ border: none;
66
+ border-radius: 20px;
67
+ font-size: 0.875rem;
68
+ font-weight: 500;
69
+ cursor: pointer;
70
+ transition: background 0.2s;
71
+ }
72
+
73
+ .sign-in-btn:active {
74
+ background: #0056b3;
75
+ }
76
+
77
+ .user-info {
78
+ display: flex;
79
+ align-items: center;
80
+ gap: 0.5rem;
81
+ }
82
+
83
+ .username {
84
+ font-size: 0.875rem;
85
+ color: #666;
86
+ font-weight: 500;
87
+ }
88
+
89
+ .auth-banner {
90
+ padding: 0.75rem 1rem;
91
+ background: #fff3cd;
92
+ color: #856404;
93
+ font-size: 0.875rem;
94
+ text-align: center;
95
+ border-bottom: 1px solid #ffeeba;
96
+ }
97
+ </style>
src/lib/components/Layout/TabBar.svelte ADDED
@@ -0,0 +1,105 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ export type TabId = 'scanner' | 'encounters' | 'pictuary';
3
+
4
+ interface Tab {
5
+ id: TabId;
6
+ label: string;
7
+ icon: string;
8
+ }
9
+
10
+ interface Props {
11
+ activeTab: TabId;
12
+ onTabChange: (tab: TabId) => void;
13
+ }
14
+
15
+ let { activeTab, onTabChange }: Props = $props();
16
+
17
+ const tabs: Tab[] = [
18
+ { id: 'scanner', label: 'Scanner', icon: '/assets/snap_logo.png' },
19
+ { id: 'encounters', label: 'Encounters', icon: '/assets/encounters_logo.png' },
20
+ { id: 'pictuary', label: 'Pictuary', icon: '/assets/pictuary_logo.png' }
21
+ ];
22
+
23
+ function handleTabClick(tabId: TabId) {
24
+ onTabChange(tabId);
25
+ }
26
+ </script>
27
+
28
+ <nav class="tab-bar">
29
+ <div class="tab-bar-inner">
30
+ {#each tabs as tab}
31
+ <button
32
+ class="tab-item"
33
+ class:active={activeTab === tab.id}
34
+ onclick={() => handleTabClick(tab.id)}
35
+ >
36
+ <div class="icon-wrapper">
37
+ <img
38
+ src={tab.icon}
39
+ alt={tab.label}
40
+ class="tab-icon"
41
+ />
42
+ </div>
43
+ </button>
44
+ {/each}
45
+ </div>
46
+ </nav>
47
+
48
+ <style>
49
+ .tab-bar {
50
+ position: fixed;
51
+ bottom: 0;
52
+ left: 0;
53
+ right: 0;
54
+ background: white;
55
+ border-top: 1px solid #eee;
56
+ z-index: 1000;
57
+ padding-bottom: env(safe-area-inset-bottom, 0);
58
+ }
59
+
60
+ .tab-bar-inner {
61
+ display: flex;
62
+ justify-content: space-around;
63
+ align-items: center;
64
+ height: 50px;
65
+ }
66
+
67
+ .tab-item {
68
+ flex: 1;
69
+ display: flex;
70
+ align-items: center;
71
+ justify-content: center;
72
+ background: none;
73
+ border: none;
74
+ padding: 6px;
75
+ cursor: pointer;
76
+ -webkit-tap-highlight-color: transparent;
77
+ }
78
+
79
+ .icon-wrapper {
80
+ position: relative;
81
+ width: 32px;
82
+ height: 32px;
83
+ display: flex;
84
+ align-items: center;
85
+ justify-content: center;
86
+ border-radius: 8px;
87
+ }
88
+
89
+ .tab-item.active .icon-wrapper {
90
+ background: rgb(190, 210, 238);
91
+ }
92
+
93
+ .tab-icon {
94
+ width: 22px;
95
+ height: 22px;
96
+ object-fit: contain;
97
+ filter: grayscale(100%) brightness(0.5);
98
+ }
99
+
100
+ .tab-item.active .tab-icon {
101
+ filter: brightness(0) saturate(100%) invert(27%) sepia(51%) saturate(2878%) hue-rotate(192deg) brightness(104%) contrast(97%);
102
+ }
103
+
104
+
105
+ </style>
src/lib/components/MonsterGenerator/todo.txt CHANGED
@@ -1,3 +1,4 @@
 
1
  Long term I want to turn this into a monster battle game. As part of this I need some kind of internal DB for the game.
2
  When I worked on a Flutter-based version of this I used Isar DB, is there an equivalent that would work well here?
3
 
 
1
+
2
  Long term I want to turn this into a monster battle game. As part of this I need some kind of internal DB for the game.
3
  When I worked on a Flutter-based version of this I used Isar DB, is there an equivalent that would work well here?
4
 
src/lib/components/Pages/Encounters.svelte ADDED
@@ -0,0 +1,90 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ // Placeholder for future implementation
3
+ </script>
4
+
5
+ <div class="encounters-page">
6
+ <div class="coming-soon">
7
+ <img
8
+ src="/assets/encounters_logo.png"
9
+ alt="Encounters"
10
+ class="page-icon"
11
+ />
12
+ <h2>Encounters</h2>
13
+ <p>Battle trainers and catch wild Piclets!</p>
14
+ <div class="feature-preview">
15
+ <div class="preview-card">
16
+ <h3>⚔️ Battle System</h3>
17
+ <p>Turn-based combat with your Piclets</p>
18
+ </div>
19
+ <div class="preview-card">
20
+ <h3>🎯 Catch Mechanics</h3>
21
+ <p>Find and capture new monsters</p>
22
+ </div>
23
+ <div class="preview-card">
24
+ <h3>🏆 Trainer Battles</h3>
25
+ <p>Challenge other trainers</p>
26
+ </div>
27
+ </div>
28
+ <p class="coming-soon-text">Coming Soon</p>
29
+ </div>
30
+ </div>
31
+
32
+ <style>
33
+ .encounters-page {
34
+ height: 100%;
35
+ overflow-y: auto;
36
+ -webkit-overflow-scrolling: touch;
37
+ padding: 2rem 1rem;
38
+ }
39
+
40
+ .coming-soon {
41
+ text-align: center;
42
+ max-width: 400px;
43
+ margin: 0 auto;
44
+ }
45
+
46
+ .page-icon {
47
+ width: 80px;
48
+ height: 80px;
49
+ margin-bottom: 1rem;
50
+ opacity: 0.8;
51
+ }
52
+
53
+ h2 {
54
+ margin: 0 0 0.5rem;
55
+ color: #333;
56
+ }
57
+
58
+ .feature-preview {
59
+ display: flex;
60
+ flex-direction: column;
61
+ gap: 1rem;
62
+ margin: 2rem 0;
63
+ }
64
+
65
+ .preview-card {
66
+ background: #f8f9fa;
67
+ padding: 1.5rem;
68
+ border-radius: 12px;
69
+ text-align: left;
70
+ }
71
+
72
+ .preview-card h3 {
73
+ margin: 0 0 0.5rem;
74
+ font-size: 1.1rem;
75
+ color: #333;
76
+ }
77
+
78
+ .preview-card p {
79
+ margin: 0;
80
+ color: #666;
81
+ font-size: 0.9rem;
82
+ }
83
+
84
+ .coming-soon-text {
85
+ color: #007bff;
86
+ font-weight: 600;
87
+ font-size: 1.2rem;
88
+ margin-top: 2rem;
89
+ }
90
+ </style>
src/lib/components/Pages/Pictuary.svelte ADDED
@@ -0,0 +1,168 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import { onMount } from 'svelte';
3
+ import { getAllMonsters } from '$lib/db/monsters';
4
+ import type { Monster } from '$lib/db/schema';
5
+
6
+ let monsters: Monster[] = $state([]);
7
+ let isLoading = $state(true);
8
+
9
+ onMount(async () => {
10
+ try {
11
+ monsters = await getAllMonsters();
12
+ } catch (err) {
13
+ console.error('Failed to load monsters:', err);
14
+ } finally {
15
+ isLoading = false;
16
+ }
17
+ });
18
+ </script>
19
+
20
+ <div class="pictuary-page">
21
+ <header class="page-header">
22
+ <h2>Your Pictuary</h2>
23
+ <p class="monster-count">{monsters.length} Piclets collected</p>
24
+ </header>
25
+
26
+ {#if isLoading}
27
+ <div class="loading-state">
28
+ <div class="spinner"></div>
29
+ <p>Loading collection...</p>
30
+ </div>
31
+ {:else if monsters.length === 0}
32
+ <div class="empty-state">
33
+ <img
34
+ src="/assets/pictuary_logo.png"
35
+ alt="Pictuary"
36
+ class="empty-icon"
37
+ />
38
+ <h3>No Piclets Yet</h3>
39
+ <p>Start scanning photos to build your collection!</p>
40
+ </div>
41
+ {:else}
42
+ <div class="monster-grid">
43
+ {#each monsters as monster}
44
+ <div class="monster-card">
45
+ <img
46
+ src={monster.imageUrl}
47
+ alt={monster.name}
48
+ class="monster-image"
49
+ />
50
+ <h4 class="monster-name">{monster.name}</h4>
51
+ <p class="monster-date">
52
+ {new Date(monster.createdAt).toLocaleDateString()}
53
+ </p>
54
+ </div>
55
+ {/each}
56
+ </div>
57
+ {/if}
58
+ </div>
59
+
60
+ <style>
61
+ .pictuary-page {
62
+ height: 100%;
63
+ overflow-y: auto;
64
+ -webkit-overflow-scrolling: touch;
65
+ }
66
+
67
+ .page-header {
68
+ padding: 1.5rem 1rem;
69
+ background: white;
70
+ border-bottom: 1px solid #eee;
71
+ position: sticky;
72
+ top: 0;
73
+ z-index: 10;
74
+ }
75
+
76
+ .page-header h2 {
77
+ margin: 0;
78
+ font-size: 1.5rem;
79
+ color: #333;
80
+ }
81
+
82
+ .monster-count {
83
+ margin: 0.25rem 0 0;
84
+ color: #666;
85
+ font-size: 0.9rem;
86
+ }
87
+
88
+ .loading-state,
89
+ .empty-state {
90
+ display: flex;
91
+ flex-direction: column;
92
+ align-items: center;
93
+ justify-content: center;
94
+ height: calc(100% - 100px);
95
+ padding: 2rem;
96
+ text-align: center;
97
+ }
98
+
99
+ .spinner {
100
+ width: 40px;
101
+ height: 40px;
102
+ border: 3px solid #f3f3f3;
103
+ border-top: 3px solid #007bff;
104
+ border-radius: 50%;
105
+ animation: spin 1s linear infinite;
106
+ margin-bottom: 1rem;
107
+ }
108
+
109
+ .empty-icon {
110
+ width: 80px;
111
+ opacity: 0.5;
112
+ margin-bottom: 1rem;
113
+ }
114
+
115
+ .empty-state h3 {
116
+ margin: 0 0 0.5rem;
117
+ color: #333;
118
+ }
119
+
120
+ .empty-state p {
121
+ margin: 0;
122
+ color: #666;
123
+ }
124
+
125
+ .monster-grid {
126
+ display: grid;
127
+ grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
128
+ gap: 1rem;
129
+ padding: 1rem;
130
+ }
131
+
132
+ .monster-card {
133
+ background: white;
134
+ border-radius: 12px;
135
+ overflow: hidden;
136
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
137
+ transition: transform 0.2s;
138
+ }
139
+
140
+ .monster-card:active {
141
+ transform: scale(0.95);
142
+ }
143
+
144
+ .monster-image {
145
+ width: 100%;
146
+ aspect-ratio: 1;
147
+ object-fit: cover;
148
+ }
149
+
150
+ .monster-name {
151
+ margin: 0;
152
+ padding: 0.75rem;
153
+ font-size: 0.9rem;
154
+ font-weight: 600;
155
+ color: #333;
156
+ }
157
+
158
+ .monster-date {
159
+ margin: 0;
160
+ padding: 0 0.75rem 0.75rem;
161
+ font-size: 0.75rem;
162
+ color: #999;
163
+ }
164
+
165
+ @keyframes spin {
166
+ to { transform: rotate(360deg); }
167
+ }
168
+ </style>
src/lib/components/Pages/Scanner.svelte ADDED
@@ -0,0 +1,58 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import MonsterGenerator from '../MonsterGenerator/MonsterGenerator.svelte';
3
+ import type { GradioClient } from '$lib/types';
4
+
5
+ interface Props {
6
+ fluxClient: GradioClient | null;
7
+ joyCaptionClient: GradioClient | null;
8
+ rwkvClient: GradioClient | null;
9
+ }
10
+
11
+ let { fluxClient, joyCaptionClient, rwkvClient }: Props = $props();
12
+ </script>
13
+
14
+ <div class="scanner-page">
15
+ {#if fluxClient && joyCaptionClient && rwkvClient}
16
+ <MonsterGenerator
17
+ {fluxClient}
18
+ {joyCaptionClient}
19
+ {rwkvClient}
20
+ />
21
+ {:else}
22
+ <div class="loading-state">
23
+ <div class="spinner"></div>
24
+ <p>Initializing scanner...</p>
25
+ </div>
26
+ {/if}
27
+ </div>
28
+
29
+ <style>
30
+ .scanner-page {
31
+ height: 100%;
32
+ overflow-y: auto;
33
+ -webkit-overflow-scrolling: touch;
34
+ }
35
+
36
+ .loading-state {
37
+ display: flex;
38
+ flex-direction: column;
39
+ align-items: center;
40
+ justify-content: center;
41
+ height: 100%;
42
+ color: #666;
43
+ }
44
+
45
+ .spinner {
46
+ width: 40px;
47
+ height: 40px;
48
+ border: 3px solid #f3f3f3;
49
+ border-top: 3px solid #007bff;
50
+ border-radius: 50%;
51
+ animation: spin 1s linear infinite;
52
+ margin-bottom: 1rem;
53
+ }
54
+
55
+ @keyframes spin {
56
+ to { transform: rotate(360deg); }
57
+ }
58
+ </style>