elm-ssr 1.0.0 → 1.0.1
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.
- package/lib/scaffold.mjs +397 -217
- package/package.json +1 -1
package/lib/scaffold.mjs
CHANGED
|
@@ -48,10 +48,10 @@ const elmJsonTemplate = () => ({
|
|
|
48
48
|
}
|
|
49
49
|
});
|
|
50
50
|
|
|
51
|
-
const sharedTemplate = (namespace) => `module ${namespace}.View.Shared exposing (head,
|
|
51
|
+
const sharedTemplate = (namespace, auth) => `module ${namespace}.View.Shared exposing (head, layout)
|
|
52
52
|
|
|
53
|
-
import ElmSsr.Html exposing (Node, div,
|
|
54
|
-
import ElmSsr.Html.Attributes exposing (class)
|
|
53
|
+
import ElmSsr.Html exposing (Node, a, div, header, main_, nav, span, text)
|
|
54
|
+
import ElmSsr.Html.Attributes exposing (class, href)
|
|
55
55
|
import ElmSsr.Page as Page
|
|
56
56
|
|
|
57
57
|
|
|
@@ -63,19 +63,33 @@ head =
|
|
|
63
63
|
]
|
|
64
64
|
|
|
65
65
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
div [ class "
|
|
66
|
+
layout : String -> List (Node msg) -> Node msg
|
|
67
|
+
layout pageTitle body =
|
|
68
|
+
div [ class "page" ]
|
|
69
|
+
[ header [ class "header" ]
|
|
70
|
+
[ div [ class "header-inner" ]
|
|
71
|
+
[ a [ class "brand", href "/" ]
|
|
72
|
+
[ span [ class "brand-icon" ] [ text "◆" ]
|
|
73
|
+
, text "elm-ssr"
|
|
74
|
+
]
|
|
75
|
+
, nav [ class "nav" ]
|
|
76
|
+
[ a [ class "nav-link", href "/" ] [ text "Home" ]
|
|
77
|
+
, a [ class "nav-link", href "/counter" ] [ text "Counter" ]${auth ? `
|
|
78
|
+
, a [ class "nav-link", href "/login" ] [ text "Sign in" ]` : ""}
|
|
79
|
+
]
|
|
80
|
+
]
|
|
81
|
+
]
|
|
82
|
+
, main_ [ class "main" ]
|
|
83
|
+
[ div [ class "container" ] body
|
|
84
|
+
]
|
|
85
|
+
]
|
|
69
86
|
`;
|
|
70
87
|
|
|
71
|
-
const indexRouteTemplate = (namespace) => `module ${namespace}.Routes.Index exposing (page, action)
|
|
72
|
-
|
|
73
|
-
-- File-based routing: this module maps to GET /.
|
|
74
|
-
-- A page is stateless (no Model/Msg) and ships no client JavaScript.
|
|
88
|
+
const indexRouteTemplate = (namespace, auth) => `module ${namespace}.Routes.Index exposing (page, action)
|
|
75
89
|
|
|
76
90
|
import ElmSsr.Action as Action exposing (Action)
|
|
77
91
|
import ElmSsr.Document exposing (Document)
|
|
78
|
-
import ElmSsr.Html exposing (a, p, text)
|
|
92
|
+
import ElmSsr.Html exposing (a, div, h1, h2, p, section, span, text)
|
|
79
93
|
import ElmSsr.Html.Attributes exposing (class, href)
|
|
80
94
|
import ElmSsr.Loader as Loader exposing (Loader)
|
|
81
95
|
import ElmSsr.Page as Page
|
|
@@ -85,10 +99,7 @@ import ${namespace}.View.Shared as Shared
|
|
|
85
99
|
|
|
86
100
|
page : Request -> Loader (Document Never)
|
|
87
101
|
page _ =
|
|
88
|
-
Loader.
|
|
89
|
-
|> Loader.map (\\maybeGreeting ->
|
|
90
|
-
view (Maybe.withDefault "Hello from default env!" maybeGreeting)
|
|
91
|
-
)
|
|
102
|
+
Loader.succeed view
|
|
92
103
|
|
|
93
104
|
|
|
94
105
|
action : Request -> Action (Document Never)
|
|
@@ -96,30 +107,47 @@ action _ =
|
|
|
96
107
|
Action.fail 405 "Method not allowed"
|
|
97
108
|
|
|
98
109
|
|
|
99
|
-
view :
|
|
100
|
-
view
|
|
110
|
+
view : Document Never
|
|
111
|
+
view =
|
|
101
112
|
Page.page
|
|
102
|
-
{ title = "
|
|
113
|
+
{ title = "elm-ssr"
|
|
103
114
|
, head = Shared.head
|
|
104
115
|
, body =
|
|
105
|
-
[ Shared.
|
|
106
|
-
[
|
|
107
|
-
|
|
108
|
-
|
|
116
|
+
[ Shared.layout "Home"
|
|
117
|
+
[ section [ class "hero" ]
|
|
118
|
+
[ h1 [ class "hero-title" ] [ text "Ship fast." ]
|
|
119
|
+
, p [ class "hero-subtitle" ]
|
|
120
|
+
[ text "Type-safe server-side rendering with interactive islands. Runs on Cloudflare Workers and Bun." ]
|
|
121
|
+
, div [ class "hero-actions" ]
|
|
122
|
+
[ a [ class "btn btn-primary", href "/counter" ] [ text "Try the counter" ]${auth ? `
|
|
123
|
+
, a [ class "btn btn-secondary", href "/login" ] [ text "Sign in" ]` : ""}
|
|
124
|
+
]
|
|
125
|
+
]
|
|
126
|
+
, section [ class "features" ]
|
|
127
|
+
[ featureCard "⚡" "Edge-first" "Renders in milliseconds at the edge. No cold starts."
|
|
128
|
+
, featureCard "🦺" "Fully typed" "End-to-end Elm types from DB to HTML. No runtime surprises."
|
|
129
|
+
, featureCard "🏝️" "Islands" "Add interactivity exactly where you need it. Zero JS elsewhere."
|
|
130
|
+
]
|
|
109
131
|
]
|
|
110
132
|
]
|
|
111
133
|
}
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
featureCard : String -> String -> String -> ElmSsr.Html.Node msg
|
|
137
|
+
featureCard icon title_ body =
|
|
138
|
+
div [ class "feature-card" ]
|
|
139
|
+
[ span [ class "feature-icon" ] [ text icon ]
|
|
140
|
+
, h2 [ class "feature-title" ] [ text title_ ]
|
|
141
|
+
, p [ class "feature-body" ] [ text body ]
|
|
142
|
+
]
|
|
112
143
|
`;
|
|
113
144
|
|
|
114
145
|
const counterRouteTemplate = (namespace) => `module ${namespace}.Routes.Counter exposing (page, action)
|
|
115
146
|
|
|
116
|
-
-- File-based routing: GET /counter. A static page that embeds an interactive
|
|
117
|
-
-- island. The page ships no client runtime; the browser mounts the island
|
|
118
|
-
-- separately, and only that root updates.
|
|
119
|
-
|
|
120
147
|
import ElmSsr.Action as Action exposing (Action)
|
|
121
148
|
import ElmSsr.Document exposing (Document)
|
|
122
|
-
import ElmSsr.Html exposing (p, text)
|
|
149
|
+
import ElmSsr.Html exposing (div, h1, p, text)
|
|
150
|
+
import ElmSsr.Html.Attributes exposing (class)
|
|
123
151
|
import ElmSsr.Loader as Loader exposing (Loader)
|
|
124
152
|
import ElmSsr.Page as Page
|
|
125
153
|
import ElmSsr.Route exposing (Request)
|
|
@@ -143,9 +171,14 @@ view =
|
|
|
143
171
|
{ title = "Counter | elm-ssr"
|
|
144
172
|
, head = Shared.head
|
|
145
173
|
, body =
|
|
146
|
-
[ Shared.
|
|
147
|
-
[
|
|
148
|
-
|
|
174
|
+
[ Shared.layout "Counter"
|
|
175
|
+
[ div [ class "page-header" ]
|
|
176
|
+
[ h1 [] [ text "Interactive Counter" ]
|
|
177
|
+
, p [ class "page-subtitle" ]
|
|
178
|
+
[ text "This page is server-rendered. Only the counter widget below is a client-side island — zero JS elsewhere." ]
|
|
179
|
+
]
|
|
180
|
+
, div [ class "card" ]
|
|
181
|
+
[ Counter.embed { start = 0 } ]
|
|
149
182
|
]
|
|
150
183
|
]
|
|
151
184
|
}
|
|
@@ -238,19 +271,18 @@ subscriptions _ =
|
|
|
238
271
|
view : Model -> Html Msg
|
|
239
272
|
view model =
|
|
240
273
|
div [ class "counter" ]
|
|
241
|
-
[ button [ class "
|
|
242
|
-
, span [ class "value" ] [ text (String.fromInt model.count) ]
|
|
243
|
-
, button [ class "
|
|
274
|
+
[ button [ class "btn btn-secondary btn-square", type_ "button", onClick Decrement ] [ text "−" ]
|
|
275
|
+
, span [ class "counter-value" ] [ text (String.fromInt model.count) ]
|
|
276
|
+
, button [ class "btn btn-primary btn-square", type_ "button", onClick Increment ] [ text "+" ]
|
|
244
277
|
]
|
|
245
278
|
`;
|
|
246
279
|
|
|
247
280
|
const notFoundRouteTemplate = (namespace) => `module ${namespace}.Routes.NotFound exposing (page, action)
|
|
248
281
|
|
|
249
|
-
-- File-based routing: NotFound is the fallback when no other route matches.
|
|
250
|
-
|
|
251
282
|
import ElmSsr.Action as Action exposing (Action)
|
|
252
283
|
import ElmSsr.Document exposing (Document)
|
|
253
|
-
import ElmSsr.Html exposing (p, text)
|
|
284
|
+
import ElmSsr.Html exposing (a, div, h1, p, text)
|
|
285
|
+
import ElmSsr.Html.Attributes exposing (class, href)
|
|
254
286
|
import ElmSsr.Loader as Loader exposing (Loader)
|
|
255
287
|
import ElmSsr.Page as Page
|
|
256
288
|
import ElmSsr.Route exposing (Request)
|
|
@@ -272,44 +304,63 @@ view =
|
|
|
272
304
|
Page.notFound
|
|
273
305
|
{ title = "Not Found | elm-ssr"
|
|
274
306
|
, head = Shared.head
|
|
275
|
-
, body =
|
|
307
|
+
, body =
|
|
308
|
+
[ Shared.layout "Not Found"
|
|
309
|
+
[ div [ class "error-page" ]
|
|
310
|
+
[ h1 [ class "error-code" ] [ text "404" ]
|
|
311
|
+
, p [ class "error-message" ] [ text "This page doesn't exist." ]
|
|
312
|
+
, a [ class "btn btn-primary", href "/" ] [ text "Go home" ]
|
|
313
|
+
]
|
|
314
|
+
]
|
|
315
|
+
]
|
|
276
316
|
}
|
|
277
317
|
`;
|
|
278
318
|
|
|
319
|
+
const authDisplayName = (authProvider) =>
|
|
320
|
+
authProvider === "auth0" ? "Auth0" : "BetterAuth";
|
|
321
|
+
|
|
279
322
|
const loginRouteTemplate = (namespace, authProvider) => `module ${namespace}.Routes.Login exposing (page, action)
|
|
280
323
|
|
|
281
324
|
import ElmSsr.Action as Action exposing (Action)
|
|
282
325
|
import ElmSsr.Document exposing (Document)
|
|
283
|
-
import ElmSsr.Html exposing (a, div, p, text)
|
|
284
|
-
import ElmSsr.Html.Attributes
|
|
326
|
+
import ElmSsr.Html exposing (a, div, h1, p, span, text)
|
|
327
|
+
import ElmSsr.Html.Attributes exposing (class, href)
|
|
285
328
|
import ElmSsr.Loader as Loader exposing (Loader)
|
|
286
329
|
import ElmSsr.Page as Page
|
|
287
330
|
import ElmSsr.Route exposing (Request)
|
|
288
331
|
import ${namespace}.View.Shared as Shared
|
|
289
332
|
|
|
333
|
+
|
|
290
334
|
page : Request -> Loader (Document Never)
|
|
291
335
|
page _ =
|
|
292
336
|
Loader.succeed view
|
|
293
337
|
|
|
338
|
+
|
|
294
339
|
action : Request -> Action (Document Never)
|
|
295
340
|
action _ =
|
|
296
341
|
Action.fail 405 "Method not allowed"
|
|
297
342
|
|
|
343
|
+
|
|
298
344
|
view : Document Never
|
|
299
345
|
view =
|
|
300
346
|
Page.page
|
|
301
|
-
{ title = "
|
|
347
|
+
{ title = "Sign in | elm-ssr"
|
|
302
348
|
, head = Shared.head
|
|
303
349
|
, body =
|
|
304
|
-
[ Shared.
|
|
305
|
-
[ div [
|
|
306
|
-
[
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
[
|
|
310
|
-
,
|
|
311
|
-
]
|
|
312
|
-
|
|
350
|
+
[ Shared.layout "Sign in"
|
|
351
|
+
[ div [ class "auth-page" ]
|
|
352
|
+
[ div [ class "auth-card" ]
|
|
353
|
+
[ div [ class "auth-header" ]
|
|
354
|
+
[ span [ class "auth-logo" ] [ text "◆" ]
|
|
355
|
+
, h1 [ class "auth-title" ] [ text "Welcome back" ]
|
|
356
|
+
, p [ class "auth-subtitle" ] [ text "Sign in to your account to continue." ]
|
|
357
|
+
]
|
|
358
|
+
, div [ class "auth-body" ]
|
|
359
|
+
[ a [ class "btn btn-primary btn-full", href "/api/auth/login" ]
|
|
360
|
+
[ text "Continue with ${authDisplayName(authProvider)}" ]
|
|
361
|
+
]
|
|
362
|
+
, p [ class "auth-footer" ]
|
|
363
|
+
[ text "Powered by ${authDisplayName(authProvider)} via elm-ssr" ]
|
|
313
364
|
]
|
|
314
365
|
]
|
|
315
366
|
]
|
|
@@ -321,47 +372,61 @@ const profileRouteTemplate = (namespace) => `module ${namespace}.Routes.Profile
|
|
|
321
372
|
|
|
322
373
|
import ElmSsr.Action as Action exposing (Action)
|
|
323
374
|
import ElmSsr.Document exposing (Document)
|
|
324
|
-
import ElmSsr.Html exposing (a, div,
|
|
325
|
-
import ElmSsr.Html.Attributes
|
|
375
|
+
import ElmSsr.Html exposing (a, div, h1, p, span, text)
|
|
376
|
+
import ElmSsr.Html.Attributes exposing (class, href)
|
|
326
377
|
import ElmSsr.Loader as Loader exposing (Loader)
|
|
327
378
|
import ElmSsr.Page as Page
|
|
328
|
-
import ElmSsr.Route
|
|
379
|
+
import ElmSsr.Route exposing (Request)
|
|
329
380
|
import ${namespace}.View.Shared as Shared
|
|
330
381
|
import Json.Decode as Decode
|
|
331
382
|
|
|
383
|
+
|
|
332
384
|
type alias UserProfile =
|
|
333
385
|
{ email : String
|
|
334
386
|
, name : Maybe String
|
|
335
387
|
}
|
|
336
388
|
|
|
389
|
+
|
|
337
390
|
userDecoder : Decode.Decoder UserProfile
|
|
338
391
|
userDecoder =
|
|
339
392
|
Decode.map2 UserProfile
|
|
340
393
|
(Decode.field "email" Decode.string)
|
|
341
394
|
(Decode.maybe (Decode.field "name" Decode.string))
|
|
342
395
|
|
|
396
|
+
|
|
343
397
|
page : Request -> Loader (Document Never)
|
|
344
398
|
page _ =
|
|
345
399
|
Loader.requireUser userDecoder "/login" (\\user ->
|
|
346
400
|
Loader.succeed (view user)
|
|
347
401
|
)
|
|
348
402
|
|
|
403
|
+
|
|
349
404
|
action : Request -> Action (Document Never)
|
|
350
405
|
action _ =
|
|
351
406
|
Action.fail 405 "Method not allowed"
|
|
352
407
|
|
|
408
|
+
|
|
353
409
|
view : UserProfile -> Document Never
|
|
354
410
|
view user =
|
|
355
411
|
Page.page
|
|
356
412
|
{ title = "Profile | elm-ssr"
|
|
357
413
|
, head = Shared.head
|
|
358
414
|
, body =
|
|
359
|
-
[ Shared.
|
|
360
|
-
[ div []
|
|
361
|
-
[
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
415
|
+
[ Shared.layout "Profile"
|
|
416
|
+
[ div [ class "auth-page" ]
|
|
417
|
+
[ div [ class "auth-card" ]
|
|
418
|
+
[ div [ class "auth-header" ]
|
|
419
|
+
[ span [ class "avatar" ]
|
|
420
|
+
[ text (String.left 1 (Maybe.withDefault user.email user.name) |> String.toUpper) ]
|
|
421
|
+
, h1 [ class "auth-title" ]
|
|
422
|
+
[ text (Maybe.withDefault "Your account" user.name) ]
|
|
423
|
+
, p [ class "auth-subtitle" ] [ text user.email ]
|
|
424
|
+
]
|
|
425
|
+
, div [ class "auth-body" ]
|
|
426
|
+
[ a [ class "btn btn-secondary btn-full", href "/api/auth/logout" ]
|
|
427
|
+
[ text "Sign out" ]
|
|
428
|
+
]
|
|
429
|
+
]
|
|
365
430
|
]
|
|
366
431
|
]
|
|
367
432
|
]
|
|
@@ -635,172 +700,289 @@ export default worker;
|
|
|
635
700
|
const stylesTemplate = () => `export const stylesheet = \`
|
|
636
701
|
@import url("https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap");
|
|
637
702
|
|
|
638
|
-
*, *::before, *::after {
|
|
639
|
-
box-sizing: border-box;
|
|
640
|
-
}
|
|
703
|
+
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
641
704
|
|
|
642
705
|
:root {
|
|
643
|
-
|
|
706
|
+
--bg: #f6f5f3;
|
|
707
|
+
--surface: #ffffff;
|
|
708
|
+
--border: #e5e2dd;
|
|
709
|
+
--text: #1a1a1a;
|
|
710
|
+
--text-muted: #6b6b6b;
|
|
711
|
+
--accent: #1a1a1a;
|
|
712
|
+
--accent-hover: #333;
|
|
713
|
+
--radius: 12px;
|
|
714
|
+
--header-h: 60px;
|
|
644
715
|
font-family: "Inter", ui-sans-serif, system-ui, -apple-system, sans-serif;
|
|
645
716
|
font-size: 16px;
|
|
646
|
-
color:
|
|
717
|
+
color: var(--text);
|
|
718
|
+
background: var(--bg);
|
|
647
719
|
}
|
|
648
720
|
|
|
649
|
-
body {
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
721
|
+
body { min-height: 100vh; }
|
|
722
|
+
|
|
723
|
+
/* ── Layout ────────────────────────────────────────── */
|
|
724
|
+
.page { display: flex; flex-direction: column; min-height: 100vh; }
|
|
725
|
+
|
|
726
|
+
.header {
|
|
727
|
+
height: var(--header-h);
|
|
728
|
+
border-bottom: 1px solid var(--border);
|
|
729
|
+
background: rgba(246, 245, 243, 0.85);
|
|
730
|
+
backdrop-filter: blur(12px);
|
|
731
|
+
position: sticky;
|
|
732
|
+
top: 0;
|
|
733
|
+
z-index: 10;
|
|
655
734
|
}
|
|
656
735
|
|
|
657
|
-
.
|
|
658
|
-
max-width:
|
|
736
|
+
.header-inner {
|
|
737
|
+
max-width: 1100px;
|
|
659
738
|
margin: 0 auto;
|
|
660
|
-
padding:
|
|
739
|
+
padding: 0 1.5rem;
|
|
740
|
+
height: 100%;
|
|
741
|
+
display: flex;
|
|
742
|
+
align-items: center;
|
|
743
|
+
justify-content: space-between;
|
|
661
744
|
}
|
|
662
745
|
|
|
663
|
-
|
|
664
|
-
|
|
746
|
+
.brand {
|
|
747
|
+
display: flex;
|
|
748
|
+
align-items: center;
|
|
749
|
+
gap: 0.5rem;
|
|
665
750
|
font-weight: 700;
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
h2 {
|
|
672
|
-
font-size: 1.4rem;
|
|
673
|
-
font-weight: 600;
|
|
674
|
-
margin: 0 0 0.75rem;
|
|
675
|
-
color: #18222f;
|
|
751
|
+
font-size: 1rem;
|
|
752
|
+
color: var(--text);
|
|
753
|
+
text-decoration: none;
|
|
754
|
+
letter-spacing: -0.01em;
|
|
676
755
|
}
|
|
677
756
|
|
|
678
|
-
|
|
679
|
-
line-height: 1.6;
|
|
680
|
-
margin: 0 0 1rem;
|
|
681
|
-
color: #4a5568;
|
|
682
|
-
}
|
|
757
|
+
.brand-icon { font-size: 0.85em; opacity: 0.6; }
|
|
683
758
|
|
|
684
|
-
|
|
685
|
-
color: #18222f;
|
|
686
|
-
text-decoration: underline;
|
|
687
|
-
text-underline-offset: 2px;
|
|
688
|
-
}
|
|
759
|
+
.nav { display: flex; align-items: center; gap: 0.25rem; }
|
|
689
760
|
|
|
690
|
-
|
|
761
|
+
.nav-link {
|
|
762
|
+
padding: 0.4rem 0.75rem;
|
|
763
|
+
border-radius: 6px;
|
|
764
|
+
font-size: 0.875rem;
|
|
765
|
+
font-weight: 500;
|
|
766
|
+
color: var(--text-muted);
|
|
691
767
|
text-decoration: none;
|
|
768
|
+
transition: background 0.12s, color 0.12s;
|
|
692
769
|
}
|
|
770
|
+
.nav-link:hover { background: var(--border); color: var(--text); }
|
|
771
|
+
|
|
772
|
+
.main { flex: 1; padding: 3rem 1.5rem; }
|
|
773
|
+
|
|
774
|
+
.container { max-width: 1100px; margin: 0 auto; }
|
|
693
775
|
|
|
694
|
-
|
|
776
|
+
/* ── Typography ────────────────────────────────────── */
|
|
777
|
+
h1 { font-size: 2rem; font-weight: 700; letter-spacing: -0.03em; line-height: 1.2; }
|
|
778
|
+
h2 { font-size: 1.25rem; font-weight: 600; letter-spacing: -0.01em; }
|
|
779
|
+
p { line-height: 1.65; color: var(--text-muted); }
|
|
780
|
+
|
|
781
|
+
/* ── Buttons ────────────────────────────────────────── */
|
|
782
|
+
.btn {
|
|
695
783
|
display: inline-flex;
|
|
696
784
|
align-items: center;
|
|
785
|
+
justify-content: center;
|
|
697
786
|
gap: 0.5rem;
|
|
698
|
-
|
|
699
|
-
border:
|
|
700
|
-
|
|
787
|
+
padding: 0.55rem 1.1rem;
|
|
788
|
+
border-radius: 8px;
|
|
789
|
+
border: 1.5px solid transparent;
|
|
701
790
|
font: inherit;
|
|
702
|
-
font-size: 0.
|
|
791
|
+
font-size: 0.875rem;
|
|
703
792
|
font-weight: 500;
|
|
704
|
-
background: white;
|
|
705
|
-
color: #18222f;
|
|
706
793
|
cursor: pointer;
|
|
707
794
|
text-decoration: none;
|
|
708
|
-
transition: background 0.
|
|
795
|
+
transition: background 0.12s, color 0.12s, border-color 0.12s;
|
|
796
|
+
white-space: nowrap;
|
|
709
797
|
}
|
|
710
798
|
|
|
711
|
-
.
|
|
712
|
-
background:
|
|
799
|
+
.btn-primary {
|
|
800
|
+
background: var(--accent);
|
|
801
|
+
color: white;
|
|
802
|
+
border-color: var(--accent);
|
|
713
803
|
}
|
|
804
|
+
.btn-primary:hover { background: var(--accent-hover); border-color: var(--accent-hover); }
|
|
714
805
|
|
|
715
|
-
.
|
|
716
|
-
background:
|
|
717
|
-
color:
|
|
718
|
-
border-color:
|
|
806
|
+
.btn-secondary {
|
|
807
|
+
background: var(--surface);
|
|
808
|
+
color: var(--text);
|
|
809
|
+
border-color: var(--border);
|
|
810
|
+
}
|
|
811
|
+
.btn-secondary:hover { background: var(--bg); }
|
|
812
|
+
|
|
813
|
+
.btn-square { width: 2.5rem; height: 2.5rem; padding: 0; font-size: 1.1rem; }
|
|
814
|
+
|
|
815
|
+
.btn-full { width: 100%; }
|
|
816
|
+
|
|
817
|
+
/* ── Cards ──────────────────────────────────────────── */
|
|
818
|
+
.card {
|
|
819
|
+
background: var(--surface);
|
|
820
|
+
border: 1px solid var(--border);
|
|
821
|
+
border-radius: var(--radius);
|
|
822
|
+
padding: 1.5rem;
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
/* ── Hero ────────────────────────────────────────────── */
|
|
826
|
+
.hero {
|
|
827
|
+
text-align: center;
|
|
828
|
+
padding: 5rem 0 4rem;
|
|
829
|
+
max-width: 640px;
|
|
830
|
+
margin: 0 auto;
|
|
719
831
|
}
|
|
720
832
|
|
|
721
|
-
.
|
|
722
|
-
|
|
833
|
+
.hero-title {
|
|
834
|
+
font-size: clamp(2.5rem, 6vw, 4rem);
|
|
835
|
+
font-weight: 800;
|
|
836
|
+
letter-spacing: -0.04em;
|
|
837
|
+
margin-bottom: 1rem;
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
.hero-subtitle {
|
|
841
|
+
font-size: 1.15rem;
|
|
842
|
+
margin-bottom: 2rem;
|
|
843
|
+
max-width: 480px;
|
|
844
|
+
margin-left: auto;
|
|
845
|
+
margin-right: auto;
|
|
723
846
|
}
|
|
724
847
|
|
|
848
|
+
.hero-actions {
|
|
849
|
+
display: flex;
|
|
850
|
+
gap: 0.75rem;
|
|
851
|
+
justify-content: center;
|
|
852
|
+
flex-wrap: wrap;
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
/* ── Features ────────────────────────────────────────── */
|
|
856
|
+
.features {
|
|
857
|
+
display: grid;
|
|
858
|
+
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
|
859
|
+
gap: 1rem;
|
|
860
|
+
margin-top: 4rem;
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
.feature-card {
|
|
864
|
+
background: var(--surface);
|
|
865
|
+
border: 1px solid var(--border);
|
|
866
|
+
border-radius: var(--radius);
|
|
867
|
+
padding: 1.5rem;
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
.feature-icon { font-size: 1.5rem; display: block; margin-bottom: 0.75rem; }
|
|
871
|
+
|
|
872
|
+
.feature-title {
|
|
873
|
+
font-size: 1rem;
|
|
874
|
+
font-weight: 600;
|
|
875
|
+
margin-bottom: 0.5rem;
|
|
876
|
+
color: var(--text);
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
.feature-body { font-size: 0.9rem; }
|
|
880
|
+
|
|
881
|
+
/* ── Page header ─────────────────────────────────────── */
|
|
882
|
+
.page-header { margin-bottom: 2rem; }
|
|
883
|
+
.page-header h1 { margin-bottom: 0.5rem; }
|
|
884
|
+
.page-subtitle { font-size: 0.95rem; }
|
|
885
|
+
|
|
886
|
+
/* ── Counter island ──────────────────────────────────── */
|
|
725
887
|
.counter {
|
|
726
888
|
display: grid;
|
|
727
889
|
grid-template-columns: auto 1fr auto;
|
|
728
|
-
gap: 0.75rem;
|
|
729
890
|
align-items: center;
|
|
730
|
-
|
|
891
|
+
gap: 1rem;
|
|
731
892
|
}
|
|
732
893
|
|
|
733
|
-
.value {
|
|
894
|
+
.counter-value {
|
|
734
895
|
text-align: center;
|
|
735
|
-
font-size:
|
|
896
|
+
font-size: 3rem;
|
|
736
897
|
font-weight: 700;
|
|
737
|
-
|
|
898
|
+
letter-spacing: -0.03em;
|
|
738
899
|
}
|
|
739
900
|
|
|
740
|
-
|
|
901
|
+
/* ── Auth pages ──────────────────────────────────────── */
|
|
902
|
+
.auth-page {
|
|
741
903
|
display: flex;
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
904
|
+
align-items: flex-start;
|
|
905
|
+
justify-content: center;
|
|
906
|
+
padding-top: 3rem;
|
|
745
907
|
}
|
|
746
908
|
|
|
747
|
-
.
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
909
|
+
.auth-card {
|
|
910
|
+
background: var(--surface);
|
|
911
|
+
border: 1px solid var(--border);
|
|
912
|
+
border-radius: 16px;
|
|
913
|
+
padding: 2.5rem;
|
|
914
|
+
width: 100%;
|
|
915
|
+
max-width: 380px;
|
|
751
916
|
}
|
|
752
917
|
|
|
753
|
-
.
|
|
754
|
-
.field span {
|
|
755
|
-
font-size: 0.875rem;
|
|
756
|
-
font-weight: 500;
|
|
757
|
-
color: #4a5568;
|
|
758
|
-
}
|
|
918
|
+
.auth-header { text-align: center; margin-bottom: 2rem; }
|
|
759
919
|
|
|
760
|
-
.
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
padding: 0.65rem 0.9rem;
|
|
765
|
-
font: inherit;
|
|
766
|
-
font-size: 0.95rem;
|
|
767
|
-
background: white;
|
|
768
|
-
color: #18222f;
|
|
769
|
-
transition: border-color 0.15s;
|
|
920
|
+
.auth-logo {
|
|
921
|
+
font-size: 1.5rem;
|
|
922
|
+
display: block;
|
|
923
|
+
margin-bottom: 1rem;
|
|
770
924
|
}
|
|
771
925
|
|
|
772
|
-
.
|
|
773
|
-
|
|
774
|
-
border-color: #18222f;
|
|
775
|
-
}
|
|
926
|
+
.auth-title { font-size: 1.5rem; margin-bottom: 0.5rem; }
|
|
927
|
+
.auth-subtitle { font-size: 0.9rem; }
|
|
776
928
|
|
|
777
|
-
.
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
929
|
+
.auth-body { display: flex; flex-direction: column; gap: 0.75rem; }
|
|
930
|
+
|
|
931
|
+
.auth-footer {
|
|
932
|
+
text-align: center;
|
|
933
|
+
font-size: 0.75rem;
|
|
934
|
+
color: var(--text-muted);
|
|
935
|
+
margin-top: 1.5rem;
|
|
784
936
|
}
|
|
785
937
|
|
|
786
|
-
.
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
938
|
+
.avatar {
|
|
939
|
+
display: inline-flex;
|
|
940
|
+
align-items: center;
|
|
941
|
+
justify-content: center;
|
|
942
|
+
width: 3.5rem;
|
|
943
|
+
height: 3.5rem;
|
|
944
|
+
border-radius: 50%;
|
|
945
|
+
background: var(--accent);
|
|
946
|
+
color: white;
|
|
947
|
+
font-size: 1.5rem;
|
|
948
|
+
font-weight: 700;
|
|
791
949
|
margin-bottom: 1rem;
|
|
792
950
|
}
|
|
793
951
|
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
952
|
+
/* ── Error page ──────────────────────────────────────── */
|
|
953
|
+
.error-page {
|
|
954
|
+
text-align: center;
|
|
955
|
+
padding: 5rem 0;
|
|
797
956
|
}
|
|
798
957
|
|
|
799
|
-
.error {
|
|
800
|
-
|
|
801
|
-
font-
|
|
802
|
-
|
|
958
|
+
.error-code {
|
|
959
|
+
font-size: 6rem;
|
|
960
|
+
font-weight: 800;
|
|
961
|
+
letter-spacing: -0.05em;
|
|
962
|
+
color: var(--border);
|
|
963
|
+
margin-bottom: 0.5rem;
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
.error-message { margin-bottom: 2rem; }
|
|
967
|
+
|
|
968
|
+
/* ── Forms ───────────────────────────────────────────── */
|
|
969
|
+
.field { display: flex; flex-direction: column; gap: 0.35rem; }
|
|
970
|
+
.field label, .field span { font-size: 0.875rem; font-weight: 500; }
|
|
971
|
+
|
|
972
|
+
.input {
|
|
973
|
+
width: 100%;
|
|
974
|
+
border-radius: 8px;
|
|
975
|
+
border: 1.5px solid var(--border);
|
|
976
|
+
padding: 0.6rem 0.9rem;
|
|
977
|
+
font: inherit;
|
|
978
|
+
font-size: 0.9rem;
|
|
979
|
+
background: white;
|
|
980
|
+
color: var(--text);
|
|
981
|
+
transition: border-color 0.12s;
|
|
803
982
|
}
|
|
983
|
+
.input:focus { outline: none; border-color: var(--accent); }
|
|
984
|
+
|
|
985
|
+
.error-hint { color: #c53030; font-size: 0.8rem; margin-top: 0.25rem; }
|
|
804
986
|
\`;
|
|
805
987
|
`;
|
|
806
988
|
|
|
@@ -815,8 +997,8 @@ const filesForApp = (name, appRoot, options = {}) => {
|
|
|
815
997
|
{ path: `${appRoot}/runtime.ts`, content: runtimeTemplate(appRoot, db, auth) },
|
|
816
998
|
{ path: `${appRoot}/worker.ts`, content: workerTemplate() },
|
|
817
999
|
{ path: `${appRoot}/styles.ts`, content: stylesTemplate() },
|
|
818
|
-
{ path: `${appRoot}/src/${namespace}/View/Shared.elm`, content: sharedTemplate(namespace) },
|
|
819
|
-
{ path: `${appRoot}/src/${namespace}/Routes/Index.elm`, content: indexRouteTemplate(namespace) },
|
|
1000
|
+
{ path: `${appRoot}/src/${namespace}/View/Shared.elm`, content: sharedTemplate(namespace, auth) },
|
|
1001
|
+
{ path: `${appRoot}/src/${namespace}/Routes/Index.elm`, content: indexRouteTemplate(namespace, auth) },
|
|
820
1002
|
{ path: `${appRoot}/src/${namespace}/Routes/Counter.elm`, content: counterRouteTemplate(namespace) },
|
|
821
1003
|
{ path: `${appRoot}/src/${namespace}/Routes/NotFound.elm`, content: notFoundRouteTemplate(namespace) },
|
|
822
1004
|
{ path: `${appRoot}/src/${namespace}/Islands/Counter.elm`, content: counterIslandTemplate(namespace) },
|
|
@@ -872,58 +1054,56 @@ SESSION_SECRET="change-me-to-a-secure-random-hmac-secret-key-that-is-at-least-32
|
|
|
872
1054
|
}
|
|
873
1055
|
|
|
874
1056
|
@layer components {
|
|
875
|
-
.
|
|
876
|
-
|
|
877
|
-
}
|
|
878
|
-
|
|
879
|
-
.
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
}
|
|
884
|
-
|
|
885
|
-
.
|
|
886
|
-
|
|
887
|
-
}
|
|
888
|
-
|
|
889
|
-
.
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
}
|
|
893
|
-
|
|
894
|
-
.
|
|
895
|
-
|
|
896
|
-
}
|
|
897
|
-
|
|
898
|
-
.
|
|
899
|
-
|
|
900
|
-
}
|
|
901
|
-
|
|
902
|
-
.
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
.
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
}
|
|
911
|
-
|
|
912
|
-
.
|
|
913
|
-
|
|
914
|
-
}
|
|
915
|
-
|
|
916
|
-
.
|
|
917
|
-
|
|
918
|
-
}
|
|
919
|
-
|
|
920
|
-
.
|
|
921
|
-
|
|
922
|
-
}
|
|
923
|
-
|
|
924
|
-
.error {
|
|
925
|
-
@apply text-red-600 text-sm mt-1;
|
|
926
|
-
}
|
|
1057
|
+
.page { @apply flex flex-col min-h-screen; }
|
|
1058
|
+
.header { @apply sticky top-0 z-10 border-b border-gray-200 bg-white/80 backdrop-blur-md; }
|
|
1059
|
+
.header-inner { @apply max-w-6xl mx-auto px-6 h-16 flex items-center justify-between; }
|
|
1060
|
+
.brand { @apply flex items-center gap-2 font-bold text-gray-900 no-underline; }
|
|
1061
|
+
.nav { @apply flex items-center gap-1; }
|
|
1062
|
+
.nav-link { @apply px-3 py-1.5 rounded-md text-sm font-medium text-gray-500 no-underline hover:bg-gray-100 hover:text-gray-900 transition-colors; }
|
|
1063
|
+
.main { @apply flex-1 px-6 py-12; }
|
|
1064
|
+
.container { @apply max-w-6xl mx-auto; }
|
|
1065
|
+
.card { @apply bg-white border border-gray-200 rounded-xl p-6; }
|
|
1066
|
+
|
|
1067
|
+
.btn { @apply inline-flex items-center justify-center gap-2 px-4 py-2 rounded-lg border text-sm font-medium no-underline cursor-pointer transition-colors whitespace-nowrap; }
|
|
1068
|
+
.btn-primary { @apply bg-gray-900 text-white border-gray-900 hover:bg-gray-700; }
|
|
1069
|
+
.btn-secondary { @apply bg-white text-gray-900 border-gray-200 hover:bg-gray-50; }
|
|
1070
|
+
.btn-square { @apply w-10 h-10 p-0 text-lg; }
|
|
1071
|
+
.btn-full { @apply w-full; }
|
|
1072
|
+
|
|
1073
|
+
.hero { @apply text-center py-20 max-w-2xl mx-auto; }
|
|
1074
|
+
.hero-title { @apply text-6xl font-extrabold tracking-tight mb-4; }
|
|
1075
|
+
.hero-subtitle { @apply text-lg text-gray-500 mb-8 max-w-lg mx-auto; }
|
|
1076
|
+
.hero-actions { @apply flex gap-3 justify-center flex-wrap; }
|
|
1077
|
+
|
|
1078
|
+
.features { @apply grid gap-4 mt-16; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); }
|
|
1079
|
+
.feature-card { @apply bg-white border border-gray-200 rounded-xl p-6; }
|
|
1080
|
+
.feature-icon { @apply text-2xl block mb-3; }
|
|
1081
|
+
.feature-title { @apply text-base font-semibold mb-2; }
|
|
1082
|
+
.feature-body { @apply text-sm text-gray-500; }
|
|
1083
|
+
|
|
1084
|
+
.page-header { @apply mb-8; }
|
|
1085
|
+
.page-subtitle { @apply text-gray-500 mt-2; }
|
|
1086
|
+
|
|
1087
|
+
.counter { @apply grid items-center gap-4; grid-template-columns: auto 1fr auto; }
|
|
1088
|
+
.counter-value { @apply text-center text-5xl font-bold tracking-tight; }
|
|
1089
|
+
|
|
1090
|
+
.auth-page { @apply flex justify-center pt-12; }
|
|
1091
|
+
.auth-card { @apply bg-white border border-gray-200 rounded-2xl p-10 w-full max-w-sm; }
|
|
1092
|
+
.auth-header { @apply text-center mb-8; }
|
|
1093
|
+
.auth-logo { @apply text-2xl block mb-4; }
|
|
1094
|
+
.auth-title { @apply text-2xl font-bold tracking-tight mb-2; }
|
|
1095
|
+
.auth-subtitle { @apply text-sm text-gray-500; }
|
|
1096
|
+
.auth-body { @apply flex flex-col gap-3; }
|
|
1097
|
+
.auth-footer { @apply text-center text-xs text-gray-400 mt-6; }
|
|
1098
|
+
.avatar { @apply inline-flex items-center justify-center w-14 h-14 rounded-full bg-gray-900 text-white text-2xl font-bold mb-4; }
|
|
1099
|
+
|
|
1100
|
+
.error-page { @apply text-center py-20; }
|
|
1101
|
+
.error-code { @apply text-8xl font-extrabold text-gray-200 mb-2; }
|
|
1102
|
+
.error-message { @apply text-gray-500 mb-8; }
|
|
1103
|
+
|
|
1104
|
+
.field { @apply flex flex-col gap-1.5; }
|
|
1105
|
+
.input { @apply w-full rounded-lg border border-gray-300 px-3.5 py-2.5 text-sm bg-white focus:outline-none focus:border-gray-900 transition-colors; }
|
|
1106
|
+
.error-hint { @apply text-red-600 text-xs mt-1; }
|
|
927
1107
|
}
|
|
928
1108
|
`
|
|
929
1109
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "elm-ssr",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.1",
|
|
4
4
|
"description": "Elm-first SSR library and framework for Cloudflare Workers (and Bun): file-based routes/islands, backend-neutral effect adapters (KV/D1/Redis/Postgres), background tasks (waitUntil/Queues), SQL-file migrations, CLI scaffold + build.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Michał Majchrzak <michmajchrzak@gmail.com>",
|