tokra 0.0.1.pre.1 → 0.0.1.pre.2

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.
Files changed (160) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSES/MIT-0.txt +16 -0
  3. data/REUSE.toml +6 -1
  4. data/Rakefile +2 -0
  5. data/Steepfile +2 -0
  6. data/clippy_exceptions.rb +75 -37
  7. data/doc/contributors/adr/004.md +63 -0
  8. data/doc/contributors/chats/002.md +177 -0
  9. data/doc/contributors/chats/003.md +2180 -0
  10. data/doc/contributors/chats/004.md +1992 -0
  11. data/doc/contributors/chats/005.md +1529 -0
  12. data/doc/contributors/plan/002.md +173 -0
  13. data/doc/contributors/plan/003.md +111 -0
  14. data/examples/verify_hello_world/index.html +15 -2
  15. data/examples/verify_ping_pong/app.rb +3 -1
  16. data/examples/verify_ping_pong/public/styles.css +36 -9
  17. data/examples/verify_ping_pong/views/layout.erb +1 -1
  18. data/examples/verify_rails_sqlite/.dockerignore +51 -0
  19. data/examples/verify_rails_sqlite/.gitattributes +9 -0
  20. data/examples/verify_rails_sqlite/.github/dependabot.yml +12 -0
  21. data/examples/verify_rails_sqlite/.github/workflows/ci.yml +124 -0
  22. data/examples/verify_rails_sqlite/.gitignore +35 -0
  23. data/examples/verify_rails_sqlite/.kamal/hooks/docker-setup.sample +3 -0
  24. data/examples/verify_rails_sqlite/.kamal/hooks/post-app-boot.sample +3 -0
  25. data/examples/verify_rails_sqlite/.kamal/hooks/post-deploy.sample +14 -0
  26. data/examples/verify_rails_sqlite/.kamal/hooks/post-proxy-reboot.sample +3 -0
  27. data/examples/verify_rails_sqlite/.kamal/hooks/pre-app-boot.sample +3 -0
  28. data/examples/verify_rails_sqlite/.kamal/hooks/pre-build.sample +51 -0
  29. data/examples/verify_rails_sqlite/.kamal/hooks/pre-connect.sample +47 -0
  30. data/examples/verify_rails_sqlite/.kamal/hooks/pre-deploy.sample +122 -0
  31. data/examples/verify_rails_sqlite/.kamal/hooks/pre-proxy-reboot.sample +3 -0
  32. data/examples/verify_rails_sqlite/.kamal/secrets +20 -0
  33. data/examples/verify_rails_sqlite/.rubocop.yml +2 -0
  34. data/examples/verify_rails_sqlite/.ruby-version +1 -0
  35. data/examples/verify_rails_sqlite/Dockerfile +77 -0
  36. data/examples/verify_rails_sqlite/Gemfile +66 -0
  37. data/examples/verify_rails_sqlite/Gemfile.lock +563 -0
  38. data/examples/verify_rails_sqlite/README.md +41 -0
  39. data/examples/verify_rails_sqlite/Rakefile +6 -0
  40. data/examples/verify_rails_sqlite/app/assets/images/.keep +0 -0
  41. data/examples/verify_rails_sqlite/app/assets/stylesheets/application.css +469 -0
  42. data/examples/verify_rails_sqlite/app/controllers/application_controller.rb +12 -0
  43. data/examples/verify_rails_sqlite/app/controllers/concerns/.keep +0 -0
  44. data/examples/verify_rails_sqlite/app/controllers/todos_controller.rb +70 -0
  45. data/examples/verify_rails_sqlite/app/helpers/application_helper.rb +2 -0
  46. data/examples/verify_rails_sqlite/app/helpers/todos_helper.rb +2 -0
  47. data/examples/verify_rails_sqlite/app/javascript/application.js +3 -0
  48. data/examples/verify_rails_sqlite/app/javascript/controllers/application.js +9 -0
  49. data/examples/verify_rails_sqlite/app/javascript/controllers/hello_controller.js +7 -0
  50. data/examples/verify_rails_sqlite/app/javascript/controllers/index.js +4 -0
  51. data/examples/verify_rails_sqlite/app/jobs/application_job.rb +7 -0
  52. data/examples/verify_rails_sqlite/app/mailers/application_mailer.rb +4 -0
  53. data/examples/verify_rails_sqlite/app/models/application_record.rb +3 -0
  54. data/examples/verify_rails_sqlite/app/models/concerns/.keep +0 -0
  55. data/examples/verify_rails_sqlite/app/models/todo.rb +10 -0
  56. data/examples/verify_rails_sqlite/app/views/layouts/application.html.erb +24 -0
  57. data/examples/verify_rails_sqlite/app/views/layouts/mailer.html.erb +13 -0
  58. data/examples/verify_rails_sqlite/app/views/layouts/mailer.text.erb +1 -0
  59. data/examples/verify_rails_sqlite/app/views/pwa/manifest.json.erb +22 -0
  60. data/examples/verify_rails_sqlite/app/views/pwa/service-worker.js +26 -0
  61. data/examples/verify_rails_sqlite/app/views/todos/_form.html.erb +27 -0
  62. data/examples/verify_rails_sqlite/app/views/todos/_todo.html.erb +18 -0
  63. data/examples/verify_rails_sqlite/app/views/todos/_todo.json.jbuilder +2 -0
  64. data/examples/verify_rails_sqlite/app/views/todos/edit.html.erb +7 -0
  65. data/examples/verify_rails_sqlite/app/views/todos/index.html.erb +22 -0
  66. data/examples/verify_rails_sqlite/app/views/todos/index.json.jbuilder +1 -0
  67. data/examples/verify_rails_sqlite/app/views/todos/new.html.erb +7 -0
  68. data/examples/verify_rails_sqlite/app/views/todos/show.html.erb +23 -0
  69. data/examples/verify_rails_sqlite/app/views/todos/show.json.jbuilder +1 -0
  70. data/examples/verify_rails_sqlite/bin/brakeman +7 -0
  71. data/examples/verify_rails_sqlite/bin/bundler-audit +6 -0
  72. data/examples/verify_rails_sqlite/bin/ci +6 -0
  73. data/examples/verify_rails_sqlite/bin/dev +2 -0
  74. data/examples/verify_rails_sqlite/bin/docker-entrypoint +8 -0
  75. data/examples/verify_rails_sqlite/bin/importmap +4 -0
  76. data/examples/verify_rails_sqlite/bin/jobs +6 -0
  77. data/examples/verify_rails_sqlite/bin/kamal +16 -0
  78. data/examples/verify_rails_sqlite/bin/rails +4 -0
  79. data/examples/verify_rails_sqlite/bin/rake +4 -0
  80. data/examples/verify_rails_sqlite/bin/rubocop +8 -0
  81. data/examples/verify_rails_sqlite/bin/setup +35 -0
  82. data/examples/verify_rails_sqlite/bin/thrust +5 -0
  83. data/examples/verify_rails_sqlite/config/application.rb +27 -0
  84. data/examples/verify_rails_sqlite/config/boot.rb +4 -0
  85. data/examples/verify_rails_sqlite/config/bundler-audit.yml +5 -0
  86. data/examples/verify_rails_sqlite/config/cable.yml +17 -0
  87. data/examples/verify_rails_sqlite/config/cache.yml +16 -0
  88. data/examples/verify_rails_sqlite/config/ci.rb +24 -0
  89. data/examples/verify_rails_sqlite/config/credentials.yml.enc +1 -0
  90. data/examples/verify_rails_sqlite/config/database.yml +40 -0
  91. data/examples/verify_rails_sqlite/config/deploy.yml +119 -0
  92. data/examples/verify_rails_sqlite/config/environment.rb +5 -0
  93. data/examples/verify_rails_sqlite/config/environments/development.rb +84 -0
  94. data/examples/verify_rails_sqlite/config/environments/production.rb +99 -0
  95. data/examples/verify_rails_sqlite/config/environments/test.rb +53 -0
  96. data/examples/verify_rails_sqlite/config/importmap.rb +7 -0
  97. data/examples/verify_rails_sqlite/config/initializers/assets.rb +7 -0
  98. data/examples/verify_rails_sqlite/config/initializers/content_security_policy.rb +29 -0
  99. data/examples/verify_rails_sqlite/config/initializers/filter_parameter_logging.rb +8 -0
  100. data/examples/verify_rails_sqlite/config/initializers/inflections.rb +16 -0
  101. data/examples/verify_rails_sqlite/config/locales/en.yml +31 -0
  102. data/examples/verify_rails_sqlite/config/puma.rb +42 -0
  103. data/examples/verify_rails_sqlite/config/queue.yml +18 -0
  104. data/examples/verify_rails_sqlite/config/recurring.yml +15 -0
  105. data/examples/verify_rails_sqlite/config/routes.rb +15 -0
  106. data/examples/verify_rails_sqlite/config/storage.yml +27 -0
  107. data/examples/verify_rails_sqlite/config.ru +6 -0
  108. data/examples/verify_rails_sqlite/db/cable_schema.rb +11 -0
  109. data/examples/verify_rails_sqlite/db/cache_schema.rb +12 -0
  110. data/examples/verify_rails_sqlite/db/migrate/20260130080933_create_todos.rb +10 -0
  111. data/examples/verify_rails_sqlite/db/queue_schema.rb +129 -0
  112. data/examples/verify_rails_sqlite/db/schema.rb +20 -0
  113. data/examples/verify_rails_sqlite/db/seeds.rb +9 -0
  114. data/examples/verify_rails_sqlite/lib/tasks/.keep +0 -0
  115. data/examples/verify_rails_sqlite/log/.keep +0 -0
  116. data/examples/verify_rails_sqlite/public/400.html +135 -0
  117. data/examples/verify_rails_sqlite/public/404.html +135 -0
  118. data/examples/verify_rails_sqlite/public/406-unsupported-browser.html +135 -0
  119. data/examples/verify_rails_sqlite/public/422.html +135 -0
  120. data/examples/verify_rails_sqlite/public/500.html +135 -0
  121. data/examples/verify_rails_sqlite/public/icon.png +0 -0
  122. data/examples/verify_rails_sqlite/public/icon.svg +3 -0
  123. data/examples/verify_rails_sqlite/public/robots.txt +1 -0
  124. data/examples/verify_rails_sqlite/public/styles.css +469 -0
  125. data/examples/verify_rails_sqlite/script/.keep +0 -0
  126. data/examples/verify_rails_sqlite/storage/.keep +0 -0
  127. data/examples/verify_rails_sqlite/test/controllers/.keep +0 -0
  128. data/examples/verify_rails_sqlite/test/controllers/todos_controller_test.rb +48 -0
  129. data/examples/verify_rails_sqlite/test/fixtures/files/.keep +0 -0
  130. data/examples/verify_rails_sqlite/test/fixtures/todos.yml +9 -0
  131. data/examples/verify_rails_sqlite/test/helpers/.keep +0 -0
  132. data/examples/verify_rails_sqlite/test/integration/.keep +0 -0
  133. data/examples/verify_rails_sqlite/test/mailers/.keep +0 -0
  134. data/examples/verify_rails_sqlite/test/models/.keep +0 -0
  135. data/examples/verify_rails_sqlite/test/models/todo_test.rb +7 -0
  136. data/examples/verify_rails_sqlite/test/test_helper.rb +15 -0
  137. data/examples/verify_rails_sqlite/tmp/.keep +0 -0
  138. data/examples/verify_rails_sqlite/tmp/pids/.keep +0 -0
  139. data/examples/verify_rails_sqlite/tmp/storage/.keep +0 -0
  140. data/examples/verify_rails_sqlite/tokra.rb +42 -0
  141. data/examples/verify_rails_sqlite/vendor/.keep +0 -0
  142. data/examples/verify_rails_sqlite/vendor/javascript/.keep +0 -0
  143. data/ext/tokra/src/event_loop.rs +206 -0
  144. data/ext/tokra/src/events.rs +430 -0
  145. data/ext/tokra/src/lib.rs +52 -664
  146. data/ext/tokra/src/proxy.rs +142 -0
  147. data/ext/tokra/src/responders.rs +86 -0
  148. data/ext/tokra/src/user_event.rs +313 -0
  149. data/ext/tokra/src/webview.rs +194 -0
  150. data/ext/tokra/src/window.rs +92 -0
  151. data/lib/tokra/rack/handler.rb +37 -14
  152. data/lib/tokra/version.rb +1 -1
  153. data/rbs_exceptions.rb +12 -0
  154. data/sig/tokra.rbs +95 -1
  155. data/tasks/lint.rake +2 -2
  156. data/tasks/lint.rb +49 -0
  157. data/tasks/rust.rake +6 -23
  158. data/tasks/steep.rake +25 -3
  159. data/tasks/test.rake +1 -1
  160. metadata +143 -1
@@ -0,0 +1,469 @@
1
+ /*
2
+ SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
3
+
4
+ SPDX-License-Identifier: AGPL-3.0-or-later
5
+ */
6
+
7
+ /*
8
+ * Tokra Tasks — "Refined Paper" aesthetic
9
+ * Warm cream palette, editorial serif typography, asymmetric layout
10
+ * NOTE: Using hex colors instead of OKLCH for WebView compatibiliy
11
+ */
12
+
13
+ *, *::before, *::after {
14
+ box-sizing: border-box;
15
+ margin: 0;
16
+ }
17
+
18
+ :root {
19
+ /* Typography - fall back to system fonts for desktop app */
20
+ --font-display: Georgia, 'Times New Roman', serif;
21
+ --font-body: system-ui, -apple-system, sans-serif;
22
+
23
+ /* Light mode: warm paper (hex equivalents of OKLCH values) */
24
+ --surface: #f5f3ed;
25
+ --surface-elevated: #fdfcfa;
26
+ --text: #3d3530;
27
+ --text-muted: #6b635b;
28
+ --accent: #8b4513;
29
+ --accent-hover: #6b3410;
30
+ --border: #ddd8d0;
31
+ --success: #2d6a4f;
32
+ --success-bg: #d8f3dc;
33
+ --strike: #a89f93;
34
+ --checkbox-bg: #e8e5de;
35
+ --focus-ring: #a0522d;
36
+ }
37
+
38
+ @media (prefers-color-scheme: dark) {
39
+ :root {
40
+ --surface: #1a1816;
41
+ --surface-elevated: #252220;
42
+ --text: #e8e5e0;
43
+ --text-muted: #9a958e;
44
+ --accent: #d4a574;
45
+ --accent-hover: #e5c09a;
46
+ --border: #3d3835;
47
+ --success: #74c69d;
48
+ --success-bg: #1e3a2a;
49
+ --strike: #6b655c;
50
+ --checkbox-bg: #353230;
51
+ --focus-ring: #cd9a6d;
52
+ }
53
+ }
54
+
55
+ html {
56
+ font-family: var(--font-body);
57
+ font-size: clamp(1rem, 0.95rem + 0.25vw, 1.0625rem);
58
+ line-height: 1.55;
59
+ color: var(--text);
60
+ background: var(--surface);
61
+ -webkit-font-smoothing: antialiased;
62
+ }
63
+
64
+ body {
65
+ min-height: 100dvh;
66
+ padding: clamp(1.5rem, 5vw, 4rem);
67
+ padding-block-start: clamp(2rem, 6vw, 5rem);
68
+ }
69
+
70
+ /* ═══════════════════════════════════════════════════════
71
+ NOTICE BANNER
72
+ ═══════════════════════════════════════════════════════ */
73
+
74
+ .notice {
75
+ position: fixed;
76
+ inset-block-start: 0;
77
+ inset-inline: 0;
78
+ padding: 0.75rem 1.5rem;
79
+ font-size: 0.875rem;
80
+ font-weight: 500;
81
+ text-align: center;
82
+ color: var(--success);
83
+ background: var(--success-bg);
84
+ border-block-end: 1px solid rgba(45, 106, 79, 0.2);
85
+ animation: notice-enter 0.3s ease-out;
86
+ }
87
+
88
+ @keyframes notice-enter {
89
+ from {
90
+ opacity: 0;
91
+ transform: translateY(-100%);
92
+ }
93
+ }
94
+
95
+ /* ═══════════════════════════════════════════════════════
96
+ PAGE HEADER
97
+ ═══════════════════════════════════════════════════════ */
98
+
99
+ .page-header {
100
+ max-width: 36rem;
101
+ margin-block-end: clamp(2rem, 4vw, 3rem);
102
+ }
103
+
104
+ .page-title {
105
+ font-family: var(--font-display);
106
+ font-size: clamp(2.25rem, 1.8rem + 2.25vw, 3.5rem);
107
+ font-weight: 400;
108
+ letter-spacing: -0.02em;
109
+ line-height: 1.1;
110
+ }
111
+
112
+ .page-subtitle {
113
+ margin-block-start: 0.5rem;
114
+ font-size: 1rem;
115
+ color: var(--text-muted);
116
+ }
117
+
118
+ /* ═══════════════════════════════════════════════════════
119
+ TODO LIST
120
+ ═══════════════════════════════════════════════════════ */
121
+
122
+ .todo-list {
123
+ list-style: none;
124
+ padding: 0;
125
+ max-width: 36rem;
126
+ }
127
+
128
+ .todo-item {
129
+ display: grid;
130
+ grid-template-columns: auto 1fr auto;
131
+ align-items: start;
132
+ gap: 1rem;
133
+ padding-block: 1rem;
134
+ border-block-end: 1px solid var(--border);
135
+ }
136
+
137
+ .todo-item:first-child {
138
+ padding-block-start: 0;
139
+ }
140
+
141
+ .todo-checkbox {
142
+ appearance: none;
143
+ width: 1.375rem;
144
+ height: 1.375rem;
145
+ margin-block-start: 0.1rem;
146
+ background: var(--checkbox-bg);
147
+ border: 1.5px solid var(--border);
148
+ border-radius: 4px;
149
+ cursor: pointer;
150
+ transition: border-color 0.15s, background 0.15s;
151
+ }
152
+
153
+ .todo-checkbox:hover {
154
+ border-color: var(--accent);
155
+ }
156
+
157
+ .todo-checkbox:checked {
158
+ background: var(--accent);
159
+ border-color: var(--accent);
160
+ background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 16 16' fill='white' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M12.207 4.793a1 1 0 0 1 0 1.414l-5 5a1 1 0 0 1-1.414 0l-2.5-2.5a1 1 0 0 1 1.414-1.414L6.5 9.086l4.293-4.293a1 1 0 0 1 1.414 0z'/%3E%3C/svg%3E");
161
+ background-position: center;
162
+ background-repeat: no-repeat;
163
+ }
164
+
165
+ .todo-checkbox:focus-visible {
166
+ outline: 2px solid var(--focus-ring);
167
+ outline-offset: 2px;
168
+ }
169
+
170
+ .todo-content {
171
+ min-width: 0;
172
+ }
173
+
174
+ .todo-title {
175
+ font-family: var(--font-display);
176
+ font-size: 1.25rem;
177
+ font-weight: 400;
178
+ line-height: 1.35;
179
+ transition: color 0.2s;
180
+ }
181
+
182
+ .todo-title a {
183
+ color: inherit;
184
+ text-decoration: none;
185
+ }
186
+
187
+ .todo-title a:hover {
188
+ color: var(--accent);
189
+ }
190
+
191
+ .todo-item[data-completed="true"] .todo-title {
192
+ color: var(--strike);
193
+ text-decoration: line-through;
194
+ text-decoration-color: var(--strike);
195
+ }
196
+
197
+ .todo-actions {
198
+ display: flex;
199
+ gap: 0.5rem;
200
+ opacity: 0;
201
+ transition: opacity 0.15s;
202
+ }
203
+
204
+ .todo-item:hover .todo-actions,
205
+ .todo-item:focus-within .todo-actions {
206
+ opacity: 1;
207
+ }
208
+
209
+ /* ═══════════════════════════════════════════════════════
210
+ EMPTY STATE
211
+ ═══════════════════════════════════════════════════════ */
212
+
213
+ .empty-state {
214
+ max-width: 24rem;
215
+ padding-block: 3rem;
216
+ text-align: left;
217
+ }
218
+
219
+ .empty-state p {
220
+ color: var(--text-muted);
221
+ font-style: italic;
222
+ }
223
+
224
+ /* ═══════════════════════════════════════════════════════
225
+ BUTTONS & LINKS
226
+ ═══════════════════════════════════════════════════════ */
227
+
228
+ .btn {
229
+ display: inline-flex;
230
+ align-items: center;
231
+ gap: 0.375rem;
232
+ padding: 0.5rem 1rem;
233
+ font-family: var(--font-body);
234
+ font-size: 0.875rem;
235
+ font-weight: 500;
236
+ color: var(--surface-elevated);
237
+ background: var(--accent);
238
+ border: none;
239
+ border-radius: 6px;
240
+ cursor: pointer;
241
+ text-decoration: none;
242
+ transition: background 0.15s, transform 0.1s;
243
+ }
244
+
245
+ .btn:hover {
246
+ background: var(--accent-hover);
247
+ }
248
+
249
+ .btn:active {
250
+ transform: scale(0.98);
251
+ }
252
+
253
+ .btn:focus-visible {
254
+ outline: 2px solid var(--focus-ring);
255
+ outline-offset: 2px;
256
+ }
257
+
258
+ .btn--ghost {
259
+ padding: 0.375rem 0.625rem;
260
+ font-size: 0.8125rem;
261
+ color: var(--text-muted);
262
+ background: transparent;
263
+ border: 1px solid var(--border);
264
+ }
265
+
266
+ .btn--ghost:hover {
267
+ color: var(--text);
268
+ background: var(--surface-elevated);
269
+ border-color: var(--text-muted);
270
+ }
271
+
272
+ .btn--danger {
273
+ background: #b53d3d;
274
+ }
275
+
276
+ .btn--danger:hover {
277
+ background: #943030;
278
+ }
279
+
280
+ .link {
281
+ color: var(--accent);
282
+ text-decoration: underline;
283
+ text-decoration-color: rgba(139, 69, 19, 0.3);
284
+ text-underline-offset: 2px;
285
+ transition: text-decoration-color 0.15s;
286
+ }
287
+
288
+ .link:hover {
289
+ text-decoration-color: var(--accent);
290
+ }
291
+
292
+ .link--muted {
293
+ color: var(--text-muted);
294
+ text-decoration-color: var(--border);
295
+ }
296
+
297
+ .link--muted:hover {
298
+ color: var(--text);
299
+ }
300
+
301
+ /* ═══════════════════════════════════════════════════════
302
+ FORMS
303
+ ═══════════════════════════════════════════════════════ */
304
+
305
+ .form {
306
+ max-width: 28rem;
307
+ }
308
+
309
+ .field {
310
+ margin-block-end: 1.5rem;
311
+ }
312
+
313
+ .field label {
314
+ display: block;
315
+ margin-block-end: 0.375rem;
316
+ font-size: 0.875rem;
317
+ font-weight: 500;
318
+ color: var(--text-muted);
319
+ }
320
+
321
+ .field input[type="text"],
322
+ .field textarea {
323
+ width: 100%;
324
+ padding: 0.625rem 0.875rem;
325
+ font-family: var(--font-display);
326
+ font-size: 1.125rem;
327
+ color: var(--text);
328
+ background: var(--surface-elevated);
329
+ border: 1.5px solid var(--border);
330
+ border-radius: 6px;
331
+ transition: border-color 0.15s, box-shadow 0.15s;
332
+ }
333
+
334
+ .field input[type="text"]:focus,
335
+ .field textarea:focus {
336
+ outline: none;
337
+ border-color: var(--accent);
338
+ box-shadow: 0 0 0 3px rgba(139, 69, 19, 0.15);
339
+ }
340
+
341
+ .field input[type="text"]::placeholder {
342
+ color: var(--text-muted);
343
+ opacity: 0.6;
344
+ }
345
+
346
+ .field-checkbox {
347
+ display: flex;
348
+ align-items: center;
349
+ gap: 0.625rem;
350
+ }
351
+
352
+ .field-checkbox label {
353
+ margin: 0;
354
+ font-size: 0.9375rem;
355
+ color: var(--text);
356
+ cursor: pointer;
357
+ }
358
+
359
+ .field-checkbox input[type="checkbox"] {
360
+ appearance: none;
361
+ width: 1.25rem;
362
+ height: 1.25rem;
363
+ background: var(--checkbox-bg);
364
+ border: 1.5px solid var(--border);
365
+ border-radius: 4px;
366
+ cursor: pointer;
367
+ transition: border-color 0.15s, background 0.15s;
368
+ }
369
+
370
+ .field-checkbox input[type="checkbox"]:checked {
371
+ background: var(--accent);
372
+ border-color: var(--accent);
373
+ background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 16 16' fill='white' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M12.207 4.793a1 1 0 0 1 0 1.414l-5 5a1 1 0 0 1-1.414 0l-2.5-2.5a1 1 0 0 1 1.414-1.414L6.5 9.086l4.293-4.293a1 1 0 0 1 1.414 0z'/%3E%3C/svg%3E");
374
+ background-position: center;
375
+ background-repeat: no-repeat;
376
+ }
377
+
378
+ .form-actions {
379
+ display: flex;
380
+ gap: 1rem;
381
+ align-items: center;
382
+ margin-block-start: 2rem;
383
+ }
384
+
385
+ .error-list {
386
+ margin-block-end: 1.5rem;
387
+ padding: 1rem;
388
+ background: #f8e8e8;
389
+ border-inline-start: 3px solid #b53d3d;
390
+ border-radius: 0 6px 6px 0;
391
+ }
392
+
393
+ @media (prefers-color-scheme: dark) {
394
+ .error-list {
395
+ background: #2d1f1f;
396
+ }
397
+ }
398
+
399
+ .error-list h2 {
400
+ font-size: 0.875rem;
401
+ font-weight: 600;
402
+ color: #b53d3d;
403
+ margin-block-end: 0.5rem;
404
+ }
405
+
406
+ @media (prefers-color-scheme: dark) {
407
+ .error-list h2 {
408
+ color: #e57575;
409
+ }
410
+ }
411
+
412
+ .error-list ul {
413
+ margin: 0;
414
+ padding-inline-start: 1.25rem;
415
+ font-size: 0.875rem;
416
+ }
417
+
418
+ /* ═══════════════════════════════════════════════════════
419
+ DETAIL VIEW
420
+ ═══════════════════════════════════════════════════════ */
421
+
422
+ .detail {
423
+ max-width: 36rem;
424
+ }
425
+
426
+ .detail-title {
427
+ font-family: var(--font-display);
428
+ font-size: clamp(1.75rem, 1.4rem + 1.75vw, 2.5rem);
429
+ font-weight: 400;
430
+ letter-spacing: -0.015em;
431
+ line-height: 1.2;
432
+ margin-block-end: 0.75rem;
433
+ }
434
+
435
+ .detail-status {
436
+ display: inline-flex;
437
+ align-items: center;
438
+ gap: 0.375rem;
439
+ padding: 0.25rem 0.625rem;
440
+ font-size: 0.8125rem;
441
+ font-weight: 500;
442
+ border-radius: 100px;
443
+ }
444
+
445
+ .detail-status--pending {
446
+ color: var(--accent);
447
+ background: rgba(139, 69, 19, 0.12);
448
+ }
449
+
450
+ .detail-status--complete {
451
+ color: var(--success);
452
+ background: var(--success-bg);
453
+ }
454
+
455
+ .detail-actions {
456
+ display: flex;
457
+ gap: 0.75rem;
458
+ margin-block-start: 2.5rem;
459
+ padding-block-start: 1.5rem;
460
+ border-block-start: 1px solid var(--border);
461
+ }
462
+
463
+ /* ═══════════════════════════════════════════════════════
464
+ UTILITIES
465
+ ═══════════════════════════════════════════════════════ */
466
+
467
+ .mt-4 { margin-block-start: 1rem; }
468
+ .mt-6 { margin-block-start: 1.5rem; }
469
+ .mt-8 { margin-block-start: 2rem; }
@@ -0,0 +1,12 @@
1
+ class ApplicationController < ActionController::Base
2
+ # CSRF protection is unnecessary in Tokra desktop apps.
3
+ # Custom URL schemes (tokra://) cannot receive cross-site requests
4
+ # from external websites, eliminating the attack vector.
5
+ skip_forgery_protection
6
+
7
+ # Only allow modern browsers supporting webp images, web push, badges, import maps, CSS nesting, and CSS :has.
8
+ allow_browser versions: :modern
9
+
10
+ # Changes to the importmap will invalidate the etag for HTML responses
11
+ stale_when_importmap_changes
12
+ end
@@ -0,0 +1,70 @@
1
+ class TodosController < ApplicationController
2
+ before_action :set_todo, only: %i[ show edit update destroy ]
3
+
4
+ # GET /todos or /todos.json
5
+ def index
6
+ @todos = Todo.all
7
+ end
8
+
9
+ # GET /todos/1 or /todos/1.json
10
+ def show
11
+ end
12
+
13
+ # GET /todos/new
14
+ def new
15
+ @todo = Todo.new
16
+ end
17
+
18
+ # GET /todos/1/edit
19
+ def edit
20
+ end
21
+
22
+ # POST /todos or /todos.json
23
+ def create
24
+ @todo = Todo.new(todo_params)
25
+
26
+ respond_to do |format|
27
+ if @todo.save
28
+ format.html { redirect_to @todo, notice: "Todo was successfully created." }
29
+ format.json { render :show, status: :created, location: @todo }
30
+ else
31
+ format.html { render :new, status: :unprocessable_entity }
32
+ format.json { render json: @todo.errors, status: :unprocessable_entity }
33
+ end
34
+ end
35
+ end
36
+
37
+ # PATCH/PUT /todos/1 or /todos/1.json
38
+ def update
39
+ respond_to do |format|
40
+ if @todo.update(todo_params)
41
+ format.html { redirect_to @todo, notice: "Todo was successfully updated.", status: :see_other }
42
+ format.json { render :show, status: :ok, location: @todo }
43
+ else
44
+ format.html { render :edit, status: :unprocessable_entity }
45
+ format.json { render json: @todo.errors, status: :unprocessable_entity }
46
+ end
47
+ end
48
+ end
49
+
50
+ # DELETE /todos/1 or /todos/1.json
51
+ def destroy
52
+ @todo.destroy!
53
+
54
+ respond_to do |format|
55
+ format.html { redirect_to todos_path, notice: "Todo was successfully destroyed.", status: :see_other }
56
+ format.json { head :no_content }
57
+ end
58
+ end
59
+
60
+ private
61
+ # Use callbacks to share common setup or constraints between actions.
62
+ def set_todo
63
+ @todo = Todo.find(params.expect(:id))
64
+ end
65
+
66
+ # Only allow a list of trusted parameters through.
67
+ def todo_params
68
+ params.expect(todo: [ :title, :completed ])
69
+ end
70
+ end
@@ -0,0 +1,2 @@
1
+ module ApplicationHelper
2
+ end
@@ -0,0 +1,2 @@
1
+ module TodosHelper
2
+ end
@@ -0,0 +1,3 @@
1
+ // Configure your import map in config/importmap.rb. Read more: https://github.com/rails/importmap-rails
2
+ import "@hotwired/turbo-rails"
3
+ import "controllers"
@@ -0,0 +1,9 @@
1
+ import { Application } from "@hotwired/stimulus"
2
+
3
+ const application = Application.start()
4
+
5
+ // Configure Stimulus development experience
6
+ application.debug = false
7
+ window.Stimulus = application
8
+
9
+ export { application }
@@ -0,0 +1,7 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+
3
+ export default class extends Controller {
4
+ connect() {
5
+ this.element.textContent = "Hello World!"
6
+ }
7
+ }
@@ -0,0 +1,4 @@
1
+ // Import and register all your controllers from the importmap via controllers/**/*_controller
2
+ import { application } from "controllers/application"
3
+ import { eagerLoadControllersFrom } from "@hotwired/stimulus-loading"
4
+ eagerLoadControllersFrom("controllers", application)
@@ -0,0 +1,7 @@
1
+ class ApplicationJob < ActiveJob::Base
2
+ # Automatically retry jobs that encountered a deadlock
3
+ # retry_on ActiveRecord::Deadlocked
4
+
5
+ # Most jobs are safe to ignore if the underlying records are no longer available
6
+ # discard_on ActiveJob::DeserializationError
7
+ end
@@ -0,0 +1,4 @@
1
+ class ApplicationMailer < ActionMailer::Base
2
+ default from: "from@example.com"
3
+ layout "mailer"
4
+ end
@@ -0,0 +1,3 @@
1
+ class ApplicationRecord < ActiveRecord::Base
2
+ primary_abstract_class
3
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ #--
4
+ # SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
5
+ # SPDX-License-Identifier: AGPL-3.0-or-later
6
+ #++
7
+
8
+ class Todo < ApplicationRecord
9
+ validates :title, presence: true
10
+ end
@@ -0,0 +1,24 @@
1
+ <!--
2
+ SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
3
+
4
+ SPDX-License-Identifier: AGPL-3.0-or-later
5
+ -->
6
+ <!DOCTYPE html>
7
+ <html lang="en">
8
+ <head>
9
+ <title><%= content_for(:title) || "Tokra Tasks" %></title>
10
+ <meta charset="utf-8">
11
+ <meta name="viewport" content="width=device-width,initial-scale=1">
12
+ <%= csrf_meta_tags %>
13
+
14
+ <!-- Static CSS (avoiding asset pipeline for now) -->
15
+ <link rel="stylesheet" href="/styles.css">
16
+ </head>
17
+
18
+ <body>
19
+ <% if notice.present? %>
20
+ <div class="notice"><%= notice %></div>
21
+ <% end %>
22
+ <%= yield %>
23
+ </body>
24
+ </html>
@@ -0,0 +1,13 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
5
+ <style>
6
+ /* Email styles need to be inline */
7
+ </style>
8
+ </head>
9
+
10
+ <body>
11
+ <%= yield %>
12
+ </body>
13
+ </html>
@@ -0,0 +1,22 @@
1
+ {
2
+ "name": "VerifyRailsSqlite",
3
+ "icons": [
4
+ {
5
+ "src": "/icon.png",
6
+ "type": "image/png",
7
+ "sizes": "512x512"
8
+ },
9
+ {
10
+ "src": "/icon.png",
11
+ "type": "image/png",
12
+ "sizes": "512x512",
13
+ "purpose": "maskable"
14
+ }
15
+ ],
16
+ "start_url": "/",
17
+ "display": "standalone",
18
+ "scope": "/",
19
+ "description": "VerifyRailsSqlite.",
20
+ "theme_color": "red",
21
+ "background_color": "red"
22
+ }