@better-state/server 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -9,293 +9,262 @@
9
9
 
10
10
  body {
11
11
  font-family: "Inter", ui-sans-serif, system-ui, -apple-system, sans-serif;
12
- background: #0a0a0f;
12
+ background: #0a0a12;
13
13
  color: #e2e8f0;
14
14
  min-height: 100vh;
15
- padding: 2rem;
16
- }
17
-
18
- .header {
19
15
  display: flex;
20
16
  align-items: center;
21
- gap: 0.75rem;
22
- margin-bottom: 2rem;
23
- }
24
- .header h1 {
25
- font-size: 1.5rem;
26
- font-weight: 700;
27
- letter-spacing: -0.02em;
28
- }
29
- .header h1 span { color: #36adf6; }
30
- .badge {
31
- font-size: 0.7rem;
32
- background: rgba(12, 147, 231, 0.15);
33
- color: #7cc8fb;
34
- padding: 0.15rem 0.5rem;
35
- border-radius: 9999px;
17
+ justify-content: center;
18
+ padding: 1.5rem;
36
19
  }
37
20
 
38
- .status-bar {
39
- display: flex;
40
- gap: 1.5rem;
41
- margin-bottom: 2rem;
42
- font-size: 0.85rem;
43
- color: #94a3b8;
44
- }
45
- .status-dot {
46
- display: inline-block;
47
- width: 8px;
48
- height: 8px;
49
- border-radius: 50%;
50
- margin-right: 0.4rem;
51
- vertical-align: middle;
21
+ .container { width: 100%; max-width: 520px; }
22
+
23
+ .header { margin-bottom: 1.5rem; }
24
+ .header h1 { font-size: 1.5rem; font-weight: 700; letter-spacing: -0.02em; }
25
+ .header h1 span { color: #38bdf8; }
26
+ .subtitle { font-size: 0.8rem; color: #64748b; margin-top: 0.25rem; }
27
+
28
+ .status {
29
+ display: flex; align-items: center; gap: 0.4rem;
30
+ font-size: 0.75rem; color: #94a3b8; margin-bottom: 1.5rem;
52
31
  }
53
- .status-dot.green { background: #34d399; }
54
- .status-dot.red { background: #f87171; }
55
- .status-dot.yellow { background: #fbbf24; }
56
-
57
- .grid {
58
- display: grid;
59
- grid-template-columns: 1fr 1fr;
60
- gap: 1.5rem;
61
- margin-bottom: 2rem;
32
+ .dot {
33
+ width: 7px; height: 7px; border-radius: 50%;
34
+ transition: background 0.3s;
62
35
  }
36
+ .dot.connecting { background: #fbbf24; animation: pulse 1.2s infinite; }
37
+ .dot.connected { background: #34d399; }
38
+ .dot.disconnected { background: #f87171; }
39
+ @keyframes pulse { 0%,100% { opacity: 1; } 50% { opacity: 0.4; } }
63
40
 
64
41
  .card {
65
- background: rgba(15, 23, 42, 0.6);
42
+ background: rgba(15, 23, 42, 0.7);
66
43
  border: 1px solid #1e293b;
67
- border-radius: 12px;
68
- padding: 1.25rem;
69
- }
70
- .card h2 {
71
- font-size: 0.75rem;
72
- font-weight: 600;
73
- text-transform: uppercase;
74
- letter-spacing: 0.05em;
75
- color: #64748b;
44
+ border-radius: 16px;
45
+ padding: 1.5rem;
76
46
  margin-bottom: 1rem;
77
47
  }
78
48
 
79
- .state-display {
80
- background: #0f172a;
81
- border: 1px solid #1e293b;
82
- border-radius: 8px;
83
- padding: 1rem;
84
- font-family: "JetBrains Mono", "Fira Code", monospace;
85
- font-size: 0.85rem;
86
- min-height: 120px;
87
- white-space: pre-wrap;
88
- word-break: break-all;
89
- color: #a5b4fc;
90
- margin-bottom: 1rem;
49
+ .card-label {
50
+ font-size: 0.65rem; font-weight: 600; text-transform: uppercase;
51
+ letter-spacing: 0.08em; color: #475569; margin-bottom: 1rem;
52
+ }
53
+
54
+ /* Counter */
55
+ .counter-value {
56
+ font-size: 4rem; font-weight: 800; text-align: center;
57
+ color: #38bdf8; padding: 0.5rem 0; transition: transform 0.15s;
58
+ font-variant-numeric: tabular-nums;
91
59
  }
60
+ .counter-value.bump { transform: scale(1.08); }
92
61
 
93
- .controls { display: flex; gap: 0.5rem; flex-wrap: wrap; }
94
-
95
- button {
96
- background: linear-gradient(135deg, #0c93e7, #015da0);
97
- color: white;
98
- border: none;
99
- padding: 0.5rem 1rem;
100
- border-radius: 8px;
101
- font-size: 0.8rem;
102
- font-weight: 500;
103
- cursor: pointer;
104
- transition: opacity 0.15s;
62
+ .btn-row {
63
+ display: flex; gap: 0.5rem; margin-top: 1rem;
105
64
  }
106
- button:hover { opacity: 0.85; }
107
- button.secondary {
108
- background: rgba(30, 41, 59, 0.8);
109
- border: 1px solid #334155;
65
+ .btn {
66
+ flex: 1; padding: 0.6rem; border-radius: 10px;
67
+ font-size: 0.9rem; font-weight: 600; cursor: pointer;
68
+ border: 1px solid #1e293b; background: #1e293b; color: #e2e8f0;
69
+ transition: background 0.15s, border-color 0.15s;
110
70
  }
111
- button.danger {
112
- background: linear-gradient(135deg, #dc2626, #991b1b);
71
+ .btn:hover { background: #334155; border-color: #334155; }
72
+ .btn.primary { background: #2563eb; border-color: #2563eb; }
73
+ .btn.primary:hover { background: #1d4ed8; border-color: #1d4ed8; }
74
+ .btn.danger { color: #f87171; }
75
+ .btn.danger:hover { background: #991b1b22; border-color: #7f1d1d; }
76
+
77
+ /* Todos */
78
+ .todo-input-row { display: flex; gap: 0.5rem; margin-bottom: 0.75rem; }
79
+ .todo-input {
80
+ flex: 1; padding: 0.55rem 0.75rem; border-radius: 10px;
81
+ background: #0f172a; border: 1px solid #334155; color: #e2e8f0;
82
+ font-size: 0.85rem;
113
83
  }
84
+ .todo-input:focus { outline: none; border-color: #38bdf8; }
114
85
 
115
- input[type="text"] {
116
- background: #0f172a;
117
- border: 1px solid #334155;
118
- color: #e2e8f0;
119
- padding: 0.5rem 0.75rem;
120
- border-radius: 8px;
86
+ .todo-list { list-style: none; }
87
+ .todo-item {
88
+ display: flex; align-items: center; gap: 0.6rem;
89
+ padding: 0.5rem 0; border-bottom: 1px solid #1e293b;
121
90
  font-size: 0.85rem;
122
- flex: 1;
123
- min-width: 180px;
124
91
  }
125
- input[type="text"]:focus { outline: none; border-color: #36adf6; }
92
+ .todo-item:last-child { border-bottom: none; }
93
+ .todo-check {
94
+ width: 18px; height: 18px; border-radius: 50%;
95
+ border: 2px solid #475569; cursor: pointer;
96
+ display: flex; align-items: center; justify-content: center;
97
+ flex-shrink: 0; transition: all 0.2s;
98
+ }
99
+ .todo-check.done { background: #34d399; border-color: #34d399; }
100
+ .todo-check.done::after { content: "✓"; color: #0a0a12; font-size: 0.7rem; font-weight: 700; }
101
+ .todo-text { flex: 1; }
102
+ .todo-text.done { text-decoration: line-through; color: #64748b; }
103
+ .todo-delete {
104
+ background: none; border: none; color: #475569; cursor: pointer;
105
+ font-size: 1rem; padding: 0 0.25rem; transition: color 0.15s;
106
+ }
107
+ .todo-delete:hover { color: #f87171; }
108
+ .empty { color: #475569; font-size: 0.8rem; text-align: center; padding: 1rem 0; }
126
109
 
110
+ /* Log */
127
111
  .log {
128
- background: #0f172a;
129
- border: 1px solid #1e293b;
130
- border-radius: 8px;
131
- padding: 1rem;
112
+ background: #0a0a12; border: 1px solid #1e293b; border-radius: 10px;
113
+ padding: 0.75rem; max-height: 160px; overflow-y: auto;
132
114
  font-family: "JetBrains Mono", "Fira Code", monospace;
133
- font-size: 0.75rem;
134
- max-height: 300px;
135
- overflow-y: auto;
136
- line-height: 1.6;
137
- }
138
- .log .entry { margin-bottom: 0.25rem; }
139
- .log .ts { color: #475569; }
140
- .log .event { color: #34d399; }
141
- .log .data { color: #94a3b8; }
142
- .log .error { color: #f87171; }
143
-
144
- .hint {
145
- font-size: 0.8rem;
146
- color: #475569;
147
- margin-top: 0.5rem;
148
- line-height: 1.5;
115
+ font-size: 0.7rem; line-height: 1.7;
149
116
  }
117
+ .log-entry .ts { color: #334155; }
118
+ .log-entry .ev { color: #34d399; }
119
+ .log-entry .dt { color: #64748b; }
150
120
 
151
- @media (max-width: 768px) {
152
- .grid { grid-template-columns: 1fr; }
121
+ .footer {
122
+ text-align: center; font-size: 0.7rem; color: #334155;
123
+ margin-top: 1.5rem; line-height: 1.6;
153
124
  }
125
+ .footer code { color: #475569; }
154
126
  </style>
155
127
  </head>
156
128
  <body>
157
- <div class="header">
158
- <h1><span>Better</span>-State</h1>
159
- <span class="badge">Playground</span>
160
- </div>
129
+ <div class="container">
130
+ <div class="header">
131
+ <h1><span>Better</span>-State</h1>
132
+ <div class="subtitle">Playground — open in multiple tabs to see real-time sync</div>
133
+ </div>
161
134
 
162
- <div class="status-bar">
163
- <div><span id="conn-dot" class="status-dot yellow"></span> <span id="conn-text">Connecting...</span></div>
164
- <div>Client: <code id="client-id">—</code></div>
165
- <div>Version: <code id="version">0</code></div>
166
- </div>
135
+ <div class="status">
136
+ <div id="dot" class="dot connecting"></div>
137
+ <span id="status-text">connecting</span>
138
+ </div>
167
139
 
168
- <div class="grid">
169
- <!-- Counter state -->
140
+ <!-- Counter -->
170
141
  <div class="card">
171
- <h2>State: counter</h2>
172
- <div id="counter-display" class="state-display">0</div>
173
- <div class="controls">
174
- <button onclick="increment()">+ Increment</button>
175
- <button onclick="decrement()" class="secondary">- Decrement</button>
176
- <button onclick="resetCounter()" class="danger">Reset</button>
142
+ <div class="card-label">Synced Counter</div>
143
+ <div id="counter" class="counter-value">0</div>
144
+ <div class="btn-row">
145
+ <button class="btn" onclick="dec()">- 1</button>
146
+ <button class="btn danger" onclick="resetCounter()">Reset</button>
147
+ <button class="btn primary" onclick="inc()">+ 1</button>
177
148
  </div>
178
- <p class="hint">Click buttons in multiple tabs — they all sync in real-time.</p>
179
149
  </div>
180
150
 
181
- <!-- Todos state -->
151
+ <!-- Todos -->
182
152
  <div class="card">
183
- <h2>State: todos</h2>
184
- <div id="todos-display" class="state-display">[]</div>
185
- <div class="controls">
186
- <input id="todo-input" type="text" placeholder="Add a todo..." onkeydown="if(event.key==='Enter') addTodo()" />
187
- <button onclick="addTodo()">Add</button>
188
- <button onclick="clearTodos()" class="danger">Clear</button>
153
+ <div class="card-label">Synced Todo List</div>
154
+ <div class="todo-input-row">
155
+ <input id="input" class="todo-input" placeholder="Add a todo..." onkeydown="if(event.key==='Enter')addTodo()" />
156
+ <button class="btn primary" style="flex:none;padding:.55rem .9rem" onclick="addTodo()">Add</button>
189
157
  </div>
158
+ <ul id="list" class="todo-list">
159
+ <li class="empty">No todos yet</li>
160
+ </ul>
190
161
  </div>
191
- </div>
192
162
 
193
- <!-- Event log -->
194
- <div class="card">
195
- <h2>Event Log</h2>
196
- <div id="log" class="log">
197
- <div class="entry"><span class="ts">[init]</span> <span class="data">Waiting for connection...</span></div>
163
+ <!-- Log -->
164
+ <div class="card">
165
+ <div class="card-label">Event Log</div>
166
+ <div id="log" class="log"></div>
167
+ </div>
168
+
169
+ <div class="footer">
170
+ State powered by <code>@better-state/client</code><br/>
171
+ Changes sync instantly across every connected tab
198
172
  </div>
199
173
  </div>
200
174
 
201
175
  <script type="module">
202
- // --- Config ---
203
- // The API key is injected by the server into the page.
204
176
  const API_KEY = "__API_KEY__";
205
- const SERVER_URL = window.location.origin;
206
-
207
- // --- Load SDK ---
208
177
  const { createClient } = await import("/sdk/browser.mjs");
209
178
 
210
- const log = document.getElementById("log");
211
- const connDot = document.getElementById("conn-dot");
212
- const connText = document.getElementById("conn-text");
213
- const clientIdEl = document.getElementById("client-id");
214
- const versionEl = document.getElementById("version");
215
-
216
- function addLog(event, data, isError = false) {
217
- const ts = new Date().toLocaleTimeString();
218
- const entry = document.createElement("div");
219
- entry.className = "entry";
220
- entry.innerHTML = `<span class="ts">[${ts}]</span> <span class="${isError ? "error" : "event"}">${event}</span> <span class="data">${data}</span>`;
221
- log.appendChild(entry);
222
- log.scrollTop = log.scrollHeight;
179
+ const logEl = document.getElementById("log");
180
+ const counterEl = document.getElementById("counter");
181
+ const listEl = document.getElementById("list");
182
+ const dotEl = document.getElementById("dot");
183
+ const statusEl = document.getElementById("status-text");
184
+ const inputEl = document.getElementById("input");
185
+
186
+ function log(ev, data) {
187
+ const t = new Date().toLocaleTimeString("en", { hour12: false });
188
+ const d = document.createElement("div");
189
+ d.className = "log-entry";
190
+ d.innerHTML = `<span class="ts">${t}</span> <span class="ev">${ev}</span> <span class="dt">${data}</span>`;
191
+ logEl.appendChild(d);
192
+ logEl.scrollTop = logEl.scrollHeight;
223
193
  }
224
194
 
225
- // --- Create client ---
226
- const client = createClient(SERVER_URL, {
227
- apiKey: API_KEY,
228
- debug: true,
229
- });
195
+ const bs = createClient(location.origin, { apiKey: API_KEY, debug: false });
230
196
 
231
- clientIdEl.textContent = "connecting...";
197
+ bs.onStatusChange(s => {
198
+ dotEl.className = "dot " + s;
199
+ statusEl.textContent = s;
200
+ log("status", s);
201
+ });
232
202
 
233
- // --- Counter state ---
234
- const counter = client.state("counter", 0);
203
+ bs.onError(e => log("error", e.message));
235
204
 
236
- counter.subscribe((val) => {
237
- document.getElementById("counter-display").textContent = JSON.stringify(val, null, 2);
238
- versionEl.textContent = counter.getVersion();
239
- addLog("counter:update", `value=${val} version=${counter.getVersion()}`);
205
+ // Counter
206
+ const counter = bs.state("counter", 0);
207
+ counter.subscribe(v => {
208
+ counterEl.textContent = v;
209
+ counterEl.classList.add("bump");
210
+ setTimeout(() => counterEl.classList.remove("bump"), 150);
211
+ log("counter", `value=${v}`);
240
212
  });
241
213
 
242
- // --- Todos state ---
243
- const todos = client.state("todos", []);
214
+ window.inc = () => counter.update(n => n + 1);
215
+ window.dec = () => counter.update(n => n - 1);
216
+ window.resetCounter = () => counter.set(0);
244
217
 
245
- todos.subscribe((val) => {
246
- document.getElementById("todos-display").textContent = JSON.stringify(val, null, 2);
247
- addLog("todos:update", `items=${Array.isArray(val) ? val.length : "?"}`);
248
- });
218
+ // Todos
219
+ const todos = bs.state("todos", []);
249
220
 
250
- // --- Connection status ---
251
- // Poll connection status (simple approach)
252
- setInterval(() => {
253
- const connected = counter.getVersion() >= 0;
254
- if (connText.textContent === "Connecting..." && counter.getVersion() > 0) {
255
- connDot.className = "status-dot green";
256
- connText.textContent = "Connected";
257
- clientIdEl.textContent = client._clientId || "active";
221
+ function renderTodos(items) {
222
+ if (!Array.isArray(items) || items.length === 0) {
223
+ listEl.innerHTML = '<li class="empty">No todos yet</li>';
224
+ return;
258
225
  }
259
- }, 500);
260
-
261
- // Mark connected once we get first state
262
- setTimeout(() => {
263
- connDot.className = "status-dot green";
264
- connText.textContent = "Connected";
265
- }, 2000);
266
-
267
- // --- Actions (exposed globally for onclick handlers) ---
268
- window.increment = () => {
269
- counter.update((n) => n + 1);
270
- addLog("action", "increment");
271
- };
226
+ listEl.innerHTML = items.map(t => `
227
+ <li class="todo-item">
228
+ <div class="todo-check ${t.done ? "done" : ""}" onclick="toggleTodo('${t.id}')"></div>
229
+ <span class="todo-text ${t.done ? "done" : ""}">${esc(t.text)}</span>
230
+ <button class="todo-delete" onclick="deleteTodo('${t.id}')">×</button>
231
+ </li>
232
+ `).join("");
233
+ }
272
234
 
273
- window.decrement = () => {
274
- counter.update((n) => n - 1);
275
- addLog("action", "decrement");
276
- };
235
+ function esc(s) {
236
+ const d = document.createElement("div");
237
+ d.textContent = s;
238
+ return d.innerHTML;
239
+ }
277
240
 
278
- window.resetCounter = () => {
279
- counter.update(() => 0);
280
- addLog("action", "reset counter");
281
- };
241
+ todos.subscribe(v => {
242
+ renderTodos(v);
243
+ log("todos", `count=${Array.isArray(v) ? v.length : 0}`);
244
+ });
282
245
 
283
246
  window.addTodo = () => {
284
- const input = document.getElementById("todo-input");
285
- const text = input.value.trim();
247
+ const text = inputEl.value.trim();
286
248
  if (!text) return;
287
249
  const id = crypto.randomUUID();
288
- todos.update((list) => [...list, { id, text, done: false }]);
289
- addLog("action", `add todo: "${text}"`);
290
- input.value = "";
250
+ const current = todos.get();
251
+ todos.set([...(Array.isArray(current) ? current : []), { id, text, done: false }]);
252
+ inputEl.value = "";
253
+ };
254
+
255
+ window.toggleTodo = (id) => {
256
+ const current = todos.get();
257
+ if (!Array.isArray(current)) return;
258
+ todos.set(current.map(t => t.id === id ? { ...t, done: !t.done } : t));
291
259
  };
292
260
 
293
- window.clearTodos = () => {
294
- todos.update(() => []);
295
- addLog("action", "clear todos");
261
+ window.deleteTodo = (id) => {
262
+ const current = todos.get();
263
+ if (!Array.isArray(current)) return;
264
+ todos.set(current.filter(t => t.id !== id));
296
265
  };
297
266
 
298
- addLog("init", "SDK loaded, connecting to server...");
267
+ log("init", "SDK loaded");
299
268
  </script>
300
269
  </body>
301
270
  </html>